Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd977654

Browse files
authored
feat: unify organization and deployment management settings (#13602)
1 parent9b1d8f7 commitd977654

19 files changed

+782
-254
lines changed

‎site/e2e/api.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,17 @@ export const createGroup = async (orgId: string) => {
5353
returngroup;
5454
};
5555

56+
exportconstcreateOrganization=async()=>{
57+
constname=randomName();
58+
constorg=awaitAPI.createOrganization({
59+
name,
60+
display_name:`Org${name}`,
61+
description:`Org description${name}`,
62+
icon:"/emojis/1f957.png",
63+
});
64+
returnorg;
65+
};
66+
5667
exportasyncfunctionverifyConfigFlagBoolean(
5768
page:Page,
5869
config:DeploymentConfig,

‎site/e2e/playwright.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export default defineConfig({
147147
gitAuth.validatePath,
148148
),
149149
CODER_PPROF_ADDRESS:"127.0.0.1:"+coderdPProfPort,
150-
CODER_EXPERIMENTS:e2eFakeExperiment1+","+e2eFakeExperiment2,
150+
CODER_EXPERIMENTS:`multi-organization,${e2eFakeExperiment1},${e2eFakeExperiment2}`,
151151

152152
// Tests for Deployment / User Authentication / OIDC
153153
CODER_OIDC_ISSUER_URL:"https://accounts.google.com",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import{test,expect}from"@playwright/test";
2+
import{setupApiCalls}from"../api";
3+
import{expectUrl}from"../expectUrl";
4+
import{requiresEnterpriseLicense}from"../helpers";
5+
import{beforeCoderTest}from"../hooks";
6+
7+
test.beforeEach(async({ page})=>{
8+
awaitbeforeCoderTest(page);
9+
awaitsetupApiCalls(page);
10+
});
11+
12+
test("create and delete organization",async({ page, baseURL})=>{
13+
requiresEnterpriseLicense();
14+
15+
// Create an organization
16+
awaitpage.goto(`${baseURL}/organizations/new`,{
17+
waitUntil:"domcontentloaded",
18+
});
19+
20+
awaitpage.getByLabel("Name",{exact:true}).fill("floop");
21+
awaitpage.getByLabel("Display name").fill("Floop");
22+
awaitpage.getByLabel("Description").fill("Org description floop");
23+
awaitpage.getByLabel("Icon",{exact:true}).fill("/emojis/1f957.png");
24+
25+
awaitpage.getByRole("button",{name:"Submit"}).click();
26+
27+
// Expect to be redirected to the new organization
28+
awaitexpectUrl(page).toHavePathName("/organizations/floop");
29+
awaitexpect(page.getByText("Organization created.")).toBeVisible();
30+
31+
awaitpage.getByRole("button",{name:"Delete this organization"}).click();
32+
constdialog=page.getByTestId("dialog");
33+
awaitdialog.getByLabel("Name").fill("floop");
34+
awaitdialog.getByRole("button",{name:"Delete"}).click();
35+
awaitexpect(page.getByText("Organization deleted.")).toBeVisible();
36+
});

‎site/src/components/Alert/Alert.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const Alert: FC<AlertProps> = ({
5252
size="small"
5353
onClick={()=>{
5454
setOpen(false);
55-
onDismiss&&onDismiss();
55+
onDismiss?.();
5656
}}
5757
data-testid="dismiss-banner-btn"
5858
>

‎site/src/pages/DeploySettingsPage/DeploySettingsLayout.tsx‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { Margins } from "components/Margins/Margins";
88
import{Stack}from"components/Stack/Stack";
99
import{useAuthenticated}from"contexts/auth/RequireAuth";
1010
import{RequirePermission}from"contexts/auth/RequirePermission";
11+
import{useDashboard}from"modules/dashboard/useDashboard";
12+
import{ManagementSettingsLayout}from"pages/ManagementSettingsPage/ManagementSettingsLayout";
1113
import{Sidebar}from"./Sidebar";
1214

1315
typeDeploySettingsContextValue={
1416
deploymentValues:DeploymentConfig;
1517
};
1618

17-
constDeploySettingsContext=createContext<
19+
exportconstDeploySettingsContext=createContext<
1820
DeploySettingsContextValue|undefined
1921
>(undefined);
2022

@@ -29,6 +31,18 @@ export const useDeploySettings = (): DeploySettingsContextValue => {
2931
};
3032

3133
exportconstDeploySettingsLayout:FC=()=>{
34+
const{ experiments}=useDashboard();
35+
36+
constmultiOrgExperimentEnabled=experiments.includes("multi-organization");
37+
38+
returnmultiOrgExperimentEnabled ?(
39+
<ManagementSettingsLayout/>
40+
) :(
41+
<DeploySettingsLayoutInner/>
42+
);
43+
};
44+
45+
constDeploySettingsLayoutInner:FC=()=>{
3246
constdeploymentConfigQuery=useQuery(deploymentConfig());
3347
const{ permissions}=useAuthenticated();
3448

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
importtype{FC}from"react";
2+
import{useMutation,useQueryClient}from"react-query";
3+
import{useNavigate}from"react-router-dom";
4+
import{createOrganization}from"api/queries/organizations";
5+
import{displaySuccess}from"components/GlobalSnackbar/utils";
6+
import{CreateOrganizationPageView}from"./CreateOrganizationPageView";
7+
8+
constCreateOrganizationPage:FC=()=>{
9+
constnavigate=useNavigate();
10+
11+
constqueryClient=useQueryClient();
12+
constcreateOrganizationMutation=useMutation(
13+
createOrganization(queryClient),
14+
);
15+
16+
consterror=createOrganizationMutation.error;
17+
18+
return(
19+
<CreateOrganizationPageView
20+
error={error}
21+
onSubmit={async(values)=>{
22+
awaitcreateOrganizationMutation.mutateAsync(values);
23+
displaySuccess("Organization created.");
24+
navigate(`/organizations/${values.name}`);
25+
}}
26+
/>
27+
);
28+
};
29+
30+
exportdefaultCreateOrganizationPage;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
importtype{Meta,StoryObj}from"@storybook/react";
2+
import{mockApiError}from"testHelpers/entities";
3+
import{CreateOrganizationPageView}from"./CreateOrganizationPageView";
4+
5+
constmeta:Meta<typeofCreateOrganizationPageView>={
6+
title:"pages/CreateOrganizationPageView",
7+
component:CreateOrganizationPageView,
8+
};
9+
10+
exportdefaultmeta;
11+
typeStory=StoryObj<typeofCreateOrganizationPageView>;
12+
13+
exportconstExample:Story={};
14+
15+
exportconstError:Story={
16+
args:{error:"Oh no!"},
17+
};
18+
19+
exportconstInvalidName:Story={
20+
args:{
21+
error:mockApiError({
22+
message:"Display name is bad",
23+
validations:[
24+
{
25+
field:"display_name",
26+
detail:"That display name is terrible. What were you thinking?",
27+
},
28+
],
29+
}),
30+
},
31+
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
importTextFieldfrom"@mui/material/TextField";
2+
import{useFormik}from"formik";
3+
importtype{FC}from"react";
4+
import*asYupfrom"yup";
5+
import{isApiValidationError}from"api/errors";
6+
importtype{CreateOrganizationRequest}from"api/typesGenerated";
7+
import{ErrorAlert}from"components/Alert/ErrorAlert";
8+
import{
9+
FormFields,
10+
FormSection,
11+
HorizontalForm,
12+
FormFooter,
13+
}from"components/Form/Form";
14+
import{IconField}from"components/IconField/IconField";
15+
import{PageHeader,PageHeaderTitle}from"components/PageHeader/PageHeader";
16+
import{
17+
getFormHelpers,
18+
nameValidator,
19+
displayNameValidator,
20+
onChangeTrimmed,
21+
}from"utils/formUtils";
22+
23+
constMAX_DESCRIPTION_CHAR_LIMIT=128;
24+
constMAX_DESCRIPTION_MESSAGE=`Please enter a description that is no longer than${MAX_DESCRIPTION_CHAR_LIMIT} characters.`;
25+
26+
constvalidationSchema=Yup.object({
27+
name:nameValidator("Name"),
28+
display_name:displayNameValidator("Display name"),
29+
description:Yup.string().max(
30+
MAX_DESCRIPTION_CHAR_LIMIT,
31+
MAX_DESCRIPTION_MESSAGE,
32+
),
33+
});
34+
35+
interfaceCreateOrganizationPageViewProps{
36+
error:unknown;
37+
onSubmit:(values:CreateOrganizationRequest)=>Promise<void>;
38+
}
39+
40+
exportconstCreateOrganizationPageView:FC<
41+
CreateOrganizationPageViewProps
42+
>=({ error, onSubmit})=>{
43+
constform=useFormik<CreateOrganizationRequest>({
44+
initialValues:{
45+
name:"",
46+
display_name:"",
47+
description:"",
48+
icon:"",
49+
},
50+
validationSchema,
51+
onSubmit,
52+
});
53+
constgetFieldHelpers=getFormHelpers(form,error);
54+
55+
return(
56+
<div>
57+
<PageHeader>
58+
<PageHeaderTitle>Organization settings</PageHeaderTitle>
59+
</PageHeader>
60+
61+
{Boolean(error)&&!isApiValidationError(error)&&(
62+
<divcss={{marginBottom:32}}>
63+
<ErrorAlerterror={error}/>
64+
</div>
65+
)}
66+
67+
<HorizontalForm
68+
onSubmit={form.handleSubmit}
69+
aria-label="Organization settings form"
70+
>
71+
<FormSection
72+
title="General info"
73+
description="Change the name or description of the organization."
74+
>
75+
<fieldset
76+
disabled={form.isSubmitting}
77+
css={{border:"unset",padding:0,margin:0,width:"100%"}}
78+
>
79+
<FormFields>
80+
<TextField
81+
{...getFieldHelpers("name")}
82+
onChange={onChangeTrimmed(form)}
83+
autoFocus
84+
fullWidth
85+
label="Name"
86+
/>
87+
<TextField
88+
{...getFieldHelpers("display_name")}
89+
fullWidth
90+
label="Display name"
91+
/>
92+
<TextField
93+
{...getFieldHelpers("description")}
94+
multiline
95+
fullWidth
96+
label="Description"
97+
rows={2}
98+
/>
99+
<IconField
100+
{...getFieldHelpers("icon")}
101+
onChange={onChangeTrimmed(form)}
102+
fullWidth
103+
onPickEmoji={(value)=>form.setFieldValue("icon",value)}
104+
/>
105+
</FormFields>
106+
</fieldset>
107+
</FormSection>
108+
<FormFooterisLoading={form.isSubmitting}/>
109+
</HorizontalForm>
110+
</div>
111+
);
112+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
importtype{Interpolation,Theme}from"@emotion/react";
2+
importtype{FC,HTMLAttributes,ReactNode}from"react";
3+
4+
exportconstHorizontalContainer:FC<HTMLAttributes<HTMLDivElement>>=({
5+
...attrs
6+
})=>{
7+
return<divcss={styles.horizontalContainer}{...attrs}/>;
8+
};
9+
10+
interfaceHorizontalSectionProps
11+
extendsOmit<HTMLAttributes<HTMLElement>,"title">{
12+
title:ReactNode;
13+
description:ReactNode;
14+
children?:ReactNode;
15+
}
16+
17+
exportconstHorizontalSection:FC<HorizontalSectionProps>=({
18+
children,
19+
title,
20+
description,
21+
...attrs
22+
})=>{
23+
return(
24+
<sectioncss={styles.formSection}{...attrs}>
25+
<divcss={styles.formSectionInfo}>
26+
<h2css={styles.formSectionInfoTitle}>{title}</h2>
27+
<divcss={styles.formSectionInfoDescription}>{description}</div>
28+
</div>
29+
30+
{children}
31+
</section>
32+
);
33+
};
34+
35+
conststyles={
36+
horizontalContainer:(theme)=>({
37+
display:"flex",
38+
flexDirection:"column",
39+
gap:80,
40+
41+
[theme.breakpoints.down("md")]:{
42+
gap:64,
43+
},
44+
}),
45+
46+
formSection:(theme)=>({
47+
display:"flex",
48+
flexDirection:"row",
49+
gap:120,
50+
51+
[theme.breakpoints.down("lg")]:{
52+
flexDirection:"column",
53+
gap:16,
54+
},
55+
}),
56+
57+
formSectionInfo:(theme)=>({
58+
width:"100%",
59+
flexShrink:0,
60+
top:24,
61+
maxWidth:312,
62+
position:"sticky",
63+
64+
[theme.breakpoints.down("md")]:{
65+
width:"100%",
66+
position:"initial",
67+
},
68+
}),
69+
70+
formSectionInfoTitle:(theme)=>({
71+
fontSize:20,
72+
color:theme.palette.text.primary,
73+
fontWeight:400,
74+
margin:0,
75+
marginBottom:8,
76+
display:"flex",
77+
flexDirection:"row",
78+
alignItems:"center",
79+
gap:12,
80+
}),
81+
82+
formSectionInfoDescription:(theme)=>({
83+
fontSize:14,
84+
color:theme.palette.text.secondary,
85+
lineHeight:"160%",
86+
margin:0,
87+
}),
88+
}satisfiesRecord<string,Interpolation<Theme>>;

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp