Skip to content

Commit 0d55592

Browse files
committed
yes
1 parent e3da0a4 commit 0d55592

File tree

6 files changed

+260
-39
lines changed

6 files changed

+260
-39
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { type FC } from "react";
2+
import { useMutation, useQueryClient } from "react-query";
3+
import { useNavigate } from "react-router-dom";
4+
import {
5+
createOrganization,
6+
updateOrganization,
7+
deleteOrganization,
8+
} from "api/queries/organizations";
9+
import { ErrorAlert } from "components/Alert/ErrorAlert";
10+
import { displaySuccess } from "components/GlobalSnackbar/utils";
11+
import { Stack } from "components/Stack/Stack";
12+
import { useOrganizationSettings } from "./ManagementSettingsLayout";
13+
import { CreateOrganizationPageView } from "./CreateOrganizationPageView";
14+
15+
const CreateOrganizationPage: FC = () => {
16+
const navigate = useNavigate();
17+
18+
const queryClient = useQueryClient();
19+
const createOrganizationMutation = useMutation(
20+
createOrganization(queryClient),
21+
);
22+
23+
const error = createOrganizationMutation.error;
24+
25+
return (
26+
<Stack>
27+
{Boolean(error) && <ErrorAlert error={error} />}
28+
29+
<CreateOrganizationPageView
30+
error={error}
31+
onSubmit={async (values) => {
32+
await createOrganizationMutation.mutateAsync(values);
33+
displaySuccess("Organization settings updated.");
34+
navigate(`/organizations/${values.name}`);
35+
}}
36+
/>
37+
</Stack>
38+
);
39+
};
40+
41+
export default CreateOrganizationPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import Button from "@mui/material/Button";
3+
import TextField from "@mui/material/TextField";
4+
import { useFormik } from "formik";
5+
import { type FC, useState } from "react";
6+
import { useMutation, useQueryClient } from "react-query";
7+
import * as Yup from "yup";
8+
import {
9+
createOrganization,
10+
updateOrganization,
11+
deleteOrganization,
12+
} from "api/queries/organizations";
13+
import type {
14+
CreateOrganizationRequest,
15+
Organization,
16+
} from "api/typesGenerated";
17+
import { ErrorAlert } from "components/Alert/ErrorAlert";
18+
import {
19+
FormFields,
20+
FormSection,
21+
HorizontalForm,
22+
FormFooter,
23+
} from "components/Form/Form";
24+
import { displaySuccess } from "components/GlobalSnackbar/utils";
25+
import { IconField } from "components/IconField/IconField";
26+
import { Margins } from "components/Margins/Margins";
27+
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
28+
import { Stack } from "components/Stack/Stack";
29+
import {
30+
getFormHelpers,
31+
nameValidator,
32+
displayNameValidator,
33+
onChangeTrimmed,
34+
} from "utils/formUtils";
35+
import { useOrganizationSettings } from "./ManagementSettingsLayout";
36+
37+
const MAX_DESCRIPTION_CHAR_LIMIT = 128;
38+
const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`;
39+
40+
const validationSchema = Yup.object({
41+
name: nameValidator("Name"),
42+
display_name: displayNameValidator("Display name"),
43+
description: Yup.string().max(
44+
MAX_DESCRIPTION_CHAR_LIMIT,
45+
MAX_DESCRIPTION_MESSAGE,
46+
),
47+
});
48+
49+
interface CreateOrganizationPageViewProps {
50+
error: unknown;
51+
onSubmit: (values: CreateOrganizationRequest) => Promise<void>;
52+
}
53+
54+
export const CreateOrganizationPageView: FC<
55+
CreateOrganizationPageViewProps
56+
> = ({ error, onSubmit }) => {
57+
const form = useFormik<CreateOrganizationRequest>({
58+
initialValues: {
59+
name: "",
60+
display_name: "",
61+
description: "",
62+
icon: "",
63+
},
64+
validationSchema,
65+
onSubmit,
66+
});
67+
const getFieldHelpers = getFormHelpers(form, error);
68+
69+
return (
70+
<div>
71+
<PageHeader>
72+
<PageHeaderTitle>Organization settings</PageHeaderTitle>
73+
</PageHeader>
74+
75+
<HorizontalForm
76+
onSubmit={form.handleSubmit}
77+
aria-label="Organization settings form"
78+
>
79+
<FormSection
80+
title="General info"
81+
description="Change the name or description of the organization."
82+
>
83+
<fieldset
84+
disabled={form.isSubmitting}
85+
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
86+
>
87+
<FormFields>
88+
<TextField
89+
{...getFieldHelpers("name")}
90+
onChange={onChangeTrimmed(form)}
91+
autoFocus
92+
fullWidth
93+
label="Name"
94+
/>
95+
<TextField
96+
{...getFieldHelpers("display_name")}
97+
fullWidth
98+
label="Display name"
99+
/>
100+
<TextField
101+
{...getFieldHelpers("description")}
102+
multiline
103+
fullWidth
104+
label="Description"
105+
rows={2}
106+
/>
107+
<IconField
108+
{...getFieldHelpers("icon")}
109+
onChange={onChangeTrimmed(form)}
110+
fullWidth
111+
onPickEmoji={(value) => form.setFieldValue("icon", value)}
112+
/>
113+
</FormFields>
114+
</fieldset>
115+
</FormSection>
116+
<FormFooter isLoading={form.isSubmitting} />
117+
</HorizontalForm>
118+
</div>
119+
);
120+
};
121+
122+
const styles = {
123+
dangerButton: (theme) => ({
124+
"&.MuiButton-contained": {
125+
backgroundColor: theme.roles.danger.fill.solid,
126+
borderColor: theme.roles.danger.fill.outline,
127+
128+
"&:not(.MuiLoadingButton-loading)": {
129+
color: theme.roles.danger.fill.text,
130+
},
131+
132+
"&:hover:not(:disabled)": {
133+
backgroundColor: theme.roles.danger.hover.fill.solid,
134+
borderColor: theme.roles.danger.hover.fill.outline,
135+
},
136+
137+
"&.Mui-disabled": {
138+
backgroundColor: theme.roles.danger.disabled.background,
139+
borderColor: theme.roles.danger.disabled.outline,
140+
141+
"&:not(.MuiLoadingButton-loading)": {
142+
color: theme.roles.danger.disabled.fill.text,
143+
},
144+
},
145+
},
146+
}),
147+
} satisfies Record<string, Interpolation<Theme>>;

site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createContext, type FC, Suspense, useContext } from "react";
22
import { useQuery } from "react-query";
3-
import { Outlet, useParams } from "react-router-dom";
3+
import { Outlet, useLocation, useParams } from "react-router-dom";
44
import { myOrganizations } from "api/queries/users";
55
import type { Organization } from "api/typesGenerated";
66
import { Loader } from "components/Loader/Loader";
@@ -34,6 +34,7 @@ export const useOrganizationSettings = (): OrganizationSettingsContextValue => {
3434
};
3535

3636
export const ManagementSettingsLayout: FC = () => {
37+
const location = useLocation();
3738
const { permissions, organizationIds } = useAuthenticated();
3839
const { experiments } = useDashboard();
3940
const { organization } = useParams() as { organization: string };
@@ -42,6 +43,12 @@ export const ManagementSettingsLayout: FC = () => {
4243

4344
const multiOrgExperimentEnabled = experiments.includes("multi-organization");
4445

46+
console.log("oh jeez", organization);
47+
48+
const inOrganizationSettings =
49+
location.pathname.startsWith("/organizations") &&
50+
location.pathname !== "/organizations/new";
51+
4552
if (!multiOrgExperimentEnabled) {
4653
return <NotFoundPage />;
4754
}
@@ -53,11 +60,13 @@ export const ManagementSettingsLayout: FC = () => {
5360
{organizationsQuery.data ? (
5461
<OrganizationSettingsContext.Provider
5562
value={{
56-
currentOrganizationId: !organization
57-
? organizationIds[0]
58-
: organizationsQuery.data.find(
59-
(org) => org.name === organization,
60-
)?.id,
63+
currentOrganizationId: !inOrganizationSettings
64+
? undefined
65+
: !organization
66+
? organizationIds[0]
67+
: organizationsQuery.data.find(
68+
(org) => org.name === organization,
69+
)?.id,
6170
organizations: organizationsQuery.data,
6271
}}
6372
>

site/src/pages/ManagementSettingsPage/OrganizationSettingsPageView.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const OrganizationSettingsPageView: FC<
8181

8282
<HorizontalForm
8383
onSubmit={form.handleSubmit}
84-
aria-label="Template settings form"
84+
aria-label="Organization settings form"
8585
>
8686
<FormSection
8787
title="General info"

site/src/pages/ManagementSettingsPage/Sidebar.tsx

+25-8
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ export const Sidebar: FC = () => {
2929
>
3030
Organizations
3131
</header>
32-
<SidebarNavItem active href="new" icon={<AddIcon />}>
32+
<SidebarNavItem
33+
active="auto"
34+
href="/organizations/new"
35+
icon={<AddIcon />}
36+
>
3337
New organization
3438
</SidebarNavItem>
3539
{organizations.map((organization) => (
@@ -165,15 +169,28 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
165169
const link = useClassName(classNames.link, []);
166170
const activeLink = useClassName(classNames.activeLink, []);
167171

168-
const LinkC = active === "auto" ? NavLink : Link;
172+
const content = (
173+
<Stack alignItems="center" spacing={1.5} direction="row">
174+
{icon}
175+
{children}
176+
</Stack>
177+
);
178+
179+
if (active === "auto") {
180+
return (
181+
<NavLink
182+
to={href}
183+
className={({ isActive }) => cx([link, isActive && activeLink])}
184+
>
185+
{content}
186+
</NavLink>
187+
);
188+
}
169189

170190
return (
171-
<LinkC to={href} className={cx([link, active && activeLink])}>
172-
<Stack alignItems="center" spacing={1.5} direction="row">
173-
{icon}
174-
{children}
175-
</Stack>
176-
</LinkC>
191+
<Link to={href} className={cx([link, active && activeLink])}>
192+
{content}
193+
</Link>
177194
);
178195
};
179196

site/src/router.tsx

+31-24
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ const AddNewLicensePage = lazy(
221221
() =>
222222
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
223223
);
224+
const CreateOrganizationPage = lazy(
225+
() => import("./pages/ManagementSettingsPage/CreateOrganizationPage"),
226+
);
224227
const OrganizationSettingsPage = lazy(
225228
() => import("./pages/ManagementSettingsPage/OrganizationSettingsPage"),
226229
);
@@ -333,31 +336,35 @@ export const router = createBrowserRouter(
333336

334337
<Route path="/audit" element={<AuditPage />} />
335338

336-
<Route
337-
path="/organizations/:organization?"
338-
element={<ManagementSettingsLayout />}
339-
>
339+
<Route path="/organizations" element={<ManagementSettingsLayout />}>
340+
<Route path="new" element={<CreateOrganizationPage />} />
341+
342+
{/* General settings for the default org can omit the organization name */}
340343
<Route index element={<OrganizationSettingsPage />} />
341-
<Route
342-
path="external-auth"
343-
element={<OrganizationSettingsPlaceholder />}
344-
/>
345-
<Route
346-
path="members"
347-
element={<OrganizationSettingsPlaceholder />}
348-
/>
349-
<Route
350-
path="groups"
351-
element={<OrganizationSettingsPlaceholder />}
352-
/>
353-
<Route
354-
path="metrics"
355-
element={<OrganizationSettingsPlaceholder />}
356-
/>
357-
<Route
358-
path="auditing"
359-
element={<OrganizationSettingsPlaceholder />}
360-
/>
344+
345+
<Route path=":organization">
346+
<Route index element={<OrganizationSettingsPage />} />
347+
<Route
348+
path="external-auth"
349+
element={<OrganizationSettingsPlaceholder />}
350+
/>
351+
<Route
352+
path="members"
353+
element={<OrganizationSettingsPlaceholder />}
354+
/>
355+
<Route
356+
path="groups"
357+
element={<OrganizationSettingsPlaceholder />}
358+
/>
359+
<Route
360+
path="metrics"
361+
element={<OrganizationSettingsPlaceholder />}
362+
/>
363+
<Route
364+
path="auditing"
365+
element={<OrganizationSettingsPlaceholder />}
366+
/>
367+
</Route>
361368
</Route>
362369

363370
<Route path="/deployment" element={<DeploySettingsLayout />}>

0 commit comments

Comments
 (0)