mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
feat: Improve empty states for workspaces and templates (#1950)
This commit is contained in:
@ -1,22 +1,25 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||||
|
import { combineClasses } from "../../util/combineClasses"
|
||||||
import { CopyButton } from "../CopyButton/CopyButton"
|
import { CopyButton } from "../CopyButton/CopyButton"
|
||||||
|
|
||||||
export interface CodeExampleProps {
|
export interface CodeExampleProps {
|
||||||
code: string
|
code: string
|
||||||
|
className?: string
|
||||||
|
buttonClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to show single-line code examples, with a copy button
|
* Component to show single-line code examples, with a copy button
|
||||||
*/
|
*/
|
||||||
export const CodeExample: FC<CodeExampleProps> = ({ code }) => {
|
export const CodeExample: FC<CodeExampleProps> = ({ code, className, buttonClassName }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={combineClasses([styles.root, className])}>
|
||||||
<code>{code}</code>
|
<code className={styles.code}>{code}</code>
|
||||||
<CopyButton text={code} />
|
<CopyButton text={code} buttonClassName={combineClasses([styles.button, buttonClassName])} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -30,8 +33,17 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
color: theme.palette.primary.contrastText,
|
color: theme.palette.primary.contrastText,
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||||
fontSize: 13,
|
fontSize: 14,
|
||||||
padding: theme.spacing(2),
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
padding: theme.spacing(0.5),
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
padding: `${theme.spacing(0.5)}px ${theme.spacing(0.75)}px ${theme.spacing(0.5)}px ${theme.spacing(2)}px`,
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
border: 0,
|
||||||
|
minWidth: 42,
|
||||||
|
minHeight: 42,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -2,13 +2,17 @@ import Box from "@material-ui/core/Box"
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import Typography from "@material-ui/core/Typography"
|
import Typography from "@material-ui/core/Typography"
|
||||||
import { FC, ReactNode } from "react"
|
import { FC, ReactNode } from "react"
|
||||||
|
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||||
|
import { combineClasses } from "../../util/combineClasses"
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
/** Text Message to display, placed inside Typography component */
|
/** Text Message to display, placed inside Typography component */
|
||||||
message: string
|
message: string
|
||||||
/** Longer optional description to display below the message */
|
/** Longer optional description to display below the message */
|
||||||
description?: string
|
description?: string | React.ReactNode
|
||||||
|
descriptionClassName?: string
|
||||||
cta?: ReactNode
|
cta?: ReactNode
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,17 +24,21 @@ export interface EmptyStateProps {
|
|||||||
* that you can directly pass props through to to customize the shape and layout of it.
|
* that you can directly pass props through to to customize the shape and layout of it.
|
||||||
*/
|
*/
|
||||||
export const EmptyState: FC<EmptyStateProps> = (props) => {
|
export const EmptyState: FC<EmptyStateProps> = (props) => {
|
||||||
const { message, description, cta, ...boxProps } = props
|
const { message, description, cta, descriptionClassName, className, ...boxProps } = props
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={styles.root} {...boxProps}>
|
<Box className={combineClasses([styles.root, className])} {...boxProps}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<Typography variant="h5" className={styles.title}>
|
<Typography variant="h5" className={styles.title}>
|
||||||
{message}
|
{message}
|
||||||
</Typography>
|
</Typography>
|
||||||
{description && (
|
{description && (
|
||||||
<Typography variant="body2" color="textSecondary" className={styles.description}>
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="textSecondary"
|
||||||
|
className={combineClasses([styles.description, descriptionClassName])}
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
@ -48,17 +56,20 @@ const useStyles = makeStyles(
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
minHeight: 120,
|
minHeight: 300,
|
||||||
padding: theme.spacing(3),
|
padding: theme.spacing(3),
|
||||||
|
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
marginBottom: theme.spacing(3),
|
marginBottom: theme.spacing(3),
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontWeight: 400,
|
fontWeight: 600,
|
||||||
|
fontFamily: "inherit",
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
|
fontFamily: "inherit",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ name: "EmptyState" },
|
{ name: "EmptyState" },
|
||||||
|
@ -27,7 +27,12 @@ export const Footer: React.FC = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
{buildInfoState.context.buildInfo && (
|
{buildInfoState.context.buildInfo && (
|
||||||
<div className={styles.buildInfo}>
|
<div className={styles.buildInfo}>
|
||||||
<Link variant="caption" target="_blank" href={buildInfoState.context.buildInfo.external_url}>
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
variant="caption"
|
||||||
|
target="_blank"
|
||||||
|
href={buildInfoState.context.buildInfo.external_url}
|
||||||
|
>
|
||||||
{Language.buildInfoText(buildInfoState.context.buildInfo)}
|
{Language.buildInfoText(buildInfoState.context.buildInfo)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -38,6 +43,7 @@ export const Footer: React.FC = ({ children }) => {
|
|||||||
|
|
||||||
const useFooterStyles = makeStyles((theme) => ({
|
const useFooterStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
|
opacity: 0.6,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
flex: "0",
|
flex: "0",
|
||||||
paddingTop: theme.spacing(2),
|
paddingTop: theme.spacing(2),
|
||||||
@ -50,4 +56,8 @@ const useFooterStyles = makeStyles((theme) => ({
|
|||||||
buildInfo: {
|
buildInfo: {
|
||||||
margin: theme.spacing(0.25),
|
margin: theme.spacing(0.25),
|
||||||
},
|
},
|
||||||
|
link: {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -33,6 +33,11 @@ export default {
|
|||||||
|
|
||||||
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
|
const Template: Story<CreateWorkspacePageViewProps> = (args) => <CreateWorkspacePageView {...args} />
|
||||||
|
|
||||||
|
export const NoTemplates = Template.bind({})
|
||||||
|
NoTemplates.args = {
|
||||||
|
templates: [],
|
||||||
|
}
|
||||||
|
|
||||||
export const NoParameters = Template.bind({})
|
export const NoParameters = Template.bind({})
|
||||||
NoParameters.args = {
|
NoParameters.args = {
|
||||||
templates: [MockTemplate],
|
templates: [MockTemplate],
|
||||||
|
@ -8,6 +8,8 @@ import { FC, useState } from "react"
|
|||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
|
import { CodeExample } from "../../components/CodeExample/CodeExample"
|
||||||
|
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||||
import { FormFooter } from "../../components/FormFooter/FormFooter"
|
import { FormFooter } from "../../components/FormFooter/FormFooter"
|
||||||
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
|
import { FullPageForm } from "../../components/FullPageForm/FullPageForm"
|
||||||
import { Loader } from "../../components/Loader/Loader"
|
import { Loader } from "../../components/Loader/Loader"
|
||||||
@ -18,6 +20,17 @@ import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formU
|
|||||||
export const Language = {
|
export const Language = {
|
||||||
templateLabel: "Template",
|
templateLabel: "Template",
|
||||||
nameLabel: "Name",
|
nameLabel: "Name",
|
||||||
|
emptyMessage: "Let's create your first template",
|
||||||
|
emptyDescription: (
|
||||||
|
<>
|
||||||
|
To create a workspace you need to have a template. You can{" "}
|
||||||
|
<Link target="_blank" href="https://github.com/coder/coder/blob/main/docs/templates.md">
|
||||||
|
create one from scratch
|
||||||
|
</Link>{" "}
|
||||||
|
or use a built-in template by typing the following Coder CLI command:
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
templateLink: "Read more about this template",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateWorkspacePageViewProps {
|
export interface CreateWorkspacePageViewProps {
|
||||||
@ -98,7 +111,18 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
|
|||||||
{props.loadingTemplates && <Loader />}
|
{props.loadingTemplates && <Loader />}
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
{props.templates && (
|
{props.templates && props.templates.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
className={styles.emptyState}
|
||||||
|
message={Language.emptyMessage}
|
||||||
|
description={Language.emptyDescription}
|
||||||
|
descriptionClassName={styles.emptyStateDescription}
|
||||||
|
cta={
|
||||||
|
<CodeExample className={styles.code} buttonClassName={styles.codeButton} code="coder template init" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.templates && props.templates.length > 0 && (
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("template_id")}
|
{...getFieldHelpers("template_id")}
|
||||||
disabled={form.isSubmitting}
|
disabled={form.isSubmitting}
|
||||||
@ -116,7 +140,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = (props)
|
|||||||
to={`/templates/${selectedTemplate.name}`}
|
to={`/templates/${selectedTemplate.name}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
Read more about this template <OpenInNewIcon />
|
{Language.templateLink} <OpenInNewIcon />
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -179,4 +203,21 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
marginLeft: theme.spacing(0.5),
|
marginLeft: theme.spacing(0.5),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
emptyState: {
|
||||||
|
padding: 0,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
textAlign: "left",
|
||||||
|
minHeight: "auto",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
},
|
||||||
|
emptyStateDescription: {
|
||||||
|
lineHeight: "160%",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
codeButton: {
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -31,7 +31,7 @@ describe("TemplatesPage", () => {
|
|||||||
render(<TemplatesPage />)
|
render(<TemplatesPage />)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await screen.findByText(Language.emptyViewCreate)
|
await screen.findByText(Language.emptyMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("renders a filled templates page", async () => {
|
it("renders a filled templates page", async () => {
|
||||||
|
@ -8,9 +8,10 @@ import TableRow from "@material-ui/core/TableRow"
|
|||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import relativeTime from "dayjs/plugin/relativeTime"
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||||
|
import { CodeExample } from "../../components/CodeExample/CodeExample"
|
||||||
|
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||||
import { Margins } from "../../components/Margins/Margins"
|
import { Margins } from "../../components/Margins/Margins"
|
||||||
import { Stack } from "../../components/Stack/Stack"
|
import { Stack } from "../../components/Stack/Stack"
|
||||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||||
@ -24,9 +25,17 @@ export const Language = {
|
|||||||
nameLabel: "Name",
|
nameLabel: "Name",
|
||||||
usedByLabel: "Used by",
|
usedByLabel: "Used by",
|
||||||
lastUpdatedLabel: "Last updated",
|
lastUpdatedLabel: "Last updated",
|
||||||
emptyViewCreateCTA: "Create a template",
|
emptyViewNoPerms: "Contact your Coder administrator to create a template. You can share the code below.",
|
||||||
emptyViewCreate: "to standardize development workspaces for your team.",
|
emptyMessage: "Create your first template",
|
||||||
emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.",
|
emptyDescription: (
|
||||||
|
<>
|
||||||
|
To create a workspace you need to have a template. You can{" "}
|
||||||
|
<Link target="_blank" href="https://github.com/coder/coder/blob/main/docs/templates.md">
|
||||||
|
create one from scratch
|
||||||
|
</Link>{" "}
|
||||||
|
or use a built-in template using the following Coder CLI command:
|
||||||
|
</>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplatesPageViewProps {
|
export interface TemplatesPageViewProps {
|
||||||
@ -53,18 +62,12 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
|||||||
{!props.loading && !props.templates?.length && (
|
{!props.loading && !props.templates?.length && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={999}>
|
<TableCell colSpan={999}>
|
||||||
<div className={styles.welcome}>
|
<EmptyState
|
||||||
{props.canCreateTemplate ? (
|
message={Language.emptyMessage}
|
||||||
<span>
|
description={props.canCreateTemplate ? Language.emptyDescription : Language.emptyViewNoPerms}
|
||||||
<Link component={RouterLink} to="/templates/new">
|
descriptionClassName={styles.emptyDescription}
|
||||||
{Language.emptyViewCreateCTA}
|
cta={<CodeExample code="coder template init" />}
|
||||||
</Link>
|
/>
|
||||||
{Language.emptyViewCreate}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span>{Language.emptyViewNoPerms}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@ -92,20 +95,9 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = (props) => {
|
|||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
marginTop: theme.spacing(3),
|
marginTop: theme.spacing(10),
|
||||||
},
|
|
||||||
welcome: {
|
|
||||||
paddingTop: theme.spacing(12),
|
|
||||||
paddingBottom: theme.spacing(12),
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
"& span": {
|
|
||||||
maxWidth: 600,
|
|
||||||
textAlign: "center",
|
|
||||||
fontSize: theme.spacing(2),
|
|
||||||
lineHeight: `${theme.spacing(3)}px`,
|
|
||||||
},
|
},
|
||||||
|
emptyDescription: {
|
||||||
|
maxWidth: theme.spacing(62),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -23,7 +23,7 @@ describe("WorkspacesPage", () => {
|
|||||||
render(<WorkspacesPage />)
|
render(<WorkspacesPage />)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
await screen.findByText(Language.emptyView)
|
await screen.findByText(Language.emptyMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("renders a filled workspaces page", async () => {
|
it("renders a filled workspaces page", async () => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ComponentMeta, Story } from "@storybook/react"
|
import { ComponentMeta, Story } from "@storybook/react"
|
||||||
import { ProvisionerJobStatus, Workspace } from "../../api/typesGenerated"
|
import { ProvisionerJobStatus, Workspace, WorkspaceTransition } from "../../api/typesGenerated"
|
||||||
import { MockWorkspace } from "../../testHelpers/entities"
|
import { MockWorkspace } from "../../testHelpers/entities"
|
||||||
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
|
import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView"
|
||||||
|
|
||||||
@ -10,7 +10,10 @@ export default {
|
|||||||
|
|
||||||
const Template: Story<WorkspacesPageViewProps> = (args) => <WorkspacesPageView {...args} />
|
const Template: Story<WorkspacesPageViewProps> = (args) => <WorkspacesPageView {...args} />
|
||||||
|
|
||||||
const createWorkspaceWithStatus = (status: ProvisionerJobStatus, transition = "start"): Workspace => {
|
const createWorkspaceWithStatus = (
|
||||||
|
status: ProvisionerJobStatus,
|
||||||
|
transition: WorkspaceTransition = "start",
|
||||||
|
): Workspace => {
|
||||||
return {
|
return {
|
||||||
...MockWorkspace,
|
...MockWorkspace,
|
||||||
latest_build: {
|
latest_build: {
|
||||||
@ -46,4 +49,6 @@ AllStates.args = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Empty = Template.bind({})
|
export const Empty = Template.bind({})
|
||||||
Empty.args = {}
|
Empty.args = {
|
||||||
|
workspaces: [],
|
||||||
|
}
|
||||||
|
@ -14,15 +14,18 @@ import { FC } from "react"
|
|||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
import { AvatarData } from "../../components/AvatarData/AvatarData"
|
||||||
|
import { EmptyState } from "../../components/EmptyState/EmptyState"
|
||||||
import { Margins } from "../../components/Margins/Margins"
|
import { Margins } from "../../components/Margins/Margins"
|
||||||
import { Stack } from "../../components/Stack/Stack"
|
import { Stack } from "../../components/Stack/Stack"
|
||||||
|
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||||
import { getDisplayStatus } from "../../util/workspace"
|
import { getDisplayStatus } from "../../util/workspace"
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
createButton: "Create workspace",
|
createButton: "Create workspace",
|
||||||
emptyView: "so you can check out your repositories, edit your source code, and build and test your software.",
|
emptyMessage: "Create your first workspace",
|
||||||
|
emptyDescription: "Start editing your source code and building your software",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspacesPageViewProps {
|
export interface WorkspacesPageViewProps {
|
||||||
@ -53,21 +56,24 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = (props) => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{!props.loading && !props.workspaces?.length && (
|
{props.loading && <TableLoader />}
|
||||||
|
{props.workspaces && props.workspaces.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={999}>
|
<TableCell colSpan={999}>
|
||||||
<div className={styles.welcome}>
|
<EmptyState
|
||||||
<span>
|
message={Language.emptyMessage}
|
||||||
<Link component={RouterLink} to="/templates">
|
description={Language.emptyDescription}
|
||||||
Create a workspace
|
cta={
|
||||||
|
<Link underline="none" component={RouterLink} to="/workspaces/new">
|
||||||
|
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{Language.emptyView}
|
}
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{props.workspaces?.map((workspace) => {
|
{props.workspaces &&
|
||||||
|
props.workspaces.map((workspace) => {
|
||||||
const status = getDisplayStatus(theme, workspace.latest_build)
|
const status = getDisplayStatus(theme, workspace.latest_build)
|
||||||
return (
|
return (
|
||||||
<TableRow key={workspace.id}>
|
<TableRow key={workspace.id}>
|
||||||
|
@ -56,6 +56,11 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
invoke: {
|
invoke: {
|
||||||
src: "getTemplates",
|
src: "getTemplates",
|
||||||
onDone: [
|
onDone: [
|
||||||
|
{
|
||||||
|
actions: ["assignTemplates"],
|
||||||
|
target: "waitingForTemplateGetCreated",
|
||||||
|
cond: "areTemplatesEmpty",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
actions: ["assignTemplates", "assignPreSelectedTemplate"],
|
actions: ["assignTemplates", "assignPreSelectedTemplate"],
|
||||||
target: "gettingTemplateSchema",
|
target: "gettingTemplateSchema",
|
||||||
@ -71,6 +76,25 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
waitingForTemplateGetCreated: {
|
||||||
|
initial: "refreshingTemplates",
|
||||||
|
states: {
|
||||||
|
refreshingTemplates: {
|
||||||
|
invoke: {
|
||||||
|
src: "getTemplates",
|
||||||
|
onDone: [
|
||||||
|
{ target: "waiting", cond: "areTemplatesEmpty" },
|
||||||
|
{ target: "#createWorkspaceState.selectingTemplate", actions: ["assignTemplates"] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
waiting: {
|
||||||
|
after: {
|
||||||
|
2_000: "refreshingTemplates",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
selectingTemplate: {
|
selectingTemplate: {
|
||||||
on: {
|
on: {
|
||||||
SELECT_TEMPLATE: {
|
SELECT_TEMPLATE: {
|
||||||
@ -147,6 +171,7 @@ export const createWorkspaceMachine = createMachine(
|
|||||||
const template = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
|
const template = event.data.find((template) => template.name === ctx.preSelectedTemplateName)
|
||||||
return !!template
|
return !!template
|
||||||
},
|
},
|
||||||
|
areTemplatesEmpty: (_, event) => event.data.length === 0,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
assignTemplates: assign({
|
assignTemplates: assign({
|
||||||
|
Reference in New Issue
Block a user