Skip to content

Commit d68340b

Browse files
authored
feat: manage groups from deployment settings for single-org deployments (coder#14016)
1 parent 68fa34f commit d68340b

File tree

11 files changed

+137
-125
lines changed

11 files changed

+137
-125
lines changed

site/src/api/queries/groups.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,28 @@ import type {
66
PatchGroupRequest,
77
} from "api/typesGenerated";
88

9-
const GROUPS_QUERY_KEY = ["groups"];
109
type GroupSortOrder = "asc" | "desc";
1110

12-
const getGroupQueryKey = (organizationId: string, groupName: string) => [
11+
const getGroupsQueryKey = (organizationId: string) => [
12+
"organization",
1313
organizationId,
14-
"group",
15-
groupName,
14+
"groups",
1615
];
1716

1817
export const groups = (organizationId: string) => {
1918
return {
20-
queryKey: GROUPS_QUERY_KEY,
19+
queryKey: getGroupsQueryKey(organizationId),
2120
queryFn: () => API.getGroups(organizationId),
2221
} satisfies UseQueryOptions<Group[]>;
2322
};
2423

24+
const getGroupQueryKey = (organizationId: string, groupName: string) => [
25+
"organization",
26+
organizationId,
27+
"group",
28+
groupName,
29+
];
30+
2531
export const group = (organizationId: string, groupName: string) => {
2632
return {
2733
queryKey: getGroupQueryKey(organizationId, groupName),
@@ -97,7 +103,7 @@ export const createGroup = (
97103
mutationFn: (request: CreateGroupRequest) =>
98104
API.createGroup(organizationId, request),
99105
onSuccess: async () => {
100-
await queryClient.invalidateQueries(GROUPS_QUERY_KEY);
106+
await queryClient.invalidateQueries(getGroupsQueryKey(organizationId));
101107
},
102108
};
103109
};
@@ -146,7 +152,7 @@ export const invalidateGroup = (
146152
groupId: string,
147153
) =>
148154
Promise.all([
149-
queryClient.invalidateQueries(GROUPS_QUERY_KEY),
155+
queryClient.invalidateQueries(getGroupsQueryKey(organizationId)),
150156
queryClient.invalidateQueries(getGroupQueryKey(organizationId, groupId)),
151157
]);
152158

site/src/modules/dashboard/Navbar/Navbar.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export const Navbar: FC = () => {
1818
const canViewAuditLog =
1919
featureVisibility["audit_log"] && Boolean(permissions.viewAuditLog);
2020
const canViewDeployment = Boolean(permissions.viewDeploymentValues);
21+
const canViewOrganizations =
22+
featureVisibility.multiple_organizations &&
23+
experiments.includes("multi-organization");
2124
const canViewAllUsers = Boolean(permissions.readAllUsers);
2225
const proxyContextValue = useProxy();
2326
const canViewHealth = canViewDeployment;
@@ -30,7 +33,7 @@ export const Navbar: FC = () => {
3033
supportLinks={appearance.support_links}
3134
onSignOut={signOut}
3235
canViewDeployment={canViewDeployment}
33-
canViewOrganizations={experiments.includes("multi-organization")}
36+
canViewOrganizations={canViewOrganizations}
3437
canViewAllUsers={canViewAllUsers}
3538
canViewHealth={canViewHealth}
3639
canViewAuditLog={canViewAuditLog}

site/src/pages/ManagementSettingsPage/GroupsPage/CreateGroupPage.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const CreateGroupPage: FC = () => {
1111
const navigate = useNavigate();
1212
const { organization } = useParams() as { organization: string };
1313
const createGroupMutation = useMutation(
14-
createGroup(queryClient, organization),
14+
createGroup(queryClient, organization ?? "default"),
1515
);
1616

1717
return (
@@ -22,7 +22,11 @@ export const CreateGroupPage: FC = () => {
2222
<CreateGroupPageView
2323
onSubmit={async (data) => {
2424
const newGroup = await createGroupMutation.mutateAsync(data);
25-
navigate(`/organizations/${organization}/groups/${newGroup.name}`);
25+
navigate(
26+
organization
27+
? `/organizations/${organization}/groups/${newGroup.name}`
28+
: `/deployment/groups/${newGroup.name}`,
29+
);
2630
}}
2731
error={createGroupMutation.error}
2832
isLoading={createGroupMutation.isLoading}

site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import { isEveryoneGroup } from "utils/groups";
5050
import { pageTitle } from "utils/page";
5151

5252
export const GroupPage: FC = () => {
53-
const { organization, groupName } = useParams() as {
53+
const { organization = "default", groupName } = useParams() as {
5454
organization: string;
5555
groupName: string;
5656
};

site/src/pages/ManagementSettingsPage/GroupsPage/GroupSettingsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { pageTitle } from "utils/page";
1111
import GroupSettingsPageView from "./GroupSettingsPageView";
1212

1313
export const GroupSettingsPage: FC = () => {
14-
const { organization, groupName } = useParams() as {
14+
const { organization = "default", groupName } = useParams() as {
1515
organization: string;
1616
groupName: string;
1717
};

site/src/pages/ManagementSettingsPage/GroupsPage/GroupsPage.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,36 @@ import Button from "@mui/material/Button";
33
import { type FC, useEffect } from "react";
44
import { Helmet } from "react-helmet-async";
55
import { useQuery } from "react-query";
6-
import { Link as RouterLink } from "react-router-dom";
6+
import {
7+
Navigate,
8+
Link as RouterLink,
9+
useLocation,
10+
useParams,
11+
} from "react-router-dom";
712
import { getErrorMessage } from "api/errors";
813
import { groups } from "api/queries/groups";
14+
import type { Organization } from "api/typesGenerated";
915
import { displayError } from "components/GlobalSnackbar/utils";
1016
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
1117
import { useAuthenticated } from "contexts/auth/RequireAuth";
18+
import { useDashboard } from "modules/dashboard/useDashboard";
1219
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
1320
import { pageTitle } from "utils/page";
1421
import { useOrganizationSettings } from "../ManagementSettingsLayout";
1522
import GroupsPageView from "./GroupsPageView";
1623

1724
export const GroupsPage: FC = () => {
1825
const { permissions } = useAuthenticated();
19-
const { currentOrganizationId } = useOrganizationSettings();
2026
const { createGroup: canCreateGroup } = permissions;
21-
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility();
22-
const groupsQuery = useQuery(groups(currentOrganizationId!));
27+
const {
28+
multiple_organizations: organizationsEnabled,
29+
template_rbac: isTemplateRBACEnabled,
30+
} = useFeatureVisibility();
31+
const { experiments } = useDashboard();
32+
const location = useLocation();
33+
const { organization = "default" } = useParams() as { organization: string };
34+
const groupsQuery = useQuery(groups(organization));
35+
const { organizations } = useOrganizationSettings();
2336

2437
useEffect(() => {
2538
if (groupsQuery.error) {
@@ -29,6 +42,16 @@ export const GroupsPage: FC = () => {
2942
}
3043
}, [groupsQuery.error]);
3144

45+
if (
46+
organizationsEnabled &&
47+
experiments.includes("multi-organization") &&
48+
location.pathname === "/deployment/groups"
49+
) {
50+
const defaultName =
51+
getOrganizationNameByDefault(organizations) ?? "default";
52+
return <Navigate to={`/organizations/${defaultName}/groups`} replace />;
53+
}
54+
3255
return (
3356
<>
3457
<Helmet>
@@ -63,3 +86,6 @@ export const GroupsPage: FC = () => {
6386
};
6487

6588
export default GroupsPage;
89+
90+
export const getOrganizationNameByDefault = (organizations: Organization[]) =>
91+
organizations.find((org) => org.is_default)?.name;

site/src/pages/ManagementSettingsPage/ManagementSettingsLayout.tsx

Lines changed: 2 additions & 25 deletions
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, useLocation, useParams } from "react-router-dom";
3+
import { Outlet } from "react-router-dom";
44
import { deploymentConfig } from "api/queries/deployment";
55
import { organizations } from "api/queries/organizations";
66
import type { Organization } from "api/typesGenerated";
@@ -15,7 +15,6 @@ import { DeploySettingsContext } from "../DeploySettingsPage/DeploySettingsLayou
1515
import { Sidebar } from "./Sidebar";
1616

1717
type OrganizationSettingsContextValue = {
18-
currentOrganizationId?: string;
1918
organizations: Organization[];
2019
};
2120

@@ -34,19 +33,13 @@ export const useOrganizationSettings = (): OrganizationSettingsContextValue => {
3433
};
3534

3635
export const ManagementSettingsLayout: FC = () => {
37-
const location = useLocation();
3836
const { permissions } = useAuthenticated();
3937
const { experiments } = useDashboard();
40-
const { organization } = useParams() as { organization: string };
4138
const deploymentConfigQuery = useQuery(deploymentConfig());
4239
const organizationsQuery = useQuery(organizations());
4340

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

46-
const inOrganizationSettings =
47-
location.pathname.startsWith("/organizations") &&
48-
location.pathname !== "/organizations/new";
49-
5043
if (!multiOrgExperimentEnabled) {
5144
return <NotFoundPage />;
5245
}
@@ -57,17 +50,7 @@ export const ManagementSettingsLayout: FC = () => {
5750
<Stack css={{ padding: "48px 0" }} direction="row" spacing={6}>
5851
{organizationsQuery.data ? (
5952
<OrganizationSettingsContext.Provider
60-
value={{
61-
currentOrganizationId: !inOrganizationSettings
62-
? undefined
63-
: !organization
64-
? getOrganizationIdByDefault(organizationsQuery.data)
65-
: getOrganizationIdByName(
66-
organizationsQuery.data,
67-
organization,
68-
),
69-
organizations: organizationsQuery.data,
70-
}}
53+
value={{ organizations: organizationsQuery.data }}
7154
>
7255
<Sidebar />
7356
<main css={{ width: "100%" }}>
@@ -94,9 +77,3 @@ export const ManagementSettingsLayout: FC = () => {
9477
</RequirePermission>
9578
);
9679
};
97-
98-
const getOrganizationIdByName = (organizations: Organization[], name: string) =>
99-
organizations.find((org) => org.name === name)?.id;
100-
101-
const getOrganizationIdByDefault = (organizations: Organization[]) =>
102-
organizations.find((org) => org.is_default)?.id;

site/src/pages/ManagementSettingsPage/OrganizationSettingsPage.tsx

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import type { FC } from "react";
22
import { useMutation, useQueryClient } from "react-query";
3-
import { useNavigate } from "react-router-dom";
3+
import { useNavigate, useParams } from "react-router-dom";
44
import {
55
updateOrganization,
66
deleteOrganization,
77
} from "api/queries/organizations";
8+
import type { Organization } from "api/typesGenerated";
89
import { EmptyState } from "components/EmptyState/EmptyState";
910
import { displaySuccess } from "components/GlobalSnackbar/utils";
1011
import { useOrganizationSettings } from "./ManagementSettingsLayout";
1112
import { OrganizationSettingsPageView } from "./OrganizationSettingsPageView";
1213

1314
const OrganizationSettingsPage: FC = () => {
14-
const navigate = useNavigate();
15+
const { organization: organizationName } = useParams() as {
16+
organization?: string;
17+
};
18+
const { organizations } = useOrganizationSettings();
1519

20+
const navigate = useNavigate();
1621
const queryClient = useQueryClient();
1722
const updateOrganizationMutation = useMutation(
1823
updateOrganization(queryClient),
@@ -21,14 +26,14 @@ const OrganizationSettingsPage: FC = () => {
2126
deleteOrganization(queryClient),
2227
);
2328

24-
const { currentOrganizationId, organizations } = useOrganizationSettings();
25-
26-
const org = organizations.find((org) => org.id === currentOrganizationId);
29+
const org = organizationName
30+
? getOrganizationByName(organizations, organizationName)
31+
: getOrganizationByDefault(organizations);
2732

2833
const error =
2934
updateOrganizationMutation.error ?? deleteOrganizationMutation.error;
3035

31-
if (!currentOrganizationId || !org) {
36+
if (!org) {
3237
return <EmptyState message="Organization not found" />;
3338
}
3439

@@ -55,3 +60,9 @@ const OrganizationSettingsPage: FC = () => {
5560
};
5661

5762
export default OrganizationSettingsPage;
63+
64+
const getOrganizationByDefault = (organizations: Organization[]) =>
65+
organizations.find((org) => org.is_default);
66+
67+
const getOrganizationByName = (organizations: Organization[], name: string) =>
68+
organizations.find((org) => org.name === name);

site/src/pages/ManagementSettingsPage/OrganizationSettingsPlaceholder.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

0 commit comments

Comments
 (0)