mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
feat: Add helpful tooltips for the key features (#2097)
This commit is contained in:
35
site/src/components/HelpTooltip/HelpTooltip.stories.tsx
Normal file
35
site/src/components/HelpTooltip/HelpTooltip.stories.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipProps,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "./HelpTooltip"
|
||||
|
||||
export default {
|
||||
title: "components/HelpTooltip",
|
||||
component: HelpTooltip,
|
||||
} as ComponentMeta<typeof HelpTooltip>
|
||||
|
||||
const Template: Story<HelpTooltipProps> = (args) => (
|
||||
<HelpTooltip {...args}>
|
||||
<HelpTooltipTitle>What is template?</HelpTooltipTitle>
|
||||
<HelpTooltipText>
|
||||
With templates you can create a common configuration for your workspaces using Terraform. So, you and your team
|
||||
can use the same environment to deliver great software.
|
||||
</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/">Creating a template</HelpTooltipLink>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/">Updating a template</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltip>
|
||||
)
|
||||
|
||||
export const Close = Template.bind({})
|
||||
|
||||
export const Open = Template.bind({})
|
||||
Open.args = {
|
||||
open: true,
|
||||
}
|
159
site/src/components/HelpTooltip/HelpTooltip.tsx
Normal file
159
site/src/components/HelpTooltip/HelpTooltip.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import Link from "@material-ui/core/Link"
|
||||
import Popover from "@material-ui/core/Popover"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import HelpIcon from "@material-ui/icons/HelpOutline"
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew"
|
||||
import { useState } from "react"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
type Size = "small" | "medium"
|
||||
export interface HelpTooltipProps {
|
||||
// Useful to test on storybook
|
||||
open?: boolean
|
||||
size?: Size
|
||||
}
|
||||
|
||||
export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size = "medium" }) => {
|
||||
const styles = useStyles({ size })
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
|
||||
open = open ?? Boolean(anchorEl)
|
||||
const id = open ? "help-popover" : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<button aria-describedby={id} className={styles.button} onClick={(event) => setAnchorEl(event.currentTarget)}>
|
||||
<HelpIcon className={styles.icon} />
|
||||
</button>
|
||||
<Popover
|
||||
classes={{ paper: styles.popoverPaper }}
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => {
|
||||
setAnchorEl(null)
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const HelpTooltipTitle: React.FC = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return <h4 className={styles.title}>{children}</h4>
|
||||
}
|
||||
|
||||
export const HelpTooltipText: React.FC = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return <p className={styles.text}>{children}</p>
|
||||
}
|
||||
|
||||
export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Link href={href} target="_blank" rel="noreferrer" className={styles.link}>
|
||||
<OpenInNewIcon className={styles.linkIcon} />
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export const HelpTooltipLinksGroup: React.FC = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Stack spacing={1} className={styles.linksGroup}>
|
||||
{children}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const getButtonSpacingFromSize = (size?: Size): number => {
|
||||
switch (size) {
|
||||
case "small":
|
||||
return 2.75
|
||||
case "medium":
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
const getIconSpacingFromSize = (size?: Size): number => {
|
||||
switch (size) {
|
||||
case "small":
|
||||
return 1.75
|
||||
case "medium":
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
button: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: ({ size }: { size?: Size }) => theme.spacing(getButtonSpacingFromSize(size)),
|
||||
height: ({ size }: { size?: Size }) => theme.spacing(getButtonSpacingFromSize(size)),
|
||||
padding: 0,
|
||||
border: 0,
|
||||
background: "transparent",
|
||||
color: theme.palette.text.secondary,
|
||||
cursor: "pointer",
|
||||
|
||||
"&:hover": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
},
|
||||
|
||||
icon: {
|
||||
width: ({ size }: { size?: Size }) => theme.spacing(getIconSpacingFromSize(size)),
|
||||
height: ({ size }: { size?: Size }) => theme.spacing(getIconSpacingFromSize(size)),
|
||||
},
|
||||
|
||||
popoverPaper: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
width: theme.spacing(38),
|
||||
padding: theme.spacing(2.5),
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
title: {
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
|
||||
text: {
|
||||
marginTop: theme.spacing(0.5),
|
||||
marginBottom: theme.spacing(0.5),
|
||||
},
|
||||
|
||||
link: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
|
||||
linkIcon: {
|
||||
color: "inherit",
|
||||
width: 14,
|
||||
height: 14,
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
|
||||
linksGroup: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}))
|
15
site/src/components/PageHeader/PageHeader.stories.tsx
Normal file
15
site/src/components/PageHeader/PageHeader.stories.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { PageHeader, PageHeaderTitle } from "./PageHeader"
|
||||
|
||||
export default {
|
||||
title: "components/PageHeader",
|
||||
component: PageHeader,
|
||||
} as ComponentMeta<typeof PageHeader>
|
||||
|
||||
const Template: Story = () => (
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>Templates</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
62
site/src/components/PageHeader/PageHeader.tsx
Normal file
62
site/src/components/PageHeader/PageHeader.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
export interface PageHeaderProps {
|
||||
actions?: JSX.Element
|
||||
}
|
||||
|
||||
export const PageHeader: React.FC<PageHeaderProps> = ({ children, actions }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<hgroup>{children}</hgroup>
|
||||
<Stack direction="row" className={styles.actions}>
|
||||
{actions}
|
||||
</Stack>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const PageHeaderTitle: React.FC = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return <h1 className={styles.title}>{children}</h1>
|
||||
}
|
||||
|
||||
export const PageHeaderSubtitle: React.FC = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return <h2 className={styles.subtitle}>{children}</h2>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingTop: theme.spacing(6),
|
||||
paddingBottom: theme.spacing(5),
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: theme.spacing(4),
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
lineHeight: "140%",
|
||||
},
|
||||
|
||||
subtitle: {
|
||||
fontSize: theme.spacing(2.5),
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 400,
|
||||
display: "block",
|
||||
margin: 0,
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
|
||||
actions: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
}))
|
@ -9,6 +9,13 @@ import { FC } from "react"
|
||||
import { Workspace, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { getDisplayAgentStatus } from "../../util/workspace"
|
||||
import { AppLink } from "../AppLink/AppLink"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "../HelpTooltip/HelpTooltip"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { TableHeaderRow } from "../TableHeaders/TableHeaders"
|
||||
import { TerminalLink } from "../TerminalLink/TerminalLink"
|
||||
@ -21,6 +28,35 @@ const Language = {
|
||||
agentLabel: "Agent",
|
||||
statusLabel: "Status",
|
||||
accessLabel: "Access",
|
||||
resourceTooltipTitle: "What is a resource?",
|
||||
resourceTooltipText: "A resource is an infrastructure object that is create when the workspace is provisioned.",
|
||||
resourceTooltipLink: "Persistent and ephemeral resources",
|
||||
agentTooltipTitle: "What is an agent?",
|
||||
agentTooltipText:
|
||||
"The Coder agent runs inside your resource and gives you direct access to the shell via the UI or CLI.",
|
||||
}
|
||||
|
||||
const ResourcesHelpTooltip: React.FC = () => {
|
||||
return (
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>{Language.resourceTooltipTitle}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.resourceTooltipText}</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/templates.md#persistent-and-ephemeral-resources">
|
||||
{Language.resourceTooltipLink}
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const AgentHelpTooltip: React.FC = () => {
|
||||
return (
|
||||
<HelpTooltip size="small">
|
||||
<HelpTooltipTitle>{Language.agentTooltipTitle}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.agentTooltipTitle}</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
interface ResourcesProps {
|
||||
@ -41,8 +77,18 @@ export const Resources: FC<ResourcesProps> = ({ resources, getResourcesError, wo
|
||||
<Table className={styles.table}>
|
||||
<TableHead>
|
||||
<TableHeaderRow>
|
||||
<TableCell>{Language.resourceLabel}</TableCell>
|
||||
<TableCell className={styles.agentColumn}>{Language.agentLabel}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
{Language.resourceLabel}
|
||||
<ResourcesHelpTooltip />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell className={styles.agentColumn}>
|
||||
<Stack direction="row" spacing={0.5} alignItems="center">
|
||||
{Language.agentLabel}
|
||||
<AgentHelpTooltip />
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>{Language.accessLabel}</TableCell>
|
||||
<TableCell>{Language.statusLabel}</TableCell>
|
||||
</TableHeaderRow>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { CSSProperties } from "@material-ui/core/styles/withStyles"
|
||||
import { FC } from "react"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
|
||||
@ -7,6 +8,7 @@ type Direction = "column" | "row"
|
||||
interface StyleProps {
|
||||
direction: Direction
|
||||
spacing: number
|
||||
alignItems?: CSSProperties["alignItems"]
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
@ -14,6 +16,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
display: "flex",
|
||||
flexDirection: ({ direction }: StyleProps) => direction,
|
||||
gap: ({ spacing }: StyleProps) => theme.spacing(spacing),
|
||||
alignItems: ({ alignItems }: StyleProps) => alignItems,
|
||||
},
|
||||
}))
|
||||
|
||||
@ -21,10 +24,11 @@ export interface StackProps {
|
||||
className?: string
|
||||
direction?: Direction
|
||||
spacing?: number
|
||||
alignItems?: CSSProperties["alignItems"]
|
||||
}
|
||||
|
||||
export const Stack: FC<StackProps> = ({ children, className, direction = "column", spacing = 2 }) => {
|
||||
const styles = useStyles({ spacing, direction })
|
||||
export const Stack: FC<StackProps> = ({ children, className, direction = "column", spacing = 2, alignItems }) => {
|
||||
const styles = useStyles({ spacing, direction, alignItems })
|
||||
|
||||
return <div className={combineClasses([styles.stack, className])}>{children}</div>
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import { FC } from "react"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||
import { BuildsTable } from "../BuildsTable/BuildsTable"
|
||||
import { Margins } from "../Margins/Margins"
|
||||
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../PageHeader/PageHeader"
|
||||
import { Resources } from "../Resources/Resources"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
|
||||
@ -46,33 +47,22 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Stack direction="column" className={styles.firstColumnSpacer} spacing={3}>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<Typography variant="h4" className={styles.title}>
|
||||
{workspace.name}
|
||||
</Typography>
|
||||
|
||||
<Typography color="textSecondary" className={styles.subtitle}>
|
||||
{workspace.owner_name}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stack direction="column" className={styles.secondColumnSpacer} spacing={3}></Stack>
|
||||
</Stack>
|
||||
<Margins>
|
||||
<PageHeader
|
||||
actions={
|
||||
<WorkspaceActions
|
||||
workspace={workspace}
|
||||
handleStart={handleStart}
|
||||
handleStop={handleStop}
|
||||
handleDelete={handleDelete}
|
||||
handleUpdate={handleUpdate}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack direction="row" spacing={3}>
|
||||
<Stack direction="column" className={styles.firstColumnSpacer} spacing={3}>
|
||||
@ -95,16 +85,12 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
<WorkspaceSchedule workspace={workspace} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
export const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
firstColumnSpacer: {
|
||||
flex: 2,
|
||||
},
|
||||
|
@ -1,7 +1,6 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import frontMatter from "front-matter"
|
||||
import { FC } from "react"
|
||||
@ -9,11 +8,11 @@ import ReactMarkdown from "react-markdown"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { Template, TemplateVersion, WorkspaceResource } from "../../api/typesGenerated"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TemplateResourcesTable } from "../../components/TemplateResourcesTable/TemplateResourcesTable"
|
||||
import { TemplateStats } from "../../components/TemplateStats/TemplateStats"
|
||||
import { WorkspaceSection } from "../../components/WorkspaceSection/WorkspaceSection"
|
||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||
|
||||
const Language = {
|
||||
createButton: "Create workspace",
|
||||
@ -38,23 +37,19 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<div className={styles.header}>
|
||||
<div>
|
||||
<Typography variant="h4" className={styles.title}>
|
||||
{template.name}
|
||||
</Typography>
|
||||
|
||||
<Typography color="textSecondary" className={styles.subtitle}>
|
||||
{template.description === "" ? Language.noDescription : template.description}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div className={styles.headerActions}>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Link underline="none" component={RouterLink} to={`/workspaces/new?template=${template.name}`}>
|
||||
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>{template.name}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>
|
||||
{" "}
|
||||
{template.description === "" ? Language.noDescription : template.description}
|
||||
</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack spacing={3}>
|
||||
<TemplateStats template={template} activeVersion={activeTemplateVersion} />
|
||||
@ -83,38 +78,6 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({ template, activeTe
|
||||
|
||||
export const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
root: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
header: {
|
||||
paddingTop: theme.spacing(5),
|
||||
paddingBottom: theme.spacing(5),
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
headerActions: {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
title: {
|
||||
fontWeight: 600,
|
||||
fontFamily: "inherit",
|
||||
},
|
||||
subtitle: {
|
||||
fontFamily: "inherit",
|
||||
marginTop: theme.spacing(0.5),
|
||||
},
|
||||
layout: {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
main: {
|
||||
width: "100%",
|
||||
},
|
||||
sidebar: {
|
||||
width: theme.spacing(32),
|
||||
flexShrink: 0,
|
||||
},
|
||||
readmeContents: {
|
||||
margin: 0,
|
||||
},
|
||||
|
@ -12,7 +12,15 @@ import * as TypesGen from "../../api/typesGenerated"
|
||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||
import { CodeExample } from "../../components/CodeExample/CodeExample"
|
||||
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "../../components/HelpTooltip/HelpTooltip"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||
|
||||
@ -36,6 +44,23 @@ export const Language = {
|
||||
or use a built-in template using the following Coder CLI command:
|
||||
</>
|
||||
),
|
||||
templateTooltipTitle: "What is template?",
|
||||
templateTooltipText: "With templates you can create a common configuration for your workspaces using Terraform.",
|
||||
templateTooltipLink: "Manage templates",
|
||||
}
|
||||
|
||||
const TemplateHelpTooltip: React.FC = () => {
|
||||
return (
|
||||
<HelpTooltip>
|
||||
<HelpTooltipTitle>{Language.templateTooltipTitle}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.templateTooltipText}</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/templates.md#manage-templates">
|
||||
{Language.templateTooltipLink}
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export interface TemplatesPageViewProps {
|
||||
@ -47,56 +72,60 @@ export interface TemplatesPageViewProps {
|
||||
export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Stack spacing={4} className={styles.root}>
|
||||
<Margins>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<Margins>
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>
|
||||
<Stack spacing={1} direction="row" alignItems="center">
|
||||
Templates
|
||||
<TemplateHelpTooltip />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{Language.nameLabel}</TableCell>
|
||||
<TableCell>{Language.usedByLabel}</TableCell>
|
||||
<TableCell>{Language.lastUpdatedLabel}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.loading && <TableLoader />}
|
||||
{!props.loading && !props.templates?.length && (
|
||||
<TableRow>
|
||||
<TableCell>{Language.nameLabel}</TableCell>
|
||||
<TableCell>{Language.usedByLabel}</TableCell>
|
||||
<TableCell>{Language.lastUpdatedLabel}</TableCell>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={Language.emptyMessage}
|
||||
description={props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms}
|
||||
descriptionClassName={styles.emptyDescription}
|
||||
cta={<CodeExample code="coder template init" />}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.loading && <TableLoader />}
|
||||
{!props.loading && !props.templates?.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<EmptyState
|
||||
message={Language.emptyMessage}
|
||||
description={props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms}
|
||||
descriptionClassName={styles.emptyDescription}
|
||||
cta={<CodeExample code="coder template init" />}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{props.templates?.map((template) => (
|
||||
<TableRow key={template.id}>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
title={template.name}
|
||||
subtitle={template.description}
|
||||
link={`/templates/${template.name}`}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
{props.templates?.map((template) => (
|
||||
<TableRow key={template.id}>
|
||||
<TableCell>
|
||||
<AvatarData
|
||||
title={template.name}
|
||||
subtitle={template.description}
|
||||
link={`/templates/${template.name}`}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell>
|
||||
<TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell>
|
||||
|
||||
<TableCell data-chromatic="ignore">{dayjs().to(dayjs(template.updated_at))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Margins>
|
||||
</Stack>
|
||||
<TableCell data-chromatic="ignore">{dayjs().to(dayjs(template.updated_at))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
marginTop: theme.spacing(10),
|
||||
},
|
||||
emptyDescription: {
|
||||
maxWidth: theme.spacing(62),
|
||||
},
|
||||
|
@ -1,11 +1,10 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import { FC } from "react"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { UsersTable } from "../../components/UsersTable/UsersTable"
|
||||
|
||||
export const Language = {
|
||||
@ -40,48 +39,34 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
|
||||
canCreateUser,
|
||||
isLoading,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Margins>
|
||||
<div className={styles.actions}>
|
||||
<div>
|
||||
{canCreateUser && (
|
||||
<Button onClick={openUserCreationDialog} startIcon={<AddCircleOutline />}>
|
||||
{Language.createButton}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{error ? (
|
||||
<ErrorSummary error={error} />
|
||||
) : (
|
||||
<UsersTable
|
||||
users={users}
|
||||
roles={roles}
|
||||
onSuspendUser={onSuspendUser}
|
||||
onResetUserPassword={onResetUserPassword}
|
||||
onUpdateUserRoles={onUpdateUserRoles}
|
||||
isUpdatingUserRoles={isUpdatingUserRoles}
|
||||
canEditUsers={canEditUsers}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</Margins>
|
||||
</Stack>
|
||||
<Margins>
|
||||
<PageHeader
|
||||
actions={
|
||||
canCreateUser ? (
|
||||
<Button onClick={openUserCreationDialog} startIcon={<AddCircleOutline />}>
|
||||
{Language.createButton}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>Users</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
{error ? (
|
||||
<ErrorSummary error={error} />
|
||||
) : (
|
||||
<UsersTable
|
||||
users={users}
|
||||
roles={roles}
|
||||
onSuspendUser={onSuspendUser}
|
||||
onResetUserPassword={onResetUserPassword}
|
||||
onUpdateUserRoles={onUpdateUserRoles}
|
||||
isUpdatingUserRoles={isUpdatingUserRoles}
|
||||
canEditUsers={canEditUsers}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)}
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actions: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
display: "flex",
|
||||
height: theme.spacing(6),
|
||||
|
||||
"& > *": {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
@ -1,21 +1,10 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { ProvisionerJobLog } from "../../api/typesGenerated"
|
||||
import { Loader } from "../../components/Loader/Loader"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||
import { WorkspaceBuildStats } from "../../components/WorkspaceBuildStats/WorkspaceBuildStats"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService"
|
||||
|
||||
const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => {
|
||||
return [...logs].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
}
|
||||
import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"
|
||||
|
||||
const useBuildId = () => {
|
||||
const { buildId } = useParams()
|
||||
@ -32,29 +21,14 @@ export const WorkspaceBuildPage: FC = () => {
|
||||
const [buildState] = useMachine(workspaceBuildMachine, { context: { buildId } })
|
||||
const { logs, build } = buildState.context
|
||||
const isWaitingForLogs = !buildState.matches("logs.loaded")
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{build ? pageTitle(`Build #${build.build_number} · ${build.workspace_name}`) : ""}</title>
|
||||
</Helmet>
|
||||
<Stack>
|
||||
<Typography variant="h4" className={styles.title}>
|
||||
Logs
|
||||
</Typography>
|
||||
|
||||
{build && <WorkspaceBuildStats build={build} />}
|
||||
{!logs && <Loader />}
|
||||
{logs && <WorkspaceBuildLogs logs={sortLogsByCreatedAt(logs)} isWaitingForLogs={isWaitingForLogs} />}
|
||||
</Stack>
|
||||
</Margins>
|
||||
<WorkspaceBuildPageView logs={logs} build={build} isWaitingForLogs={isWaitingForLogs} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
paddingTop: theme.spacing(5),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
}))
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { MockWorkspaceBuild, MockWorkspaceBuildLogs } from "../../testHelpers/entities"
|
||||
import { WorkspaceBuildPageView, WorkspaceBuildPageViewProps } from "./WorkspaceBuildPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/WorkspaceBuildPageView",
|
||||
component: WorkspaceBuildPageView,
|
||||
} as ComponentMeta<typeof WorkspaceBuildPageView>
|
||||
|
||||
const Template: Story<WorkspaceBuildPageViewProps> = (args) => <WorkspaceBuildPageView {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
build: MockWorkspaceBuild,
|
||||
logs: MockWorkspaceBuildLogs,
|
||||
isWaitingForLogs: false,
|
||||
}
|
34
site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx
Normal file
34
site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { FC } from "react"
|
||||
import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated"
|
||||
import { Loader } from "../../components/Loader/Loader"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||
import { WorkspaceBuildStats } from "../../components/WorkspaceBuildStats/WorkspaceBuildStats"
|
||||
|
||||
const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => {
|
||||
return [...logs].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())
|
||||
}
|
||||
|
||||
export interface WorkspaceBuildPageViewProps {
|
||||
logs: ProvisionerJobLog[] | undefined
|
||||
build: WorkspaceBuild | undefined
|
||||
isWaitingForLogs: boolean
|
||||
}
|
||||
|
||||
export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({ logs, build, isWaitingForLogs }) => {
|
||||
return (
|
||||
<Margins>
|
||||
<PageHeader>
|
||||
<PageHeaderTitle>Logs</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack>
|
||||
{build && <WorkspaceBuildStats build={build} />}
|
||||
{!logs && <Loader />}
|
||||
{logs && <WorkspaceBuildLogs logs={sortLogsByCreatedAt(logs)} isWaitingForLogs={isWaitingForLogs} />}
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
}
|
@ -5,8 +5,6 @@ import { useNavigate, useParams } from "react-router-dom"
|
||||
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
|
||||
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
|
||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { Workspace } from "../../components/Workspace/Workspace"
|
||||
import { firstOrItem } from "../../util/array"
|
||||
import { pageTitle } from "../../util/page"
|
||||
@ -38,40 +36,37 @@ export const WorkspacePage: React.FC = () => {
|
||||
return <FullScreenLoader />
|
||||
} else {
|
||||
return (
|
||||
<Margins>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(`${workspace.owner_name}/${workspace.name}`)}</title>
|
||||
</Helmet>
|
||||
<Stack spacing={4}>
|
||||
<>
|
||||
<Workspace
|
||||
bannerProps={{
|
||||
isLoading: bannerState.hasTag("loading"),
|
||||
onExtend: () => {
|
||||
bannerSend({ type: "EXTEND_DEADLINE_DEFAULT", workspaceId: workspace.id })
|
||||
},
|
||||
}}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend("START")}
|
||||
handleStop={() => workspaceSend("STOP")}
|
||||
handleDelete={() => workspaceSend("ASK_DELETE")}
|
||||
handleUpdate={() => workspaceSend("UPDATE")}
|
||||
handleCancel={() => workspaceSend("CANCEL")}
|
||||
resources={resources}
|
||||
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
|
||||
builds={builds}
|
||||
/>
|
||||
<DeleteWorkspaceDialog
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
handleCancel={() => workspaceSend("CANCEL_DELETE")}
|
||||
handleConfirm={() => {
|
||||
workspaceSend("DELETE")
|
||||
navigate("/workspaces")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</Stack>
|
||||
</Margins>
|
||||
|
||||
<Workspace
|
||||
bannerProps={{
|
||||
isLoading: bannerState.hasTag("loading"),
|
||||
onExtend: () => {
|
||||
bannerSend({ type: "EXTEND_DEADLINE_DEFAULT", workspaceId: workspace.id })
|
||||
},
|
||||
}}
|
||||
workspace={workspace}
|
||||
handleStart={() => workspaceSend("START")}
|
||||
handleStop={() => workspaceSend("STOP")}
|
||||
handleDelete={() => workspaceSend("ASK_DELETE")}
|
||||
handleUpdate={() => workspaceSend("UPDATE")}
|
||||
handleCancel={() => workspaceSend("CANCEL")}
|
||||
resources={resources}
|
||||
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
|
||||
builds={builds}
|
||||
/>
|
||||
<DeleteWorkspaceDialog
|
||||
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
|
||||
handleCancel={() => workspaceSend("CANCEL_DELETE")}
|
||||
handleConfirm={() => {
|
||||
workspaceSend("DELETE")
|
||||
navigate("/workspaces")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,174 +1,32 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Fade from "@material-ui/core/Fade"
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import Menu from "@material-ui/core/Menu"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import SearchIcon from "@material-ui/icons/Search"
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { FormikErrors, useFormik } from "formik"
|
||||
import { FC, useState } from "react"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { pageTitle } from "../../util/page"
|
||||
import { workspacesMachine } from "../../xServices/workspaces/workspacesXService"
|
||||
import { WorkspacesPageView } from "./WorkspacesPageView"
|
||||
|
||||
interface FilterFormValues {
|
||||
query: string
|
||||
}
|
||||
|
||||
const Language = {
|
||||
filterName: "Filters",
|
||||
createWorkspaceButton: "Create workspace",
|
||||
yourWorkspacesButton: "Your workspaces",
|
||||
allWorkspacesButton: "All workspaces",
|
||||
}
|
||||
|
||||
export type FilterFormErrors = FormikErrors<FilterFormValues>
|
||||
|
||||
const WorkspacesPage: FC = () => {
|
||||
const styles = useStyles()
|
||||
const [workspacesState, send] = useMachine(workspacesMachine)
|
||||
|
||||
const form = useFormik<FilterFormValues>({
|
||||
initialValues: {
|
||||
query: workspacesState.context.filter || "",
|
||||
},
|
||||
onSubmit: (values) => {
|
||||
send({
|
||||
type: "SET_FILTER",
|
||||
query: values.query,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const setYourWorkspaces = () => {
|
||||
void form.setFieldValue("query", "owner:me")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const setAllWorkspaces = () => {
|
||||
void form.setFieldValue("query", "")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle("Workspaces")}</title>
|
||||
</Helmet>
|
||||
<Stack direction="row" className={styles.workspacesHeaderContainer}>
|
||||
<Stack direction="column" className={styles.filterColumn}>
|
||||
<Stack direction="row" spacing={0} className={styles.filterContainer}>
|
||||
<Button
|
||||
aria-controls="filter-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
className={styles.buttonRoot}
|
||||
>
|
||||
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
|
||||
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
|
||||
<TextField
|
||||
{...getFieldHelpers("query")}
|
||||
className={styles.textFieldRoot}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Menu
|
||||
id="filter-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
TransitionComponent={Fade}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
|
||||
<MenuItem onClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
||||
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
|
||||
{Language.createWorkspaceButton}
|
||||
</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
<WorkspacesPageView loading={workspacesState.hasTag("loading")} workspaces={workspacesState.context.workspaces} />
|
||||
</Margins>
|
||||
<WorkspacesPageView
|
||||
filter={workspacesState.context.filter}
|
||||
loading={workspacesState.hasTag("loading")}
|
||||
workspaces={workspacesState.context.workspaces}
|
||||
onFilter={(query) => {
|
||||
send({
|
||||
type: "SET_FILTER",
|
||||
query,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
workspacesHeaderContainer: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
filterColumn: {
|
||||
width: "60%",
|
||||
cursor: "text",
|
||||
},
|
||||
filterContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: "6px",
|
||||
},
|
||||
filterForm: {
|
||||
width: "100%",
|
||||
},
|
||||
buttonRoot: {
|
||||
border: "none",
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: "6px 0px 0px 6px",
|
||||
},
|
||||
textFieldRoot: {
|
||||
margin: "0px",
|
||||
"& fieldset": {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
export default WorkspacesPage
|
||||
|
@ -1,22 +1,40 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Fade from "@material-ui/core/Fade"
|
||||
import InputAdornment from "@material-ui/core/InputAdornment"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import Menu from "@material-ui/core/Menu"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import { makeStyles, Theme } from "@material-ui/core/styles"
|
||||
import Table from "@material-ui/core/Table"
|
||||
import TableBody from "@material-ui/core/TableBody"
|
||||
import TableCell from "@material-ui/core/TableCell"
|
||||
import TableHead from "@material-ui/core/TableHead"
|
||||
import TableRow from "@material-ui/core/TableRow"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import SearchIcon from "@material-ui/icons/Search"
|
||||
import useTheme from "@material-ui/styles/useTheme"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import { FC } from "react"
|
||||
import { FormikErrors, useFormik } from "formik"
|
||||
import { FC, useState } from "react"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||
import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows"
|
||||
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||
import {
|
||||
HelpTooltip,
|
||||
HelpTooltipLink,
|
||||
HelpTooltipLinksGroup,
|
||||
HelpTooltipText,
|
||||
HelpTooltipTitle,
|
||||
} from "../../components/HelpTooltip/HelpTooltip"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderTitle } from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getDisplayStatus } from "../../util/workspace"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
@ -25,19 +43,150 @@ export const Language = {
|
||||
createButton: "Create workspace",
|
||||
emptyMessage: "Create your first workspace",
|
||||
emptyDescription: "Start editing your source code and building your software",
|
||||
filterName: "Filters",
|
||||
createWorkspaceButton: "Create workspace",
|
||||
yourWorkspacesButton: "Your workspaces",
|
||||
allWorkspacesButton: "All workspaces",
|
||||
workspaceTooltipTitle: "What is workspace?",
|
||||
workspaceTooltipText:
|
||||
"It is your workstation. It is a workspace that will provide you the necessary compute and access to your development environment.",
|
||||
workspaceTooltipLink1: "Create workspaces",
|
||||
workspaceTooltipLink2: "Connect with SSH",
|
||||
workspaceTooltipLink3: "Editors and IDEs",
|
||||
}
|
||||
|
||||
const WorkspaceHelpTooltip: React.FC = () => {
|
||||
return (
|
||||
<HelpTooltip>
|
||||
<HelpTooltipTitle>{Language.workspaceTooltipTitle}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.workspaceTooltipTitle}</HelpTooltipText>
|
||||
<HelpTooltipLinksGroup>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#create-workspaces">
|
||||
{Language.workspaceTooltipLink1}
|
||||
</HelpTooltipLink>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#connect-with-ssh">
|
||||
{Language.workspaceTooltipLink2}
|
||||
</HelpTooltipLink>
|
||||
<HelpTooltipLink href="https://github.com/coder/coder/blob/main/docs/workspaces.md#editors-and-ides">
|
||||
{Language.workspaceTooltipLink3}
|
||||
</HelpTooltipLink>
|
||||
</HelpTooltipLinksGroup>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
interface FilterFormValues {
|
||||
query: string
|
||||
}
|
||||
|
||||
export type FilterFormErrors = FormikErrors<FilterFormValues>
|
||||
|
||||
export interface WorkspacesPageViewProps {
|
||||
loading?: boolean
|
||||
workspaces?: TypesGen.Workspace[]
|
||||
filter?: string
|
||||
onFilter: (query: string) => void
|
||||
}
|
||||
|
||||
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaces }) => {
|
||||
useStyles()
|
||||
export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, workspaces, filter, onFilter }) => {
|
||||
const styles = useStyles()
|
||||
const theme: Theme = useTheme()
|
||||
|
||||
const form = useFormik<FilterFormValues>({
|
||||
initialValues: {
|
||||
query: filter ?? "",
|
||||
},
|
||||
onSubmit: ({ query }) => {
|
||||
onFilter(query)
|
||||
},
|
||||
})
|
||||
|
||||
const getFieldHelpers = getFormHelpers<FilterFormValues>(form)
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const setYourWorkspaces = () => {
|
||||
void form.setFieldValue("query", "owner:me")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const setAllWorkspaces = () => {
|
||||
void form.setFieldValue("query", "")
|
||||
void form.submitForm()
|
||||
handleClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Margins>
|
||||
<PageHeader
|
||||
actions={
|
||||
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
||||
<Button startIcon={<AddCircleOutline />} style={{ height: "44px" }}>
|
||||
{Language.createWorkspaceButton}
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>Workspaces</span>
|
||||
<WorkspaceHelpTooltip />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
<Stack direction="row" spacing={0} className={styles.filterContainer}>
|
||||
<Button aria-controls="filter-menu" aria-haspopup="true" onClick={handleClick} className={styles.buttonRoot}>
|
||||
{Language.filterName} {anchorEl ? <CloseDropdown /> : <OpenDropdown />}
|
||||
</Button>
|
||||
|
||||
<form onSubmit={form.handleSubmit} className={styles.filterForm}>
|
||||
<TextField
|
||||
{...getFieldHelpers("query")}
|
||||
className={styles.textFieldRoot}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon fontSize="small" />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
|
||||
<Menu
|
||||
id="filter-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
TransitionComponent={Fade}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={setYourWorkspaces}>{Language.yourWorkspacesButton}</MenuItem>
|
||||
<MenuItem onClick={setAllWorkspaces}>{Language.allWorkspacesButton}</MenuItem>
|
||||
</Menu>
|
||||
</Stack>
|
||||
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@ -98,7 +247,7 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({ loading, works
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
@ -110,6 +259,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& span": {
|
||||
maxWidth: 600,
|
||||
textAlign: "center",
|
||||
@ -117,4 +267,23 @@ const useStyles = makeStyles((theme) => ({
|
||||
lineHeight: `${theme.spacing(3)}px`,
|
||||
},
|
||||
},
|
||||
filterContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
filterForm: {
|
||||
width: "100%",
|
||||
},
|
||||
buttonRoot: {
|
||||
border: "none",
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
|
||||
},
|
||||
textFieldRoot: {
|
||||
margin: "0px",
|
||||
"& fieldset": {
|
||||
border: "none",
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
Reference in New Issue
Block a user