Skip to content

Commit c2699a8

Browse files
committed
Create xService
1 parent a72ba40 commit c2699a8

File tree

8 files changed

+277
-6
lines changed

8 files changed

+277
-6
lines changed

site/src/AppRouter.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { OrgsPage } from "./pages/OrgsPage/OrgsPage"
1212
import { SettingsPage } from "./pages/SettingsPage/SettingsPage"
1313
import { AccountPage } from "./pages/SettingsPages/AccountPage/AccountPage"
1414
import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
15+
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
1516
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
1617
import { UsersPage } from "./pages/UsersPage/UsersPage"
1718
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
@@ -73,6 +74,17 @@ export const AppRouter: React.FC = () => (
7374
</Route>
7475
</Route>
7576

77+
<Route path="templates">
78+
<Route
79+
index
80+
element={
81+
<AuthAndFrame>
82+
<TemplatesPage />
83+
</AuthAndFrame>
84+
}
85+
/>
86+
</Route>
87+
7688
<Route path="users">
7789
<Route
7890
index

site/src/api/api.ts

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ export const getTemplate = async (templateId: string): Promise<TypesGen.Template
110110
return response.data
111111
}
112112

113+
export const getTemplates = async (organizationId: string): Promise<TypesGen.Template[]> => {
114+
const response = await axios.get<TypesGen.Template[]>(`/api/v2/organizations/${organizationId}/templates`)
115+
return response.data
116+
}
117+
113118
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
114119
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
115120
return response.data

site/src/components/NavbarView/NavbarView.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export const NavbarView: React.FC<NavbarViewProps> = ({ user, onSignOut, display
3030
Workspaces
3131
</NavLink>
3232
</ListItem>
33+
<ListItem button className={styles.item}>
34+
<NavLink className={styles.link} to="/templates">
35+
Templates
36+
</NavLink>
37+
</ListItem>
3338
</List>
3439
<div className={styles.fullWidth} />
3540
{displayAdminDropdown && <AdminDropdown />}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { useMachine } from "@xstate/react"
12
import React from "react"
3+
import { templatesMachine } from "../../xServices/templates/templatesXService"
24
import { TemplatesPageView } from "./TemplatesPageView"
35

46
const TemplatesPage: React.FC = () => {
5-
return (
6-
<TemplatesPageView />
7-
)
7+
const [templatesState] = useMachine(templatesMachine)
8+
9+
return <TemplatesPageView templates={templatesState.context.templates} loading={templatesState.hasTag("loading")} />
810
}
911

1012
export default TemplatesPage
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,152 @@
1+
import Avatar from "@material-ui/core/Avatar"
2+
import Button from "@material-ui/core/Button"
3+
import Link from "@material-ui/core/Link"
4+
import { makeStyles, Theme } from "@material-ui/core/styles"
5+
import Table from "@material-ui/core/Table"
6+
import TableBody from "@material-ui/core/TableBody"
7+
import TableCell from "@material-ui/core/TableCell"
8+
import TableHead from "@material-ui/core/TableHead"
9+
import TableRow from "@material-ui/core/TableRow"
10+
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
11+
import useTheme from "@material-ui/styles/useTheme"
12+
import dayjs from "dayjs"
13+
import relativeTime from "dayjs/plugin/relativeTime"
114
import React from "react"
15+
import { Link as RouterLink } from "react-router-dom"
16+
import * as TypesGen from "../../api/typesGenerated"
17+
import { Margins } from "../../components/Margins/Margins"
18+
import { Stack } from "../../components/Stack/Stack"
19+
import { firstLetter } from "../../util/firstLetter"
220

3-
export const TemplatesPageView: React.FC = () => {
4-
return <div>testing</div>
21+
dayjs.extend(relativeTime)
22+
23+
export const Language = {
24+
createButton: "Create Template",
25+
emptyView: "so you can check out your repositories, edit your source code, and build and test your software.",
26+
}
27+
28+
export interface TemplatesPageViewProps {
29+
loading?: boolean
30+
templates?: TypesGen.Template[]
31+
error?: unknown
532
}
33+
34+
export const TemplatesPageView: React.FC<TemplatesPageViewProps> = (props) => {
35+
const styles = useStyles()
36+
const theme: Theme = useTheme()
37+
return (
38+
<Stack spacing={4}>
39+
<Margins>
40+
<div className={styles.actions}>
41+
<Button startIcon={<AddCircleOutline />}>{Language.createButton}</Button>
42+
</div>
43+
<Table>
44+
<TableHead>
45+
<TableRow>
46+
<TableCell>Name</TableCell>
47+
<TableCell>Description</TableCell>
48+
<TableCell>Last Updated</TableCell>
49+
<TableCell>Provisioner</TableCell>
50+
<TableCell>Developers</TableCell>
51+
</TableRow>
52+
</TableHead>
53+
<TableBody>
54+
{!props.loading && !props.templates?.length && (
55+
<TableRow>
56+
<TableCell colSpan={999}>
57+
<div className={styles.welcome}>
58+
<span>
59+
<Link component={RouterLink} to="/templates/new">
60+
Create a template
61+
</Link>
62+
&nbsp;{Language.emptyView}
63+
</span>
64+
</div>
65+
</TableCell>
66+
</TableRow>
67+
)}
68+
{props.templates?.map((template) => {
69+
return (
70+
<TableRow key={template.id} className={styles.templateRow}>
71+
<TableCell>
72+
<div className={styles.templateName}>
73+
<Avatar variant="square" className={styles.templateAvatar}>
74+
{firstLetter(template.name)}
75+
</Avatar>
76+
<Link component={RouterLink} to={`/templates/${template.id}`} className={styles.templateLink}>
77+
<b>{template.name}</b>
78+
</Link>
79+
</div>
80+
</TableCell>
81+
<TableCell>{template.description}</TableCell>
82+
<TableCell>{dayjs().to(dayjs(template.updated_at))}</TableCell>
83+
<TableCell>
84+
<img alt="Terraform" src="/terraform-logo.svg" />
85+
</TableCell>
86+
<TableCell>{template.workspace_owner_count}</TableCell>
87+
</TableRow>
88+
)
89+
})}
90+
</TableBody>
91+
</Table>
92+
</Margins>
93+
</Stack>
94+
)
95+
}
96+
97+
const useStyles = makeStyles((theme) => ({
98+
actions: {
99+
marginTop: theme.spacing(3),
100+
marginBottom: theme.spacing(3),
101+
display: "flex",
102+
height: theme.spacing(6),
103+
104+
"& button": {
105+
marginLeft: "auto",
106+
},
107+
},
108+
welcome: {
109+
paddingTop: theme.spacing(12),
110+
paddingBottom: theme.spacing(12),
111+
display: "flex",
112+
flexDirection: "column",
113+
alignItems: "center",
114+
justifyContent: "center",
115+
"& span": {
116+
maxWidth: 600,
117+
textAlign: "center",
118+
fontSize: theme.spacing(2),
119+
lineHeight: `${theme.spacing(3)}px`,
120+
},
121+
},
122+
templateRow: {
123+
"& > td": {
124+
paddingTop: theme.spacing(2),
125+
paddingBottom: theme.spacing(2),
126+
},
127+
},
128+
templateAvatar: {
129+
borderRadius: 2,
130+
marginRight: theme.spacing(1),
131+
width: 24,
132+
height: 24,
133+
fontSize: 16,
134+
},
135+
templateName: {
136+
display: "flex",
137+
alignItems: "center",
138+
},
139+
templateLink: {
140+
display: "flex",
141+
flexDirection: "column",
142+
color: theme.palette.text.primary,
143+
textDecoration: "none",
144+
"&:hover": {
145+
textDecoration: "underline",
146+
},
147+
"& span": {
148+
fontSize: 12,
149+
color: theme.palette.text.secondary,
150+
},
151+
},
152+
}))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { assign, createMachine } from "xstate"
2+
import * as API from "../../api/api"
3+
import * as TypesGen from "../../api/typesGenerated"
4+
5+
interface TemplatesContext {
6+
organizations?: TypesGen.Organization[]
7+
templates?: TypesGen.Template[]
8+
organizationsError?: Error | unknown
9+
templatesError?: Error | unknown
10+
}
11+
12+
export const templatesMachine = createMachine(
13+
{
14+
tsTypes: {} as import("./templatesXService.typegen").Typegen0,
15+
schema: {
16+
context: {} as TemplatesContext,
17+
services: {} as {
18+
getOrganizations: {
19+
data: TypesGen.Organization[]
20+
}
21+
getTemplates: {
22+
data: TypesGen.Template[]
23+
}
24+
},
25+
},
26+
id: "templatesState",
27+
initial: "gettingOrganizations",
28+
states: {
29+
gettingOrganizations: {
30+
entry: "clearOrganizationsError",
31+
invoke: {
32+
src: "getOrganizations",
33+
id: "getOrganizations",
34+
onDone: [
35+
{
36+
actions: ["assignOrganizations", "clearOrganizationsError"],
37+
target: "gettingTemplates",
38+
},
39+
],
40+
onError: [
41+
{
42+
actions: "assignOrganizationsError",
43+
target: "error",
44+
},
45+
],
46+
},
47+
tags: "loading",
48+
},
49+
gettingTemplates: {
50+
entry: "clearTemplatesError",
51+
invoke: {
52+
src: "getTemplates",
53+
id: "getTemplates",
54+
onDone: {
55+
target: "done",
56+
actions: ["assignTemplates", "clearTemplatesError"],
57+
},
58+
onError: {
59+
target: "error",
60+
actions: "assignTemplatesError",
61+
},
62+
},
63+
tags: "loading",
64+
},
65+
done: {},
66+
error: {},
67+
},
68+
},
69+
{
70+
actions: {
71+
assignOrganizations: assign({
72+
organizations: (_, event) => event.data,
73+
}),
74+
assignOrganizationsError: assign({
75+
organizationsError: (_, event) => event.data,
76+
}),
77+
clearOrganizationsError: assign((context) => ({
78+
...context,
79+
organizationsError: undefined,
80+
})),
81+
assignTemplates: assign({
82+
templates: (_, event) => event.data,
83+
}),
84+
assignTemplatesError: assign({
85+
templatesError: (_, event) => event.data,
86+
}),
87+
clearTemplatesError: (context) => assign({ ...context, getWorkspacesError: undefined }),
88+
},
89+
services: {
90+
getOrganizations: API.getOrganizations,
91+
getTemplates: async (context) => {
92+
if (!context.organizations || context.organizations.length === 0) {
93+
throw new Error("no organizations")
94+
}
95+
return API.getTemplates(context.organizations[0].id)
96+
},
97+
},
98+
},
99+
)

site/static/terraform-logo.svg

+1
Loading

site/webpack.dev.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const config: Configuration = {
6161
port: process.env.PORT || 8080,
6262
proxy: {
6363
"/api": {
64-
target: "http://localhost:3000",
64+
target: "https://dev.coder.com",
6565
ws: true,
6666
secure: false,
6767
},

0 commit comments

Comments
 (0)