Skip to content

feat: unify organization and deployment management settings #13602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jul 1, 2024
Prev Previous commit
Next Next commit
yes
  • Loading branch information
aslilac committed Jun 18, 2024
commit 0d555927a17693ca90ea3066a277fff23fa5150a
41 changes: 41 additions & 0 deletions site/src/pages/ManagementSettingsPage/CreateOrganizationPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type FC } from "react";
import { useMutation, useQueryClient } from "react-query";
import { useNavigate } from "react-router-dom";
import {
createOrganization,
updateOrganization,
deleteOrganization,
} from "api/queries/organizations";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
import { useOrganizationSettings } from "./ManagementSettingsLayout";
import { CreateOrganizationPageView } from "./CreateOrganizationPageView";

const CreateOrganizationPage: FC = () => {
const navigate = useNavigate();

const queryClient = useQueryClient();
const createOrganizationMutation = useMutation(
createOrganization(queryClient),
);

const error = createOrganizationMutation.error;

return (
<Stack>
{Boolean(error) && <ErrorAlert error={error} />}

<CreateOrganizationPageView
error={error}
onSubmit={async (values) => {
await createOrganizationMutation.mutateAsync(values);
displaySuccess("Organization settings updated.");
navigate(`/organizations/${values.name}`);
}}
/>
</Stack>
);
};

export default CreateOrganizationPage;
147 changes: 147 additions & 0 deletions site/src/pages/ManagementSettingsPage/CreateOrganizationPageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Interpolation, Theme } from "@emotion/react";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import { useFormik } from "formik";
import { type FC, useState } from "react";
import { useMutation, useQueryClient } from "react-query";
import * as Yup from "yup";
import {
createOrganization,
updateOrganization,
deleteOrganization,
} from "api/queries/organizations";
import type {
CreateOrganizationRequest,
Organization,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import {
FormFields,
FormSection,
HorizontalForm,
FormFooter,
} from "components/Form/Form";
import { displaySuccess } from "components/GlobalSnackbar/utils";
import { IconField } from "components/IconField/IconField";
import { Margins } from "components/Margins/Margins";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import {
getFormHelpers,
nameValidator,
displayNameValidator,
onChangeTrimmed,
} from "utils/formUtils";
import { useOrganizationSettings } from "./ManagementSettingsLayout";

const MAX_DESCRIPTION_CHAR_LIMIT = 128;
const MAX_DESCRIPTION_MESSAGE = `Please enter a description that is no longer than ${MAX_DESCRIPTION_CHAR_LIMIT} characters.`;

const validationSchema = Yup.object({
name: nameValidator("Name"),
display_name: displayNameValidator("Display name"),
description: Yup.string().max(
MAX_DESCRIPTION_CHAR_LIMIT,
MAX_DESCRIPTION_MESSAGE,
),
});

interface CreateOrganizationPageViewProps {
error: unknown;
onSubmit: (values: CreateOrganizationRequest) => Promise<void>;
}

export const CreateOrganizationPageView: FC<
CreateOrganizationPageViewProps
> = ({ error, onSubmit }) => {
const form = useFormik<CreateOrganizationRequest>({
initialValues: {
name: "",
display_name: "",
description: "",
icon: "",
},
validationSchema,
onSubmit,
});
const getFieldHelpers = getFormHelpers(form, error);

return (
<div>
<PageHeader>
<PageHeaderTitle>Organization settings</PageHeaderTitle>
</PageHeader>

<HorizontalForm
onSubmit={form.handleSubmit}
aria-label="Organization settings form"
>
<FormSection
title="General info"
description="Change the name or description of the organization."
>
<fieldset
disabled={form.isSubmitting}
css={{ border: "unset", padding: 0, margin: 0, width: "100%" }}
>
<FormFields>
<TextField
{...getFieldHelpers("name")}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
label="Name"
/>
<TextField
{...getFieldHelpers("display_name")}
fullWidth
label="Display name"
/>
<TextField
{...getFieldHelpers("description")}
multiline
fullWidth
label="Description"
rows={2}
/>
<IconField
{...getFieldHelpers("icon")}
onChange={onChangeTrimmed(form)}
fullWidth
onPickEmoji={(value) => form.setFieldValue("icon", value)}
/>
</FormFields>
</fieldset>
</FormSection>
<FormFooter isLoading={form.isSubmitting} />
</HorizontalForm>
</div>
);
};

const styles = {
dangerButton: (theme) => ({
"&.MuiButton-contained": {
backgroundColor: theme.roles.danger.fill.solid,
borderColor: theme.roles.danger.fill.outline,

"&:not(.MuiLoadingButton-loading)": {
color: theme.roles.danger.fill.text,
},

"&:hover:not(:disabled)": {
backgroundColor: theme.roles.danger.hover.fill.solid,
borderColor: theme.roles.danger.hover.fill.outline,
},

"&.Mui-disabled": {
backgroundColor: theme.roles.danger.disabled.background,
borderColor: theme.roles.danger.disabled.outline,

"&:not(.MuiLoadingButton-loading)": {
color: theme.roles.danger.disabled.fill.text,
},
},
},
}),
} satisfies Record<string, Interpolation<Theme>>;
21 changes: 15 additions & 6 deletions site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, type FC, Suspense, useContext } from "react";
import { useQuery } from "react-query";
import { Outlet, useParams } from "react-router-dom";
import { Outlet, useLocation, useParams } from "react-router-dom";
import { myOrganizations } from "api/queries/users";
import type { Organization } from "api/typesGenerated";
import { Loader } from "components/Loader/Loader";
Expand Down Expand Up @@ -34,6 +34,7 @@ export const useOrganizationSettings = (): OrganizationSettingsContextValue => {
};

export const ManagementSettingsLayout: FC = () => {
const location = useLocation();
const { permissions, organizationIds } = useAuthenticated();
const { experiments } = useDashboard();
const { organization } = useParams() as { organization: string };
Expand All @@ -42,6 +43,12 @@ export const ManagementSettingsLayout: FC = () => {

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

console.log("oh jeez", organization);

const inOrganizationSettings =
location.pathname.startsWith("/organizations") &&
location.pathname !== "/organizations/new";

if (!multiOrgExperimentEnabled) {
return <NotFoundPage />;
}
Expand All @@ -53,11 +60,13 @@ export const ManagementSettingsLayout: FC = () => {
{organizationsQuery.data ? (
<OrganizationSettingsContext.Provider
value={{
currentOrganizationId: !organization
? organizationIds[0]
: organizationsQuery.data.find(
(org) => org.name === organization,
)?.id,
currentOrganizationId: !inOrganizationSettings
? undefined
: !organization
? organizationIds[0]
: organizationsQuery.data.find(
(org) => org.name === organization,
)?.id,
organizations: organizationsQuery.data,
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const OrganizationSettingsPageView: FC<

<HorizontalForm
onSubmit={form.handleSubmit}
aria-label="Template settings form"
aria-label="Organization settings form"
>
<FormSection
title="General info"
Expand Down
33 changes: 25 additions & 8 deletions site/src/pages/ManagementSettingsPage/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ export const Sidebar: FC = () => {
>
Organizations
</header>
<SidebarNavItem active href="new" icon={<AddIcon />}>
<SidebarNavItem
active="auto"
href="/organizations/new"
icon={<AddIcon />}
>
New organization
</SidebarNavItem>
{organizations.map((organization) => (
Expand Down Expand Up @@ -165,15 +169,28 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
const link = useClassName(classNames.link, []);
const activeLink = useClassName(classNames.activeLink, []);

const LinkC = active === "auto" ? NavLink : Link;
const content = (
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
);

if (active === "auto") {
return (
<NavLink
to={href}
className={({ isActive }) => cx([link, isActive && activeLink])}
>
{content}
</NavLink>
);
}

return (
<LinkC to={href} className={cx([link, active && activeLink])}>
<Stack alignItems="center" spacing={1.5} direction="row">
{icon}
{children}
</Stack>
</LinkC>
<Link to={href} className={cx([link, active && activeLink])}>
{content}
</Link>
);
};

Expand Down
55 changes: 31 additions & 24 deletions site/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ const AddNewLicensePage = lazy(
() =>
import("./pages/DeploySettingsPage/LicensesSettingsPage/AddNewLicensePage"),
);
const CreateOrganizationPage = lazy(
() => import("./pages/ManagementSettingsPage/CreateOrganizationPage"),
);
const OrganizationSettingsPage = lazy(
() => import("./pages/ManagementSettingsPage/OrganizationSettingsPage"),
);
Expand Down Expand Up @@ -333,31 +336,35 @@ export const router = createBrowserRouter(

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

<Route
path="/organizations/:organization?"
element={<ManagementSettingsLayout />}
>
<Route path="/organizations" element={<ManagementSettingsLayout />}>
<Route path="new" element={<CreateOrganizationPage />} />

{/* General settings for the default org can omit the organization name */}
<Route index element={<OrganizationSettingsPage />} />
<Route
path="external-auth"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="members"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="groups"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="metrics"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="auditing"
element={<OrganizationSettingsPlaceholder />}
/>

<Route path=":organization">
<Route index element={<OrganizationSettingsPage />} />
<Route
path="external-auth"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="members"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="groups"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="metrics"
element={<OrganizationSettingsPlaceholder />}
/>
<Route
path="auditing"
element={<OrganizationSettingsPlaceholder />}
/>
</Route>
</Route>

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