diff --git a/site/src/components/HelpTooltip/HelpTooltip.stories.tsx b/site/src/components/HelpTooltip/HelpTooltip.stories.tsx new file mode 100644 index 0000000000000..6780a5700a6d0 --- /dev/null +++ b/site/src/components/HelpTooltip/HelpTooltip.stories.tsx @@ -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 + +const Template: Story = (args) => ( + + What is template? + + 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. + + + Creating a template + Updating a template + + +) + +export const Close = Template.bind({}) + +export const Open = Template.bind({}) +Open.args = { + open: true, +} diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx new file mode 100644 index 0000000000000..8d14452ec2421 --- /dev/null +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -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 = ({ children, open, size = "medium" }) => { + const styles = useStyles({ size }) + const [anchorEl, setAnchorEl] = useState(null) + open = open ?? Boolean(anchorEl) + const id = open ? "help-popover" : undefined + + return ( + <> + + { + setAnchorEl(null) + }} + anchorOrigin={{ + vertical: "bottom", + horizontal: "left", + }} + transformOrigin={{ + vertical: "top", + horizontal: "left", + }} + > + {children} + + + ) +} + +export const HelpTooltipTitle: React.FC = ({ children }) => { + const styles = useStyles() + + return

{children}

+} + +export const HelpTooltipText: React.FC = ({ children }) => { + const styles = useStyles() + + return

{children}

+} + +export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href }) => { + const styles = useStyles() + + return ( + + + {children} + + ) +} + +export const HelpTooltipLinksGroup: React.FC = ({ children }) => { + const styles = useStyles() + + return ( + + {children} + + ) +} + +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), + }, +})) diff --git a/site/src/components/PageHeader/PageHeader.stories.tsx b/site/src/components/PageHeader/PageHeader.stories.tsx new file mode 100644 index 0000000000000..be1a594fd4381 --- /dev/null +++ b/site/src/components/PageHeader/PageHeader.stories.tsx @@ -0,0 +1,15 @@ +import { ComponentMeta, Story } from "@storybook/react" +import { PageHeader, PageHeaderTitle } from "./PageHeader" + +export default { + title: "components/PageHeader", + component: PageHeader, +} as ComponentMeta + +const Template: Story = () => ( + + Templates + +) + +export const Example = Template.bind({}) diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx new file mode 100644 index 0000000000000..9a6416972dd53 --- /dev/null +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -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 = ({ children, actions }) => { + const styles = useStyles() + + return ( +
+
{children}
+ + {actions} + +
+ ) +} + +export const PageHeaderTitle: React.FC = ({ children }) => { + const styles = useStyles() + + return

{children}

+} + +export const PageHeaderSubtitle: React.FC = ({ children }) => { + const styles = useStyles() + + return

{children}

+} + +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", + }, +})) diff --git a/site/src/components/Resources/Resources.tsx b/site/src/components/Resources/Resources.tsx index 73ae16713d1f5..8929457e1ef19 100644 --- a/site/src/components/Resources/Resources.tsx +++ b/site/src/components/Resources/Resources.tsx @@ -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 ( + + {Language.resourceTooltipTitle} + {Language.resourceTooltipText} + + + {Language.resourceTooltipLink} + + + + ) +} + +const AgentHelpTooltip: React.FC = () => { + return ( + + {Language.agentTooltipTitle} + {Language.agentTooltipTitle} + + ) } interface ResourcesProps { @@ -41,8 +77,18 @@ export const Resources: FC = ({ resources, getResourcesError, wo - {Language.resourceLabel} - {Language.agentLabel} + + + {Language.resourceLabel} + + + + + + {Language.agentLabel} + + + {Language.accessLabel} {Language.statusLabel} diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index dc91c716f863c..e0089c06f0382 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -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 = ({ children, className, direction = "column", spacing = 2 }) => { - const styles = useStyles({ spacing, direction }) +export const Stack: FC = ({ children, className, direction = "column", spacing = 2, alignItems }) => { + const styles = useStyles({ spacing, direction, alignItems }) return
{children}
} diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 3903dcdd2a1c6..0646421e5be78 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -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 = ({ const styles = useStyles() return ( -
- - -
-
- - {workspace.name} - - - - {workspace.owner_name} - -
- - -
-
- - -
+ + + } + > + {workspace.name} + {workspace.owner_name} + @@ -95,16 +85,12 @@ export const Workspace: FC = ({ -
+ ) } export const useStyles = makeStyles((theme) => { return { - root: { - display: "flex", - flexDirection: "column", - }, firstColumnSpacer: { flex: 2, }, diff --git a/site/src/pages/TemplatePage/TemplatePageView.tsx b/site/src/pages/TemplatePage/TemplatePageView.tsx index 83d9ac64f5608..c9b572041a877 100644 --- a/site/src/pages/TemplatePage/TemplatePageView.tsx +++ b/site/src/pages/TemplatePage/TemplatePageView.tsx @@ -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 = ({ template, activeTe return ( -
-
- - {template.name} - - - - {template.description === "" ? Language.noDescription : template.description} - -
- -
+ -
-
+ } + > + {template.name} + + {" "} + {template.description === "" ? Language.noDescription : template.description} + + @@ -83,38 +78,6 @@ export const TemplatePageView: FC = ({ 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, }, diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index e7629627d1216..c78e09be89f43 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -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 ( + + {Language.templateTooltipTitle} + {Language.templateTooltipText} + + + {Language.templateTooltipLink} + + + + ) } export interface TemplatesPageViewProps { @@ -47,56 +72,60 @@ export interface TemplatesPageViewProps { export const TemplatesPageView: FC = (props) => { const styles = useStyles() return ( - - -
- + + + + + Templates + + + + + +
+ + + {Language.nameLabel} + {Language.usedByLabel} + {Language.lastUpdatedLabel} + + + + {props.loading && } + {!props.loading && !props.templates?.length && ( - {Language.nameLabel} - {Language.usedByLabel} - {Language.lastUpdatedLabel} + + } + /> + - - - {props.loading && } - {!props.loading && !props.templates?.length && ( - - - } - /> - - - )} - {props.templates?.map((template) => ( - - - - + )} + {props.templates?.map((template) => ( + + + + - {Language.developerCount(template.workspace_owner_count)} + {Language.developerCount(template.workspace_owner_count)} - {dayjs().to(dayjs(template.updated_at))} - - ))} - -
- - + {dayjs().to(dayjs(template.updated_at))} + + ))} + + + ) } const useStyles = makeStyles((theme) => ({ - root: { - marginTop: theme.spacing(10), - }, emptyDescription: { maxWidth: theme.spacing(62), }, diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx index 59fceebf935b9..af315017c3462 100644 --- a/site/src/pages/UsersPage/UsersPageView.tsx +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -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 = ({ canCreateUser, isLoading, }) => { - const styles = useStyles() - return ( - - -
-
- {canCreateUser && ( - - )} -
-
- {error ? ( - - ) : ( - - )} -
-
+ + }> + {Language.createButton} + + ) : undefined + } + > + Users + + + {error ? ( + + ) : ( + + )} + ) } - -const useStyles = makeStyles((theme) => ({ - actions: { - marginTop: theme.spacing(3), - marginBottom: theme.spacing(3), - display: "flex", - height: theme.spacing(6), - - "& > *": { - marginLeft: "auto", - }, - }, -})) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 85d6ddbc0ae7d..46550ac62225a 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -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 ( - + <> {build ? pageTitle(`Build #${build.build_number} ยท ${build.workspace_name}`) : ""} - - - Logs - - {build && } - {!logs && } - {logs && } - - + + ) } - -const useStyles = makeStyles((theme) => ({ - title: { - paddingTop: theme.spacing(5), - paddingBottom: theme.spacing(2), - }, -})) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx new file mode 100644 index 0000000000000..24e47c97918ce --- /dev/null +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.stories.tsx @@ -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 + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + build: MockWorkspaceBuild, + logs: MockWorkspaceBuildLogs, + isWaitingForLogs: false, +} diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx new file mode 100644 index 0000000000000..5dd1e0a47c813 --- /dev/null +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -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 = ({ logs, build, isWaitingForLogs }) => { + return ( + + + Logs + + + + {build && } + {!logs && } + {logs && } + + + ) +} diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 8d9f3d30f08ab..a65fa95e55bc6 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -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" @@ -37,40 +35,37 @@ export const WorkspacePage: React.FC = () => { return } else { return ( - + <> {pageTitle(`${workspace.owner_name}/${workspace.name}`)} - - <> - { - 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} - /> - workspaceSend("CANCEL_DELETE")} - handleConfirm={() => { - workspaceSend("DELETE") - navigate("/workspaces") - }} - /> - - - + + { + 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} + /> + workspaceSend("CANCEL_DELETE")} + handleConfirm={() => { + workspaceSend("DELETE") + navigate("/workspaces") + }} + /> + ) } } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 1df0d52342306..b3c47f173e46a 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -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 - const WorkspacesPage: FC = () => { - const styles = useStyles() const [workspacesState, send] = useMachine(workspacesMachine) - const form = useFormik({ - initialValues: { - query: workspacesState.context.filter || "", - }, - onSubmit: (values) => { - send({ - type: "SET_FILTER", - query: values.query, - }) - }, - }) - - const getFieldHelpers = getFormHelpers(form) - - const [anchorEl, setAnchorEl] = useState(null) - - const handleClick = (event: React.MouseEvent) => { - 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 ( - + <> {pageTitle("Workspaces")} - - - - -
- - - - ), - }} - /> - - - - {Language.yourWorkspacesButton} - {Language.allWorkspacesButton} - -
-
- - - - -
- -
+ { + 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 diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 2a7c8a22b4a40..555ea0d0d3a00 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -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 ( + + {Language.workspaceTooltipTitle} + {Language.workspaceTooltipTitle} + + + {Language.workspaceTooltipLink1} + + + {Language.workspaceTooltipLink2} + + + {Language.workspaceTooltipLink3} + + + + ) +} + +interface FilterFormValues { + query: string +} + +export type FilterFormErrors = FormikErrors + export interface WorkspacesPageViewProps { loading?: boolean workspaces?: TypesGen.Workspace[] + filter?: string + onFilter: (query: string) => void } -export const WorkspacesPageView: FC = ({ loading, workspaces }) => { - useStyles() +export const WorkspacesPageView: FC = ({ loading, workspaces, filter, onFilter }) => { + const styles = useStyles() const theme: Theme = useTheme() + const form = useFormik({ + initialValues: { + query: filter ?? "", + }, + onSubmit: ({ query }) => { + onFilter(query) + }, + }) + + const getFieldHelpers = getFormHelpers(form) + + const [anchorEl, setAnchorEl] = useState(null) + + const handleClick = (event: React.MouseEvent) => { + 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 ( - + + + + + } + > + + + Workspaces + + + + + + + + +
+ + + + ), + }} + /> + + + + {Language.yourWorkspacesButton} + {Language.allWorkspacesButton} + +
+ @@ -98,7 +247,7 @@ export const WorkspacesPageView: FC = ({ loading, works })}
-
+ ) } @@ -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", + }, + }, }))