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
);

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy