Skip to content

Commit 9aa686b

Browse files
committed
Add group settings
1 parent 7770498 commit 9aa686b

File tree

6 files changed

+242
-7
lines changed

6 files changed

+242
-7
lines changed

site/src/AppRouter.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@ import { RequirePermission } from "components/RequirePermission/RequirePermissio
55
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
66
import IndexPage from "pages"
77
import AuditPage from "pages/AuditPage/AuditPage"
8-
import CreateGroupPage from "pages/GroupsPage/CreateGroupPage"
9-
import GroupPage from "pages/GroupsPage/GroupPage"
108
import GroupsPage from "pages/GroupsPage/GroupsPage"
119
import LoginPage from "pages/LoginPage/LoginPage"
1210
import { SetupPage } from "pages/SetupPage/SetupPage"
13-
import TemplatePermissionsPage from "pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage"
11+
1412
import TemplateSummaryPage from "pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"
1513
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
1614
import TemplatesPage from "pages/TemplatesPage/TemplatesPage"
@@ -44,7 +42,13 @@ const WorkspaceAppErrorPage = lazy(
4442
() => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"),
4543
)
4644
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
45+
const TemplatePermissionsPage = lazy(
46+
() => import("./pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage"),
47+
)
4748
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
49+
const CreateGroupPage = lazy(() => import("./pages/GroupsPage/CreateGroupPage"))
50+
const GroupPage = lazy(() => import("./pages/GroupsPage/GroupPage"))
51+
const SettingsGroupPage = lazy(() => import("./pages/GroupsPage/SettingsGroupPage"))
4852

4953
export const AppRouter: FC = () => {
5054
const xServices = useContext(XServiceContext)
@@ -177,6 +181,14 @@ export const AppRouter: FC = () => {
177181
</AuthAndFrame>
178182
}
179183
/>
184+
<Route
185+
path=":groupId/settings"
186+
element={
187+
<RequireAuth>
188+
<SettingsGroupPage />
189+
</RequireAuth>
190+
}
191+
/>
180192
</Route>
181193

182194
<Route path="/audit">

site/src/pages/GroupsPage/GroupPage.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Button from "@material-ui/core/Button"
2+
import Link from "@material-ui/core/Link"
23
import Table from "@material-ui/core/Table"
34
import TableBody from "@material-ui/core/TableBody"
45
import TableCell from "@material-ui/core/TableCell"
@@ -23,7 +24,7 @@ import { TableRowMenu } from "components/TableRowMenu/TableRowMenu"
2324
import { UserAutocompleteInline } from "components/UserAutocomplete/UserAutocomplete"
2425
import { useState } from "react"
2526
import { Helmet } from "react-helmet-async"
26-
import { useNavigate, useParams } from "react-router-dom"
27+
import { Link as RouterLink, useNavigate, useParams } from "react-router-dom"
2728
import { pageTitle } from "util/page"
2829
import { groupMachine } from "xServices/groups/groupXService"
2930

@@ -104,7 +105,9 @@ export const GroupPage: React.FC = () => {
104105
<PageHeader
105106
actions={
106107
<>
107-
<Button startIcon={<SettingsOutlined />}>Settings</Button>
108+
<Link to="settings" underline="none" component={RouterLink}>
109+
<Button startIcon={<SettingsOutlined />}>Settings</Button>
110+
</Link>
108111
<Button
109112
onClick={() => {
110113
send("DELETE")
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import TextField from "@material-ui/core/TextField"
2+
import { useMachine } from "@xstate/react"
3+
import { Group } from "api/typesGenerated"
4+
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
5+
import { FormFooter } from "components/FormFooter/FormFooter"
6+
import { FullPageForm } from "components/FullPageForm/FullPageForm"
7+
import { Loader } from "components/Loader/Loader"
8+
import { Margins } from "components/Margins/Margins"
9+
import { useFormik } from "formik"
10+
import React from "react"
11+
import { Helmet } from "react-helmet-async"
12+
import { useNavigate, useParams } from "react-router-dom"
13+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
14+
import { pageTitle } from "util/page"
15+
import { editGroupMachine } from "xServices/groups/editGroupXService"
16+
import * as Yup from "yup"
17+
18+
type FormData = {
19+
name: string
20+
}
21+
22+
const validationSchema = Yup.object({
23+
name: nameValidator("Name"),
24+
})
25+
26+
const UpdateGroupForm: React.FC<{
27+
group: Group
28+
errors: unknown
29+
onSubmit: (data: FormData) => void
30+
onCancel: () => void
31+
isLoading: boolean
32+
}> = ({ group, errors, onSubmit, onCancel, isLoading }) => {
33+
const form = useFormik<FormData>({
34+
initialValues: {
35+
name: group.name,
36+
},
37+
validationSchema,
38+
onSubmit,
39+
})
40+
const getFieldHelpers = getFormHelpers<FormData>(form, errors)
41+
42+
return (
43+
<FullPageForm title="Group settings" onCancel={onCancel}>
44+
<form onSubmit={form.handleSubmit}>
45+
<TextField
46+
{...getFieldHelpers("name")}
47+
onChange={onChangeTrimmed(form)}
48+
autoComplete="name"
49+
autoFocus
50+
fullWidth
51+
label="Name"
52+
variant="outlined"
53+
/>
54+
<FormFooter onCancel={onCancel} isLoading={isLoading} />
55+
</form>
56+
</FullPageForm>
57+
)
58+
}
59+
60+
export const SettingsGroupPage: React.FC = () => {
61+
const { groupId } = useParams()
62+
if (!groupId) {
63+
throw new Error("Group ID not defined.")
64+
}
65+
const navigate = useNavigate()
66+
const [editState, sendEditEvent] = useMachine(editGroupMachine, {
67+
context: {
68+
groupId,
69+
},
70+
actions: {
71+
onUpdate: () => {
72+
navigate(`/groups/${groupId}`)
73+
},
74+
},
75+
})
76+
const { updateGroupFormErrors, group } = editState.context
77+
78+
const onCancel = () => {
79+
navigate("/groups")
80+
}
81+
82+
return (
83+
<>
84+
<Helmet>
85+
<title>{pageTitle("Settings Group")}</title>
86+
</Helmet>
87+
88+
<ChooseOne>
89+
<Cond condition={editState.matches("loading")}>
90+
<Loader />
91+
</Cond>
92+
93+
<Cond condition>
94+
<Margins>
95+
<UpdateGroupForm
96+
group={group as Group}
97+
onCancel={onCancel}
98+
errors={updateGroupFormErrors}
99+
isLoading={editState.matches("updating")}
100+
onSubmit={(data) => {
101+
sendEditEvent({ type: "UPDATE", data })
102+
}}
103+
/>
104+
</Margins>
105+
</Cond>
106+
</ChooseOne>
107+
</>
108+
)
109+
}
110+
export default SettingsGroupPage

site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { templateUsersMachine } from "xServices/template/templateUsersXService"
99
import { TemplateContext } from "xServices/template/templateXService"
1010
import { TemplatePermissionsPageView } from "./TemplatePermissionsPageView"
1111

12-
export const TemplateCollaboratorsPage: FC<React.PropsWithChildren<unknown>> = () => {
12+
export const TemplatePermissionsPage: FC<React.PropsWithChildren<unknown>> = () => {
1313
const { templateContext, permissions } = useOutletContext<{
1414
templateContext: TemplateContext
1515
permissions: Permissions
@@ -54,4 +54,4 @@ export const TemplateCollaboratorsPage: FC<React.PropsWithChildren<unknown>> = (
5454
)
5555
}
5656

57-
export default TemplateCollaboratorsPage
57+
export default TemplatePermissionsPage
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { getGroup, patchGroup } from "api/api"
2+
import {
3+
ApiError,
4+
getErrorMessage,
5+
hasApiFieldErrors,
6+
isApiError,
7+
mapApiErrorToFieldErrors,
8+
} from "api/errors"
9+
import { Group } from "api/typesGenerated"
10+
import { displayError } from "components/GlobalSnackbar/utils"
11+
import { assign, createMachine } from "xstate"
12+
13+
export const editGroupMachine = createMachine(
14+
{
15+
id: "editGroup",
16+
schema: {
17+
context: {} as {
18+
groupId: string
19+
group?: Group
20+
updateGroupFormErrors?: unknown
21+
},
22+
services: {} as {
23+
loadGroup: {
24+
data: Group
25+
}
26+
updateGroup: {
27+
data: Group
28+
}
29+
},
30+
events: {} as {
31+
type: "UPDATE"
32+
data: { name: string }
33+
},
34+
},
35+
tsTypes: {} as import("./editGroupXService.typegen").Typegen0,
36+
initial: "loading",
37+
states: {
38+
loading: {
39+
invoke: {
40+
src: "loadGroup",
41+
onDone: {
42+
actions: ["assignGroup"],
43+
target: "idle",
44+
},
45+
onError: {
46+
actions: ["displayLoadGroupError"],
47+
target: "idle",
48+
},
49+
},
50+
},
51+
idle: {
52+
on: {
53+
UPDATE: {
54+
target: "updating",
55+
},
56+
},
57+
},
58+
updating: {
59+
invoke: {
60+
src: "updateGroup",
61+
onDone: {
62+
actions: ["onUpdate"],
63+
},
64+
onError: [
65+
{
66+
target: "idle",
67+
cond: "hasFieldErrors",
68+
actions: ["assignUpdateGroupFormErrors"],
69+
},
70+
{
71+
target: "idle",
72+
actions: ["displayUpdateGroupError"],
73+
},
74+
],
75+
},
76+
},
77+
},
78+
},
79+
{
80+
guards: {
81+
hasFieldErrors: (_, event) => isApiError(event.data) && hasApiFieldErrors(event.data),
82+
},
83+
services: {
84+
loadGroup: ({ groupId }) => getGroup(groupId),
85+
86+
updateGroup: ({ group }, { data }) => {
87+
if (!group) {
88+
throw new Error("Group not defined.")
89+
}
90+
91+
return patchGroup(group.id, { ...data, add_users: [], remove_users: [] })
92+
},
93+
},
94+
actions: {
95+
assignGroup: assign({
96+
group: (_, { data }) => data,
97+
}),
98+
displayLoadGroupError: (_, { data }) => {
99+
const message = getErrorMessage(data, "Failed to the group.")
100+
displayError(message)
101+
},
102+
displayUpdateGroupError: (_, { data }) => {
103+
const message = getErrorMessage(data, "Failed to update the group.")
104+
displayError(message)
105+
},
106+
assignUpdateGroupFormErrors: (_, event) =>
107+
mapApiErrorToFieldErrors((event.data as ApiError).response.data),
108+
},
109+
},
110+
)

0 commit comments

Comments
 (0)