mirror of https://github.com/coder/coder.git
feat: Add templates page (#1510)
* feat: Add template page * Create xService * Update column names * Show create template conditionally * Add template description * Route to templates * Add empty states * Add tests * Add loading indicator * Requested changes
This commit is contained in:
parent
b7481489b1
commit
76fc59aa79
|
@ -12,6 +12,7 @@ import { OrgsPage } from "./pages/OrgsPage/OrgsPage"
|
|||
import { SettingsPage } from "./pages/SettingsPage/SettingsPage"
|
||||
import { AccountPage } from "./pages/SettingsPages/AccountPage/AccountPage"
|
||||
import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
|
||||
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
|
||||
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
|
||||
import { UsersPage } from "./pages/UsersPage/UsersPage"
|
||||
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
|
||||
|
@ -73,6 +74,17 @@ export const AppRouter: React.FC = () => (
|
|||
</Route>
|
||||
</Route>
|
||||
|
||||
<Route path="templates">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplatesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="users">
|
||||
<Route
|
||||
index
|
||||
|
|
|
@ -110,6 +110,11 @@ export const getTemplate = async (templateId: string): Promise<TypesGen.Template
|
|||
return response.data
|
||||
}
|
||||
|
||||
export const getTemplates = async (organizationId: string): Promise<TypesGen.Template[]> => {
|
||||
const response = await axios.get<TypesGen.Template[]>(`/api/v2/organizations/${organizationId}/templates`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
|
||||
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
|
||||
return response.data
|
||||
|
|
|
@ -30,6 +30,11 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, display
|
|||
Workspaces
|
||||
</NavLink>
|
||||
</ListItem>
|
||||
<ListItem button className={styles.item}>
|
||||
<NavLink className={styles.link} to="/templates">
|
||||
Templates
|
||||
</NavLink>
|
||||
</ListItem>
|
||||
</List>
|
||||
<div className={styles.fullWidth} />
|
||||
{displayAdminDropdown && <AdminDropdown />}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import Avatar from "@material-ui/core/Avatar"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } 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 AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import React from "react"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { firstLetter } from "../../util/firstLetter"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const Language = {
|
||||
createButton: "Create Template",
|
||||
emptyViewCreate: "to standardize development workspaces for your team.",
|
||||
emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.",
|
||||
}
|
||||
|
||||
export interface TemplatesPageViewProps {
|
||||
loading?: boolean
|
||||
canCreateTemplate?: boolean
|
||||
templates?: TypesGen.Template[]
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Margins>
|
||||
<div className={styles.actions}>
|
||||
{props.canCreateTemplate && <Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>}
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Used By</TableCell>
|
||||
<TableCell>Last Updated</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{!props.loading && !props.templates?.length && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={999}>
|
||||
<div className={styles.welcome}>
|
||||
{props.canCreateTemplate ? (
|
||||
<span>
|
||||
<Link component={RouterLink} to="/templates/new">
|
||||
Create a template
|
||||
</Link>
|
||||
{Language.emptyViewCreate}
|
||||
</span>
|
||||
) : (
|
||||
<span>{Language.emptyViewNoPerms}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{props.templates?.map((template) => {
|
||||
return (
|
||||
<TableRow key={template.id} className={styles.templateRow}>
|
||||
<TableCell>
|
||||
<div className={styles.templateName}>
|
||||
<Avatar variant="square" className={styles.templateAvatar}>
|
||||
{firstLetter(template.name)}
|
||||
</Avatar>
|
||||
<Link component={RouterLink} to={`/templates/${template.id}`} className={styles.templateLink}>
|
||||
<b>{template.name}</b>
|
||||
<span>{template.description}</span>
|
||||
</Link>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{template.workspace_owner_count} developer{template.workspace_owner_count !== 1 && "s"}
|
||||
</TableCell>
|
||||
<TableCell>{dayjs().to(dayjs(template.updated_at))}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Margins>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actions: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
display: "flex",
|
||||
height: theme.spacing(6),
|
||||
|
||||
"& button": {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
},
|
||||
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`,
|
||||
},
|
||||
},
|
||||
templateRow: {
|
||||
"& > td": {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
templateAvatar: {
|
||||
borderRadius: 2,
|
||||
marginRight: theme.spacing(1),
|
||||
width: 24,
|
||||
height: 24,
|
||||
fontSize: 16,
|
||||
},
|
||||
templateName: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
templateLink: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "none",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
"& span": {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}))
|
|
@ -0,0 +1,67 @@
|
|||
import { screen } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import React from "react"
|
||||
import { MockTemplate } from "../../testHelpers/entities"
|
||||
import { history, render } from "../../testHelpers/renderHelpers"
|
||||
import { server } from "../../testHelpers/server"
|
||||
import TemplatesPage from "./TemplatesPage"
|
||||
import { Language } from "./TemplatesPageView"
|
||||
|
||||
describe("TemplatesPage", () => {
|
||||
beforeEach(() => {
|
||||
history.replace("/workspaces")
|
||||
})
|
||||
|
||||
it("renders an empty templates page", async () => {
|
||||
// Given
|
||||
server.use(
|
||||
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([]))
|
||||
}),
|
||||
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
createTemplates: true,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
// When
|
||||
render(<TemplatesPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(Language.emptyViewCreate)
|
||||
})
|
||||
|
||||
it("renders a filled templates page", async () => {
|
||||
// When
|
||||
render(<TemplatesPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(MockTemplate.name)
|
||||
})
|
||||
|
||||
it("shows empty view without permissions to create", async () => {
|
||||
server.use(
|
||||
rest.get("/api/v2/organizations/:organizationId/templates", (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([]))
|
||||
}),
|
||||
rest.post("/api/v2/users/:userId/authorization", async (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
createTemplates: false,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
// When
|
||||
render(<TemplatesPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(Language.emptyViewNoPerms)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
import { useActor, useMachine } from "@xstate/react"
|
||||
import React, { useContext } from "react"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { templatesMachine } from "../../xServices/templates/templatesXService"
|
||||
import { TemplatesPageView } from "./TemplatesPageView"
|
||||
|
||||
const TemplatesPage: React.FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [authState] = useActor(xServices.authXService)
|
||||
const [templatesState] = useMachine(templatesMachine)
|
||||
|
||||
return (
|
||||
<TemplatesPageView
|
||||
templates={templatesState.context.templates}
|
||||
canCreateTemplate={authState.context.permissions?.createTemplates}
|
||||
loading={templatesState.hasTag("loading")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplatesPage
|
|
@ -0,0 +1,36 @@
|
|||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import React from "react"
|
||||
import { MockTemplate } from "../../testHelpers/entities"
|
||||
import { TemplatesPageView, TemplatesPageViewProps } from "./TemplatesPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/TemplatesPageView",
|
||||
component: TemplatesPageView,
|
||||
} as ComponentMeta<typeof TemplatesPageView>
|
||||
|
||||
const Template: Story<TemplatesPageViewProps> = (args) => <TemplatesPageView {...args} />
|
||||
|
||||
export const AllStates = Template.bind({})
|
||||
AllStates.args = {
|
||||
canCreateTemplate: true,
|
||||
templates: [
|
||||
MockTemplate,
|
||||
{
|
||||
...MockTemplate,
|
||||
description: "🚀 Some magical template that does some magical things!",
|
||||
},
|
||||
{
|
||||
...MockTemplate,
|
||||
workspace_owner_count: 150,
|
||||
description: "😮 Wow, this one has a bunch of usage!",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const EmptyCanCreate = Template.bind({})
|
||||
EmptyCanCreate.args = {
|
||||
canCreateTemplate: true,
|
||||
}
|
||||
|
||||
export const EmptyCannotCreate = Template.bind({})
|
||||
EmptyCannotCreate.args = {}
|
|
@ -0,0 +1,156 @@
|
|||
import Avatar from "@material-ui/core/Avatar"
|
||||
import Box from "@material-ui/core/Box"
|
||||
import Button from "@material-ui/core/Button"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } 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 AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||
import dayjs from "dayjs"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import React from "react"
|
||||
import { Link as RouterLink } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { TableLoader } from "../../components/TableLoader/TableLoader"
|
||||
import { firstLetter } from "../../util/firstLetter"
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export const Language = {
|
||||
createButton: "Create Template",
|
||||
developerCount: (ownerCount: number): string => {
|
||||
return `${ownerCount} developer${ownerCount !== 1 ? "s" : ""}`
|
||||
},
|
||||
nameLabel: "Name",
|
||||
usedByLabel: "Used By",
|
||||
lastUpdatedLabel: "Last Updated",
|
||||
emptyViewCreateCTA: "Create a template",
|
||||
emptyViewCreate: "to standardize development workspaces for your team.",
|
||||
emptyViewNoPerms: "No templates have been created! Contact your Coder administrator.",
|
||||
}
|
||||
|
||||
export interface TemplatesPageViewProps {
|
||||
loading?: boolean
|
||||
canCreateTemplate?: boolean
|
||||
templates?: TypesGen.Template[]
|
||||
}
|
||||
|
||||
export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Margins>
|
||||
<div className={styles.actions}>
|
||||
{props.canCreateTemplate && <Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>}
|
||||
</div>
|
||||
<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 colSpan={999}>
|
||||
<div className={styles.welcome}>
|
||||
{props.canCreateTemplate ? (
|
||||
<span>
|
||||
<Link component={RouterLink} to="/templates/new">
|
||||
{Language.emptyViewCreateCTA}
|
||||
</Link>
|
||||
{Language.emptyViewCreate}
|
||||
</span>
|
||||
) : (
|
||||
<span>{Language.emptyViewNoPerms}</span>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{props.templates?.map((template) => (
|
||||
<TableRow key={template.id} className={styles.templateRow}>
|
||||
<TableCell>
|
||||
<Box alignItems="center" display="flex">
|
||||
<Avatar variant="square" className={styles.templateAvatar}>
|
||||
{firstLetter(template.name)}
|
||||
</Avatar>
|
||||
<Link component={RouterLink} to={`/templates/${template.id}`} className={styles.templateLink}>
|
||||
<b>{template.name}</b>
|
||||
<span>{template.description}</span>
|
||||
</Link>
|
||||
</Box>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{Language.developerCount(template.workspace_owner_count)}</TableCell>
|
||||
|
||||
<TableCell>{dayjs().to(dayjs(template.updated_at))}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Margins>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actions: {
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(3),
|
||||
display: "flex",
|
||||
height: theme.spacing(6),
|
||||
|
||||
"& button": {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
},
|
||||
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`,
|
||||
},
|
||||
},
|
||||
templateRow: {
|
||||
"& > td": {
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
templateAvatar: {
|
||||
borderRadius: 2,
|
||||
marginRight: theme.spacing(1),
|
||||
width: 24,
|
||||
height: 24,
|
||||
fontSize: 16,
|
||||
},
|
||||
templateLink: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: "none",
|
||||
"&:hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
"& span": {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
},
|
||||
}))
|
|
@ -39,7 +39,9 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
|
|||
<Stack spacing={4}>
|
||||
<Margins>
|
||||
<div className={styles.actions}>
|
||||
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||
<Link component={RouterLink} to="/templates">
|
||||
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
@ -57,7 +59,7 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
|
|||
<TableCell colSpan={999}>
|
||||
<div className={styles.welcome}>
|
||||
<span>
|
||||
<Link component={RouterLink} to="/workspaces/new">
|
||||
<Link component={RouterLink} to="/templates">
|
||||
Create a workspace
|
||||
</Link>
|
||||
{Language.emptyView}
|
||||
|
@ -114,7 +116,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
display: "flex",
|
||||
height: theme.spacing(6),
|
||||
|
||||
"& button": {
|
||||
"& > *": {
|
||||
marginLeft: "auto",
|
||||
},
|
||||
},
|
||||
|
|
|
@ -80,8 +80,8 @@ export const MockRunningProvisionerJob = { ...MockProvisionerJob, status: "runni
|
|||
|
||||
export const MockTemplate: TypesGen.Template = {
|
||||
id: "test-template",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
created_at: new Date().toString(),
|
||||
updated_at: new Date().toString(),
|
||||
organization_id: MockOrganization.id,
|
||||
name: "Test Template",
|
||||
provisioner: MockProvisioner.id,
|
||||
|
|
|
@ -17,6 +17,9 @@ export const handlers = [
|
|||
rest.get("/api/v2/organizations/:organizationId/templates/:templateId", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockTemplate))
|
||||
}),
|
||||
rest.get("/api/v2/organizations/:organizationId/templates", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json([M.MockTemplate]))
|
||||
}),
|
||||
|
||||
// templates
|
||||
rest.get("/api/v2/templates/:templateId", async (req, res, ctx) => {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const Language = {
|
|||
|
||||
export const checks = {
|
||||
readAllUsers: "readAllUsers",
|
||||
createTemplates: "createTemplates",
|
||||
} as const
|
||||
|
||||
export const permissionsToCheck = {
|
||||
|
@ -20,6 +21,12 @@ export const permissionsToCheck = {
|
|||
},
|
||||
action: "read",
|
||||
},
|
||||
[checks.createTemplates]: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
},
|
||||
action: "write",
|
||||
},
|
||||
} as const
|
||||
|
||||
type Permissions = Record<keyof typeof permissionsToCheck, boolean>
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import { assign, createMachine } from "xstate"
|
||||
import * as API from "../../api/api"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
|
||||
interface TemplatesContext {
|
||||
organizations?: TypesGen.Organization[]
|
||||
templates?: TypesGen.Template[]
|
||||
canCreateTemplate?: boolean
|
||||
permissionsError?: Error | unknown
|
||||
organizationsError?: Error | unknown
|
||||
templatesError?: Error | unknown
|
||||
}
|
||||
|
||||
export const templatesMachine = createMachine(
|
||||
{
|
||||
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
|
||||
schema: {
|
||||
context: {} as TemplatesContext,
|
||||
services: {} as {
|
||||
getOrganizations: {
|
||||
data: TypesGen.Organization[]
|
||||
}
|
||||
getPermissions: {
|
||||
data: boolean
|
||||
}
|
||||
getTemplates: {
|
||||
data: TypesGen.Template[]
|
||||
}
|
||||
},
|
||||
},
|
||||
id: "templatesState",
|
||||
initial: "gettingOrganizations",
|
||||
states: {
|
||||
gettingOrganizations: {
|
||||
entry: "clearOrganizationsError",
|
||||
invoke: {
|
||||
src: "getOrganizations",
|
||||
id: "getOrganizations",
|
||||
onDone: [
|
||||
{
|
||||
actions: ["assignOrganizations", "clearOrganizationsError"],
|
||||
target: "gettingTemplates",
|
||||
},
|
||||
],
|
||||
onError: [
|
||||
{
|
||||
actions: "assignOrganizationsError",
|
||||
target: "error",
|
||||
},
|
||||
],
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
gettingTemplates: {
|
||||
entry: "clearTemplatesError",
|
||||
invoke: {
|
||||
src: "getTemplates",
|
||||
id: "getTemplates",
|
||||
onDone: {
|
||||
target: "done",
|
||||
actions: ["assignTemplates", "clearTemplatesError"],
|
||||
},
|
||||
onError: {
|
||||
target: "error",
|
||||
actions: "assignTemplatesError",
|
||||
},
|
||||
},
|
||||
tags: "loading",
|
||||
},
|
||||
done: {},
|
||||
error: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
assignOrganizations: assign({
|
||||
organizations: (_, event) => event.data,
|
||||
}),
|
||||
assignOrganizationsError: assign({
|
||||
organizationsError: (_, event) => event.data,
|
||||
}),
|
||||
clearOrganizationsError: assign((context) => ({
|
||||
...context,
|
||||
organizationsError: undefined,
|
||||
})),
|
||||
assignTemplates: assign({
|
||||
templates: (_, event) => event.data,
|
||||
}),
|
||||
assignTemplatesError: assign({
|
||||
templatesError: (_, event) => event.data,
|
||||
}),
|
||||
clearTemplatesError: (context) => assign({ ...context, getWorkspacesError: undefined }),
|
||||
},
|
||||
services: {
|
||||
getOrganizations: API.getOrganizations,
|
||||
getTemplates: async (context) => {
|
||||
if (!context.organizations || context.organizations.length === 0) {
|
||||
throw new Error("no organizations")
|
||||
}
|
||||
return API.getTemplates(context.organizations[0].id)
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
Loading…
Reference in New Issue