Skip to content

Commit 8c1bd32

Browse files
authored
feat(site): add basic organization management ui (#13288)
1 parent 07cd9ac commit 8c1bd32

32 files changed

+743
-187
lines changed

codersdk/organizations.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ type OrganizationMemberWithName struct {
6666
type CreateOrganizationRequest struct {
6767
Name string `json:"name" validate:"required,organization_name"`
6868
// DisplayName will default to the same value as `Name` if not provided.
69-
DisplayName string `json:"display_name" validate:"omitempty,organization_display_name"`
69+
DisplayName string `json:"display_name,omitempty" validate:"omitempty,organization_display_name"`
7070
Description string `json:"description,omitempty"`
7171
Icon string `json:"icon,omitempty"`
7272
}

site/src/api/api.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,31 @@ class ApiMethods {
505505
return response.data;
506506
};
507507

508+
createOrganization = async (params: TypesGen.CreateOrganizationRequest) => {
509+
const response = await this.axios.post<TypesGen.Organization>(
510+
"/api/v2/organizations",
511+
params,
512+
);
513+
return response.data;
514+
};
515+
516+
updateOrganization = async (
517+
orgId: string,
518+
params: TypesGen.UpdateOrganizationRequest,
519+
) => {
520+
const response = await this.axios.patch<TypesGen.Organization>(
521+
`/api/v2/organizations/${orgId}`,
522+
params,
523+
);
524+
return response.data;
525+
};
526+
527+
deleteOrganization = async (orgId: string) => {
528+
await this.axios.delete<TypesGen.Organization>(
529+
`/api/v2/organizations/${orgId}`,
530+
);
531+
};
532+
508533
getOrganization = async (
509534
organizationId: string,
510535
): Promise<TypesGen.Organization> => {

site/src/api/queries/organizations.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { QueryClient } from "react-query";
2+
import { API } from "api/api";
3+
import type {
4+
CreateOrganizationRequest,
5+
UpdateOrganizationRequest,
6+
} from "api/typesGenerated";
7+
import { meKey, myOrganizationsKey } from "./users";
8+
9+
export const createOrganization = (queryClient: QueryClient) => {
10+
return {
11+
mutationFn: (params: CreateOrganizationRequest) =>
12+
API.createOrganization(params),
13+
14+
onSuccess: async () => {
15+
await queryClient.invalidateQueries(meKey);
16+
await queryClient.invalidateQueries(myOrganizationsKey);
17+
},
18+
};
19+
};
20+
21+
interface UpdateOrganizationVariables {
22+
orgId: string;
23+
req: UpdateOrganizationRequest;
24+
}
25+
26+
export const updateOrganization = (queryClient: QueryClient) => {
27+
return {
28+
mutationFn: (variables: UpdateOrganizationVariables) =>
29+
API.updateOrganization(variables.orgId, variables.req),
30+
31+
onSuccess: async () => {
32+
await queryClient.invalidateQueries(myOrganizationsKey);
33+
},
34+
};
35+
};
36+
37+
export const deleteOrganization = (queryClient: QueryClient) => {
38+
return {
39+
mutationFn: (orgId: string) => API.deleteOrganization(orgId),
40+
41+
onSuccess: async () => {
42+
await queryClient.invalidateQueries(meKey);
43+
await queryClient.invalidateQueries(myOrganizationsKey);
44+
},
45+
};
46+
};

site/src/api/queries/users.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const authMethods = () => {
124124
};
125125
};
126126

127-
const meKey = ["me"];
127+
export const meKey = ["me"];
128128

129129
export const me = (metadata: MetadataState<User>) => {
130130
return cachedQuery({
@@ -250,9 +250,11 @@ export const updateAppearanceSettings = (
250250
};
251251
};
252252

253+
export const myOrganizationsKey = ["organizations", "me"] as const;
254+
253255
export const myOrganizations = () => {
254256
return {
255-
queryKey: ["organizations", "me"],
257+
queryKey: myOrganizationsKey,
256258
queryFn: () => API.getOrganizations(),
257259
};
258260
};

site/src/api/typesGenerated.ts

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

site/src/components/FormFooter/FormFooter.stories.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,31 @@
1+
import { action } from "@storybook/addon-actions";
12
import type { Meta, StoryObj } from "@storybook/react";
23
import { FormFooter } from "./FormFooter";
34

45
const meta: Meta<typeof FormFooter> = {
56
title: "components/FormFooter",
67
component: FormFooter,
8+
args: {
9+
isLoading: false,
10+
onCancel: action("onCancel"),
11+
},
712
};
813

914
export default meta;
1015
type Story = StoryObj<typeof FormFooter>;
1116

1217
export const Ready: Story = {
18+
args: {},
19+
};
20+
21+
export const NoCancel: Story = {
1322
args: {
14-
isLoading: false,
23+
onCancel: undefined,
1524
},
1625
};
1726

1827
export const Custom: Story = {
1928
args: {
20-
isLoading: false,
2129
submitLabel: "Create",
2230
},
2331
};

site/src/components/FormFooter/FormFooter.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface FormFooterStyles {
1414
}
1515

1616
export interface FormFooterProps {
17-
onCancel: () => void;
17+
onCancel?: () => void;
1818
isLoading: boolean;
1919
styles?: FormFooterStyles;
2020
submitLabel?: string;
@@ -45,15 +45,17 @@ export const FormFooter: FC<FormFooterProps> = ({
4545
>
4646
{submitLabel}
4747
</LoadingButton>
48-
<Button
49-
size="large"
50-
type="button"
51-
css={styles.button}
52-
onClick={onCancel}
53-
tabIndex={0}
54-
>
55-
{Language.cancelLabel}
56-
</Button>
48+
{onCancel && (
49+
<Button
50+
size="large"
51+
type="button"
52+
css={styles.button}
53+
onClick={onCancel}
54+
tabIndex={0}
55+
>
56+
{Language.cancelLabel}
57+
</Button>
58+
)}
5759
{extraActions}
5860
</div>
5961
);

site/src/components/Margins/Margins.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,29 @@ const widthBySize: Record<Size, number> = {
1313
small: containerWidth / 3,
1414
};
1515

16-
export const Margins: FC<JSX.IntrinsicElements["div"] & { size?: Size }> = ({
16+
type MarginsProps = JSX.IntrinsicElements["div"] & {
17+
size?: Size;
18+
};
19+
20+
export const Margins: FC<MarginsProps> = ({
1721
size = "regular",
22+
children,
1823
...divProps
1924
}) => {
2025
const maxWidth = widthBySize[size];
2126
return (
2227
<div
2328
{...divProps}
2429
css={{
25-
margin: "0 auto",
30+
marginLeft: "auto",
31+
marginRight: "auto",
2632
maxWidth: maxWidth,
27-
padding: `0 ${sidePadding}px`,
33+
paddingLeft: sidePadding,
34+
paddingRight: sidePadding,
2835
width: "100%",
2936
}}
30-
/>
37+
>
38+
{children}
39+
</div>
3140
);
3241
};

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,25 @@ import {
1313
import { USERS_LINK } from "modules/navigation";
1414

1515
interface DeploymentDropdownProps {
16-
canViewAuditLog: boolean;
1716
canViewDeployment: boolean;
17+
canViewOrganizations: boolean;
1818
canViewAllUsers: boolean;
19+
canViewAuditLog: boolean;
1920
canViewHealth: boolean;
2021
}
2122

2223
export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
23-
canViewAuditLog,
2424
canViewDeployment,
25+
canViewOrganizations,
2526
canViewAllUsers,
27+
canViewAuditLog,
2628
canViewHealth,
2729
}) => {
2830
const theme = useTheme();
2931

3032
if (
3133
!canViewAuditLog &&
34+
!canViewOrganizations &&
3235
!canViewDeployment &&
3336
!canViewAllUsers &&
3437
!canViewHealth
@@ -64,9 +67,10 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
6467
}}
6568
>
6669
<DeploymentDropdownContent
67-
canViewAuditLog={canViewAuditLog}
6870
canViewDeployment={canViewDeployment}
71+
canViewOrganizations={canViewOrganizations}
6972
canViewAllUsers={canViewAllUsers}
73+
canViewAuditLog={canViewAuditLog}
7074
canViewHealth={canViewHealth}
7175
/>
7276
</PopoverContent>
@@ -75,9 +79,10 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
7579
};
7680

7781
const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
78-
canViewAuditLog,
7982
canViewDeployment,
83+
canViewOrganizations,
8084
canViewAllUsers,
85+
canViewAuditLog,
8186
canViewHealth,
8287
}) => {
8388
const popover = usePopover();
@@ -96,6 +101,16 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
96101
Settings
97102
</MenuItem>
98103
)}
104+
{canViewOrganizations && (
105+
<MenuItem
106+
component={NavLink}
107+
to="/organizations"
108+
css={styles.menuItem}
109+
onClick={onPopoverClose}
110+
>
111+
Organizations
112+
</MenuItem>
113+
)}
99114
{canViewAllUsers && (
100115
<MenuItem
101116
component={NavLink}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const Navbar: FC = () => {
1212
const { metadata } = useEmbeddedMetadata();
1313
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
1414

15-
const { appearance } = useDashboard();
15+
const { appearance, experiments } = useDashboard();
1616
const { user: me, permissions, signOut } = useAuthenticated();
1717
const featureVisibility = useFeatureVisibility();
1818
const canViewAuditLog =
@@ -29,10 +29,11 @@ export const Navbar: FC = () => {
2929
buildInfo={buildInfoQuery.data}
3030
supportLinks={appearance.support_links}
3131
onSignOut={signOut}
32-
canViewAuditLog={canViewAuditLog}
3332
canViewDeployment={canViewDeployment}
33+
canViewOrganizations={experiments.includes("multi-organization")}
3434
canViewAllUsers={canViewAllUsers}
3535
canViewHealth={canViewHealth}
36+
canViewAuditLog={canViewAuditLog}
3637
proxyContextValue={proxyContextValue}
3738
/>
3839
);

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ describe("NavbarView", () => {
2828
proxyContextValue={proxyContextValue}
2929
user={MockUser}
3030
onSignOut={noop}
31-
canViewAuditLog
3231
canViewDeployment
32+
canViewOrganizations
3333
canViewAllUsers
3434
canViewHealth
35+
canViewAuditLog
3536
/>,
3637
);
3738
const workspacesLink = await screen.findByText(navLanguage.workspaces);
@@ -44,10 +45,11 @@ describe("NavbarView", () => {
4445
proxyContextValue={proxyContextValue}
4546
user={MockUser}
4647
onSignOut={noop}
47-
canViewAuditLog
4848
canViewDeployment
49+
canViewOrganizations
4950
canViewAllUsers
5051
canViewHealth
52+
canViewAuditLog
5153
/>,
5254
);
5355
const templatesLink = await screen.findByText(navLanguage.templates);
@@ -60,10 +62,11 @@ describe("NavbarView", () => {
6062
proxyContextValue={proxyContextValue}
6163
user={MockUser}
6264
onSignOut={noop}
63-
canViewAuditLog
6465
canViewDeployment
66+
canViewOrganizations
6567
canViewAllUsers
6668
canViewHealth
69+
canViewAuditLog
6770
/>,
6871
);
6972
const deploymentMenu = await screen.findByText("Deployment");
@@ -78,10 +81,11 @@ describe("NavbarView", () => {
7881
proxyContextValue={proxyContextValue}
7982
user={MockUser}
8083
onSignOut={noop}
81-
canViewAuditLog
8284
canViewDeployment
85+
canViewOrganizations
8386
canViewAllUsers
8487
canViewHealth
88+
canViewAuditLog
8589
/>,
8690
);
8791
const deploymentMenu = await screen.findByText("Deployment");
@@ -96,10 +100,11 @@ describe("NavbarView", () => {
96100
proxyContextValue={proxyContextValue}
97101
user={MockUser}
98102
onSignOut={noop}
99-
canViewAuditLog
100103
canViewDeployment
104+
canViewOrganizations
101105
canViewAllUsers
102106
canViewHealth
107+
canViewAuditLog
103108
/>,
104109
);
105110
const deploymentMenu = await screen.findByText("Deployment");

0 commit comments

Comments
 (0)