Skip to content

Commit a8a0be9

Browse files
authored
chore: expose all organization ids from AuthContext (coder#13268)
1 parent 721ab2a commit a8a0be9

File tree

31 files changed

+120
-124
lines changed

31 files changed

+120
-124
lines changed

site/src/contexts/auth/AuthProvider.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type AuthContextValue = {
3030
isUpdatingProfile: boolean;
3131
user: User | undefined;
3232
permissions: Permissions | undefined;
33-
organizationId: string | undefined;
33+
organizationIds: readonly string[] | undefined;
3434
signInError: unknown;
3535
updateProfileError: unknown;
3636
signOut: () => void;
@@ -119,7 +119,7 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
119119
permissions: permissionsQuery.data as Permissions | undefined,
120120
signInError: loginMutation.error,
121121
updateProfileError: updateProfileMutation.error,
122-
organizationId: userQuery.data?.organization_ids[0],
122+
organizationIds: userQuery.data?.organization_ids,
123123
}}
124124
>
125125
{children}

site/src/contexts/auth/RequireAuth.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const createAuthWrapper = (override: Partial<AuthContextValue>) => {
4545
isUpdatingProfile: false,
4646
permissions: undefined,
4747
authMethods: undefined,
48-
organizationId: undefined,
48+
organizationIds: undefined,
4949
signInError: undefined,
5050
updateProfileError: undefined,
5151
signOut: jest.fn(),
@@ -95,6 +95,7 @@ describe("useAuthenticated", () => {
9595
wrapper: createAuthWrapper({
9696
user: MockUser,
9797
permissions: MockPermissions,
98+
organizationIds: [],
9899
}),
99100
});
100101
}).not.toThrow();

site/src/contexts/auth/RequireAuth.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,18 @@ export const RequireAuth: FC = () => {
6666
);
6767
};
6868

69+
type RequireKeys<T, R extends keyof T> = Omit<T, R> & {
70+
[K in keyof Pick<T, R>]: NonNullable<T[K]>;
71+
};
72+
6973
// We can do some TS magic here but I would rather to be explicit on what
7074
// values are not undefined when authenticated
71-
type NonNullableAuth = AuthContextValue & {
72-
user: Exclude<AuthContextValue["user"], undefined>;
73-
permissions: Exclude<AuthContextValue["permissions"], undefined>;
74-
organizationId: Exclude<AuthContextValue["organizationId"], undefined>;
75-
};
75+
type AuthenticatedAuthContextValue = RequireKeys<
76+
AuthContextValue,
77+
"user" | "permissions" | "organizationIds"
78+
>;
7679

77-
export const useAuthenticated = (): NonNullableAuth => {
80+
export const useAuthenticated = (): AuthenticatedAuthContextValue => {
7881
const auth = useAuthContext();
7982

8083
if (!auth.user) {
@@ -85,5 +88,9 @@ export const useAuthenticated = (): NonNullableAuth => {
8588
throw new Error("Permissions are not available.");
8689
}
8790

88-
return auth as NonNullableAuth;
91+
if (!auth.organizationIds) {
92+
throw new Error("Organization ID is not available.");
93+
}
94+
95+
return auth as AuthenticatedAuthContextValue;
8996
};

site/src/modules/dashboard/DashboardProvider.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { createContext, type FC, type PropsWithChildren } from "react";
1+
import {
2+
createContext,
3+
type FC,
4+
type PropsWithChildren,
5+
useState,
6+
} from "react";
27
import { useQuery } from "react-query";
38
import { appearance } from "api/queries/appearance";
49
import { entitlements } from "api/queries/entitlements";
@@ -9,9 +14,13 @@ import type {
914
Experiments,
1015
} from "api/typesGenerated";
1116
import { Loader } from "components/Loader/Loader";
17+
import { useAuthenticated } from "contexts/auth/RequireAuth";
18+
import { useEffectEvent } from "hooks/hookPolyfills";
1219
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
1320

1421
export interface DashboardValue {
22+
organizationId: string;
23+
setOrganizationId: (id: string) => void;
1524
entitlements: Entitlements;
1625
experiments: Experiments;
1726
appearance: AppearanceConfig;
@@ -23,20 +32,40 @@ export const DashboardContext = createContext<DashboardValue | undefined>(
2332

2433
export const DashboardProvider: FC<PropsWithChildren> = ({ children }) => {
2534
const { metadata } = useEmbeddedMetadata();
35+
const { user, organizationIds } = useAuthenticated();
2636
const entitlementsQuery = useQuery(entitlements(metadata.entitlements));
2737
const experimentsQuery = useQuery(experiments(metadata.experiments));
2838
const appearanceQuery = useQuery(appearance(metadata.appearance));
2939

3040
const isLoading =
3141
!entitlementsQuery.data || !appearanceQuery.data || !experimentsQuery.data;
3242

43+
const lastUsedOrganizationId = localStorage.getItem(
44+
`user:${user.id}.lastUsedOrganizationId`,
45+
);
46+
const [activeOrganizationId, setActiveOrganizationId] = useState(() =>
47+
lastUsedOrganizationId && organizationIds.includes(lastUsedOrganizationId)
48+
? lastUsedOrganizationId
49+
: organizationIds[0],
50+
);
51+
52+
const setOrganizationId = useEffectEvent((id: string) => {
53+
if (!organizationIds.includes(id)) {
54+
throw new ReferenceError("Invalid organization ID");
55+
}
56+
localStorage.setItem(`user:${user.id}.lastUsedOrganizationId`, id);
57+
setActiveOrganizationId(id);
58+
});
59+
3360
if (isLoading) {
3461
return <Loader fullscreen />;
3562
}
3663

3764
return (
3865
<DashboardContext.Provider
3966
value={{
67+
organizationId: activeOrganizationId,
68+
setOrganizationId: setOrganizationId,
4069
entitlements: entitlementsQuery.data,
4170
experiments: experimentsQuery.data,
4271
appearance: appearanceQuery.data,

site/src/modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge.stories.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import type { Meta, StoryObj } from "@storybook/react";
2-
import { DashboardContext } from "modules/dashboard/DashboardProvider";
32
import {
4-
MockAppearanceConfig,
53
MockBuildInfo,
64
MockCanceledWorkspace,
75
MockCancelingWorkspace,
86
MockDeletedWorkspace,
97
MockDeletingWorkspace,
10-
MockEntitlementsWithScheduling,
11-
MockExperiments,
128
MockFailedWorkspace,
139
MockPendingWorkspace,
1410
MockStartingWorkspace,
1511
MockStoppedWorkspace,
1612
MockStoppingWorkspace,
1713
MockWorkspace,
1814
} from "testHelpers/entities";
15+
import { withDashboardProvider } from "testHelpers/storybook";
1916
import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge";
2017

2118
const meta: Meta<typeof WorkspaceStatusBadge> = {
@@ -29,19 +26,7 @@ const meta: Meta<typeof WorkspaceStatusBadge> = {
2926
},
3027
],
3128
},
32-
decorators: [
33-
(Story) => (
34-
<DashboardContext.Provider
35-
value={{
36-
entitlements: MockEntitlementsWithScheduling,
37-
experiments: MockExperiments,
38-
appearance: MockAppearanceConfig,
39-
}}
40-
>
41-
<Story />
42-
</DashboardContext.Provider>
43-
),
44-
],
29+
decorators: [withDashboardProvider],
4530
};
4631

4732
export default meta;

site/src/pages/CreateTemplatePage/DuplicateTemplateView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
} from "api/queries/templates";
1111
import { ErrorAlert } from "components/Alert/ErrorAlert";
1212
import { Loader } from "components/Loader/Loader";
13-
import { useAuthenticated } from "contexts/auth/RequireAuth";
1413
import { useDashboard } from "modules/dashboard/useDashboard";
1514
import { CreateTemplateForm } from "./CreateTemplateForm";
1615
import type { CreateTemplatePageViewProps } from "./types";
@@ -24,7 +23,7 @@ export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
2423
isCreating,
2524
}) => {
2625
const navigate = useNavigate();
27-
const { organizationId } = useAuthenticated();
26+
const { entitlements, organizationId } = useDashboard();
2827
const [searchParams] = useSearchParams();
2928
const templateByNameQuery = useQuery(
3029
templateByName(organizationId, searchParams.get("fromTemplate")!),
@@ -47,8 +46,7 @@ export const DuplicateTemplateView: FC<CreateTemplatePageViewProps> = ({
4746
templateVersionQuery.error ||
4847
templateVersionVariablesQuery.error;
4948

50-
const dashboard = useDashboard();
51-
const formPermissions = getFormPermissions(dashboard.entitlements);
49+
const formPermissions = getFormPermissions(entitlements);
5250

5351
const isJobError = error instanceof JobError;
5452
const templateVersionLogsQuery = useQuery({

site/src/pages/CreateTemplatePage/ImportStarterTemplateView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
} from "api/queries/templates";
1010
import { ErrorAlert } from "components/Alert/ErrorAlert";
1111
import { Loader } from "components/Loader/Loader";
12-
import { useAuthenticated } from "contexts/auth/RequireAuth";
1312
import { useDashboard } from "modules/dashboard/useDashboard";
1413
import { CreateTemplateForm } from "./CreateTemplateForm";
1514
import type { CreateTemplatePageViewProps } from "./types";
@@ -27,7 +26,7 @@ export const ImportStarterTemplateView: FC<CreateTemplatePageViewProps> = ({
2726
isCreating,
2827
}) => {
2928
const navigate = useNavigate();
30-
const { organizationId } = useAuthenticated();
29+
const { entitlements, organizationId } = useDashboard();
3130
const [searchParams] = useSearchParams();
3231
const templateExamplesQuery = useQuery(templateExamples(organizationId));
3332
const templateExample = templateExamplesQuery.data?.find(
@@ -37,8 +36,7 @@ export const ImportStarterTemplateView: FC<CreateTemplatePageViewProps> = ({
3736
const isLoading = templateExamplesQuery.isLoading;
3837
const loadingError = templateExamplesQuery.error;
3938

40-
const dashboard = useDashboard();
41-
const formPermissions = getFormPermissions(dashboard.entitlements);
39+
const formPermissions = getFormPermissions(entitlements);
4240

4341
const isJobError = error instanceof JobError;
4442
const templateVersionLogsQuery = useQuery({

site/src/pages/CreateTemplatePage/UploadTemplateView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
JobError,
88
templateVersionVariables,
99
} from "api/queries/templates";
10-
import { useAuthenticated } from "contexts/auth/RequireAuth";
1110
import { useDashboard } from "modules/dashboard/useDashboard";
1211
import { CreateTemplateForm } from "./CreateTemplateForm";
1312
import type { CreateTemplatePageViewProps } from "./types";
@@ -21,10 +20,9 @@ export const UploadTemplateView: FC<CreateTemplatePageViewProps> = ({
2120
error,
2221
}) => {
2322
const navigate = useNavigate();
24-
const { organizationId } = useAuthenticated();
2523

26-
const dashboard = useDashboard();
27-
const formPermissions = getFormPermissions(dashboard.entitlements);
24+
const { entitlements, organizationId } = useDashboard();
25+
const formPermissions = getFormPermissions(entitlements);
2826

2927
const uploadFileMutation = useMutation(uploadFile());
3028
const uploadedFile = uploadFileMutation.data;

site/src/pages/CreateUserPage/CreateUserPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
55
import { authMethods, createUser } from "api/queries/users";
66
import { displaySuccess } from "components/GlobalSnackbar/utils";
77
import { Margins } from "components/Margins/Margins";
8-
import { useAuthenticated } from "contexts/auth/RequireAuth";
8+
import { useDashboard } from "modules/dashboard/useDashboard";
99
import { pageTitle } from "utils/page";
1010
import { CreateUserForm } from "./CreateUserForm";
1111

@@ -14,7 +14,7 @@ export const Language = {
1414
};
1515

1616
export const CreateUserPage: FC = () => {
17-
const { organizationId } = useAuthenticated();
17+
const { organizationId } = useDashboard();
1818
const navigate = useNavigate();
1919
const queryClient = useQueryClient();
2020
const createUserMutation = useMutation(createUser(queryClient));

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ export type ExternalAuthPollingState = "idle" | "polling" | "abandoned";
3535

3636
const CreateWorkspacePage: FC = () => {
3737
const { template: templateName } = useParams() as { template: string };
38-
const { user: me, organizationId } = useAuthenticated();
38+
const { user: me } = useAuthenticated();
3939
const navigate = useNavigate();
4040
const [searchParams, setSearchParams] = useSearchParams();
41-
const { experiments } = useDashboard();
41+
const { experiments, organizationId } = useDashboard();
4242

4343
const customVersionId = searchParams.get("version") ?? undefined;
4444
const defaultName = searchParams.get("name");

site/src/pages/GroupsPage/CreateGroupPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ import { Helmet } from "react-helmet-async";
33
import { useMutation, useQueryClient } from "react-query";
44
import { useNavigate } from "react-router-dom";
55
import { createGroup } from "api/queries/groups";
6-
import { useAuthenticated } from "contexts/auth/RequireAuth";
6+
import { useDashboard } from "modules/dashboard/useDashboard";
77
import { pageTitle } from "utils/page";
88
import CreateGroupPageView from "./CreateGroupPageView";
99

1010
export const CreateGroupPage: FC = () => {
1111
const queryClient = useQueryClient();
1212
const navigate = useNavigate();
13-
const { organizationId } = useAuthenticated();
13+
const { organizationId } = useDashboard();
1414
const createGroupMutation = useMutation(createGroup(queryClient));
1515

1616
return (

site/src/pages/GroupsPage/GroupsPage.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { getErrorMessage } from "api/errors";
55
import { groups } from "api/queries/groups";
66
import { displayError } from "components/GlobalSnackbar/utils";
77
import { useAuthenticated } from "contexts/auth/RequireAuth";
8+
import { useDashboard } from "modules/dashboard/useDashboard";
89
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
910
import { pageTitle } from "utils/page";
1011
import GroupsPageView from "./GroupsPageView";
1112

1213
export const GroupsPage: FC = () => {
13-
const { organizationId, permissions } = useAuthenticated();
14+
const { permissions } = useAuthenticated();
15+
const { organizationId } = useDashboard();
1416
const { createGroup: canCreateGroup } = permissions;
1517
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility();
1618
const groupsQuery = useQuery(groups(organizationId));

site/src/pages/StarterTemplatePage/StarterTemplatePage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { Helmet } from "react-helmet-async";
33
import { useQuery } from "react-query";
44
import { useParams } from "react-router-dom";
55
import { templateExamples } from "api/queries/templates";
6-
import { useAuthenticated } from "contexts/auth/RequireAuth";
6+
import { useDashboard } from "modules/dashboard/useDashboard";
77
import { pageTitle } from "utils/page";
88
import { StarterTemplatePageView } from "./StarterTemplatePageView";
99

1010
const StarterTemplatePage: FC = () => {
1111
const { exampleId } = useParams() as { exampleId: string };
12-
const { organizationId } = useAuthenticated();
12+
const { organizationId } = useDashboard();
1313
const templateExamplesQuery = useQuery(templateExamples(organizationId));
1414
const starterTemplate = templateExamplesQuery.data?.find(
1515
(example) => example.id === exampleId,

site/src/pages/StarterTemplatesPage/StarterTemplatesPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { Helmet } from "react-helmet-async";
33
import { useQuery } from "react-query";
44
import { templateExamples } from "api/queries/templates";
55
import type { TemplateExample } from "api/typesGenerated";
6-
import { useAuthenticated } from "contexts/auth/RequireAuth";
6+
import { useDashboard } from "modules/dashboard/useDashboard";
77
import { pageTitle } from "utils/page";
88
import { getTemplatesByTag } from "utils/starterTemplates";
99
import { StarterTemplatesPageView } from "./StarterTemplatesPageView";
1010

1111
const StarterTemplatesPage: FC = () => {
12-
const { organizationId } = useAuthenticated();
12+
const { organizationId } = useDashboard();
1313
const templateExamplesQuery = useQuery(templateExamples(organizationId));
1414
const starterTemplatesByTag = templateExamplesQuery.data
1515
? // Currently, the scratch template should not be displayed on the starter templates page.

site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { Helmet } from "react-helmet-async";
33
import { useQuery } from "react-query";
44
import { previousTemplateVersion, templateFiles } from "api/queries/templates";
55
import { Loader } from "components/Loader/Loader";
6-
import { useAuthenticated } from "contexts/auth/RequireAuth";
6+
import { useDashboard } from "modules/dashboard/useDashboard";
77
import { TemplateFiles } from "modules/templates/TemplateFiles/TemplateFiles";
88
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
99
import { getTemplatePageTitle } from "../utils";
1010

1111
const TemplateFilesPage: FC = () => {
12-
const { organizationId } = useAuthenticated();
12+
const { organizationId } = useDashboard();
1313
const { template, activeVersion } = useTemplateLayoutContext();
1414
const { data: currentFiles } = useQuery(
1515
templateFiles(activeVersion.job.file_id),

site/src/pages/TemplatePage/TemplateLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
1313
import { Loader } from "components/Loader/Loader";
1414
import { Margins } from "components/Margins/Margins";
1515
import { TAB_PADDING_Y, TabLink, Tabs, TabsList } from "components/Tabs/Tabs";
16-
import { useAuthenticated } from "contexts/auth/RequireAuth";
16+
import { useDashboard } from "modules/dashboard/useDashboard";
1717
import { TemplatePageHeader } from "./TemplatePageHeader";
1818

1919
const templatePermissions = (
@@ -71,7 +71,7 @@ export const TemplateLayout: FC<PropsWithChildren> = ({
7171
children = <Outlet />,
7272
}) => {
7373
const navigate = useNavigate();
74-
const { organizationId } = useAuthenticated();
74+
const { organizationId } = useDashboard();
7575
const { template: templateName } = useParams() as { template: string };
7676
const { data, error, isLoading } = useQuery({
7777
queryKey: ["template", templateName],

0 commit comments

Comments
 (0)