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

Commit7599ad4

Browse files
feat: Add template settings page (#3557)
1 parentaabb727 commit7599ad4

File tree

11 files changed

+460
-8
lines changed

11 files changed

+460
-8
lines changed

‎site/src/AppRouter.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import{useSelector}from"@xstate/react"
22
import{SetupPage}from"pages/SetupPage/SetupPage"
3+
import{TemplateSettingsPage}from"pages/TemplateSettingsPage/TemplateSettingsPage"
34
import{FC,lazy,Suspense,useContext}from"react"
45
import{Navigate,Route,Routes}from"react-router-dom"
56
import{selectPermissions}from"xServices/auth/authSelectors"
@@ -97,6 +98,14 @@ export const AppRouter: FC = () => {
9798
</RequireAuth>
9899
}
99100
/>
101+
<Route
102+
path="settings"
103+
element={
104+
<RequireAuth>
105+
<TemplateSettingsPage/>
106+
</RequireAuth>
107+
}
108+
/>
100109
</Route>
101110
</Route>
102111

‎site/src/api/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,14 @@ export const getTemplateVersions = async (
145145
returnresponse.data
146146
}
147147

148+
exportconstupdateTemplateMeta=async(
149+
templateId:string,
150+
data:TypesGen.UpdateTemplateMeta,
151+
):Promise<TypesGen.Template>=>{
152+
constresponse=awaitaxios.patch<TypesGen.Template>(`/api/v2/templates/${templateId}`,data)
153+
returnresponse.data
154+
}
155+
148156
exportconstgetWorkspace=async(
149157
workspaceId:string,
150158
params?:TypesGen.WorkspaceOptions,

‎site/src/pages/TemplatePage/TemplatePageView.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Button from "@material-ui/core/Button"
22
importLinkfrom"@material-ui/core/Link"
33
import{makeStyles}from"@material-ui/core/styles"
44
importAddCircleOutlinefrom"@material-ui/icons/AddCircleOutline"
5+
importSettingsOutlinedfrom"@material-ui/icons/SettingsOutlined"
56
importfrontMatterfrom"front-matter"
67
import{FC}from"react"
78
importReactMarkdownfrom"react-markdown"
@@ -20,6 +21,7 @@ import { VersionsTable } from "../../components/VersionsTable/VersionsTable"
2021
import{WorkspaceSection}from"../../components/WorkspaceSection/WorkspaceSection"
2122

2223
constLanguage={
24+
settingsButton:"Settings",
2325
createButton:"Create workspace",
2426
noDescription:"",
2527
readmeTitle:"README",
@@ -51,13 +53,24 @@ export const TemplatePageView: FC<TemplatePageViewProps> = ({
5153
<Margins>
5254
<PageHeader
5355
actions={
54-
<Link
55-
underline="none"
56-
component={RouterLink}
57-
to={`/templates/${template.name}/workspace`}
58-
>
59-
<ButtonstartIcon={<AddCircleOutline/>}>{Language.createButton}</Button>
60-
</Link>
56+
<Stackdirection="row"spacing={1}>
57+
<Link
58+
underline="none"
59+
component={RouterLink}
60+
to={`/templates/${template.name}/settings`}
61+
>
62+
<Buttonvariant="outlined"startIcon={<SettingsOutlined/>}>
63+
{Language.settingsButton}
64+
</Button>
65+
</Link>
66+
<Link
67+
underline="none"
68+
component={RouterLink}
69+
to={`/templates/${template.name}/workspace`}
70+
>
71+
<ButtonstartIcon={<AddCircleOutline/>}>{Language.createButton}</Button>
72+
</Link>
73+
</Stack>
6174
}
6275
>
6376
<PageHeaderTitle>{template.name}</PageHeaderTitle>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
importTextFieldfrom"@material-ui/core/TextField"
2+
import{Template,UpdateTemplateMeta}from"api/typesGenerated"
3+
import{FormFooter}from"components/FormFooter/FormFooter"
4+
import{Stack}from"components/Stack/Stack"
5+
import{FormikContextType,FormikTouched,useFormik}from"formik"
6+
import{FC}from"react"
7+
import{getFormHelpersWithError,nameValidator,onChangeTrimmed}from"util/formUtils"
8+
import*asYupfrom"yup"
9+
10+
exportconstLanguage={
11+
nameLabel:"Name",
12+
descriptionLabel:"Description",
13+
maxTtlLabel:"Max TTL",
14+
// This is the same from the CLI on https://github.com/coder/coder/blob/546157b63ef9204658acf58cb653aa9936b70c49/cli/templateedit.go#L59
15+
maxTtlHelperText:"Edit the template maximum time before shutdown in milliseconds",
16+
formAriaLabel:"Template settings form",
17+
}
18+
19+
exportconstvalidationSchema=Yup.object({
20+
name:nameValidator(Language.nameLabel),
21+
description:Yup.string(),
22+
max_ttl_ms:Yup.number(),
23+
})
24+
25+
exportinterfaceTemplateSettingsForm{
26+
template:Template
27+
onSubmit:(data:UpdateTemplateMeta)=>void
28+
onCancel:()=>void
29+
isSubmitting:boolean
30+
error?:unknown
31+
// Helpful to show field errors on Storybook
32+
initialTouched?:FormikTouched<UpdateTemplateMeta>
33+
}
34+
35+
exportconstTemplateSettingsForm:FC<TemplateSettingsForm>=({
36+
template,
37+
onSubmit,
38+
onCancel,
39+
error,
40+
isSubmitting,
41+
initialTouched,
42+
})=>{
43+
constform:FormikContextType<UpdateTemplateMeta>=useFormik<UpdateTemplateMeta>({
44+
initialValues:{
45+
name:template.name,
46+
description:template.description,
47+
max_ttl_ms:template.max_ttl_ms,
48+
},
49+
validationSchema,
50+
onSubmit:(data)=>{
51+
onSubmit(data)
52+
},
53+
initialTouched,
54+
})
55+
constgetFieldHelpers=getFormHelpersWithError<UpdateTemplateMeta>(form,error)
56+
57+
return(
58+
<formonSubmit={form.handleSubmit}aria-label={Language.formAriaLabel}>
59+
<Stack>
60+
<TextField
61+
{...getFieldHelpers("name")}
62+
disabled={isSubmitting}
63+
onChange={onChangeTrimmed(form)}
64+
autoFocus
65+
fullWidth
66+
label={Language.nameLabel}
67+
variant="outlined"
68+
/>
69+
70+
<TextField
71+
{...getFieldHelpers("description")}
72+
multiline
73+
disabled={isSubmitting}
74+
fullWidth
75+
label={Language.descriptionLabel}
76+
variant="outlined"
77+
rows={2}
78+
/>
79+
80+
<TextField
81+
{...getFieldHelpers("max_ttl_ms")}
82+
helperText={Language.maxTtlHelperText}
83+
disabled={isSubmitting}
84+
fullWidth
85+
inputProps={{min:0,step:1}}
86+
label={Language.maxTtlLabel}
87+
variant="outlined"
88+
/>
89+
</Stack>
90+
91+
<FormFooteronCancel={onCancel}isLoading={isSubmitting}/>
92+
</form>
93+
)
94+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import{screen,waitFor}from"@testing-library/react"
2+
importuserEventfrom"@testing-library/user-event"
3+
import*asAPIfrom"api/api"
4+
import{UpdateTemplateMeta}from"api/typesGenerated"
5+
import{LanguageasFooterFormLanguage}from"components/FormFooter/FormFooter"
6+
import{MockTemplate}from"../../testHelpers/entities"
7+
import{renderWithAuth}from"../../testHelpers/renderHelpers"
8+
import{LanguageasFormLanguage}from"./TemplateSettingsForm"
9+
import{TemplateSettingsPage}from"./TemplateSettingsPage"
10+
import{LanguageasViewLanguage}from"./TemplateSettingsPageView"
11+
12+
constrenderTemplateSettingsPage=async()=>{
13+
constrenderResult=renderWithAuth(<TemplateSettingsPage/>,{
14+
route:`/templates/${MockTemplate.name}/settings`,
15+
path:`/templates/:templateId/settings`,
16+
})
17+
// Wait the form to be rendered
18+
awaitscreen.findAllByLabelText(FormLanguage.nameLabel)
19+
returnrenderResult
20+
}
21+
22+
constfillAndSubmitForm=async({
23+
name,
24+
description,
25+
max_ttl_ms,
26+
}:Omit<Required<UpdateTemplateMeta>,"min_autostart_interval_ms">)=>{
27+
constnameField=awaitscreen.findByLabelText(FormLanguage.nameLabel)
28+
awaituserEvent.clear(nameField)
29+
awaituserEvent.type(nameField,name)
30+
31+
constdescriptionField=awaitscreen.findByLabelText(FormLanguage.descriptionLabel)
32+
awaituserEvent.clear(descriptionField)
33+
awaituserEvent.type(descriptionField,description)
34+
35+
constmaxTtlField=awaitscreen.findByLabelText(FormLanguage.maxTtlLabel)
36+
awaituserEvent.clear(maxTtlField)
37+
awaituserEvent.type(maxTtlField,max_ttl_ms.toString())
38+
39+
constsubmitButton=awaitscreen.findByText(FooterFormLanguage.defaultSubmitLabel)
40+
awaituserEvent.click(submitButton)
41+
}
42+
43+
describe("TemplateSettingsPage",()=>{
44+
it("renders",async()=>{
45+
awaitrenderTemplateSettingsPage()
46+
constelement=awaitscreen.findByText(ViewLanguage.title)
47+
expect(element).toBeDefined()
48+
})
49+
50+
it("succeeds",async()=>{
51+
awaitrenderTemplateSettingsPage()
52+
53+
constnewTemplateSettings={
54+
name:"edited-template-name",
55+
description:"Edited description",
56+
max_ttl_ms:4000,
57+
}
58+
jest.spyOn(API,"updateTemplateMeta").mockResolvedValueOnce({
59+
...MockTemplate,
60+
...newTemplateSettings,
61+
})
62+
awaitfillAndSubmitForm(newTemplateSettings)
63+
64+
awaitwaitFor(()=>expect(API.updateTemplateMeta).toBeCalledTimes(1))
65+
})
66+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import{useMachine}from"@xstate/react"
2+
import{useOrganizationId}from"hooks/useOrganizationId"
3+
import{FC}from"react"
4+
import{Helmet}from"react-helmet"
5+
import{useNavigate,useParams}from"react-router-dom"
6+
import{pageTitle}from"util/page"
7+
import{templateSettingsMachine}from"xServices/templateSettings/templateSettingsXService"
8+
import{TemplateSettingsPageView}from"./TemplateSettingsPageView"
9+
10+
constLanguage={
11+
title:"Template Settings",
12+
}
13+
14+
exportconstTemplateSettingsPage:FC=()=>{
15+
const{template:templateName}=useParams()as{template:string}
16+
constnavigate=useNavigate()
17+
constorganizationId=useOrganizationId()
18+
const[state,send]=useMachine(templateSettingsMachine,{
19+
context:{ templateName, organizationId},
20+
actions:{
21+
onSave:(_,{ data})=>{
22+
// Use the data.name because the template name can be changed
23+
navigate(`/templates/${data.name}`)
24+
},
25+
},
26+
})
27+
const{templateSettings:template, saveTemplateSettingsError, getTemplateError}=state.context
28+
29+
return(
30+
<>
31+
<Helmet>
32+
<title>{pageTitle(Language.title)}</title>
33+
</Helmet>
34+
<TemplateSettingsPageView
35+
isSubmitting={state.hasTag("submitting")}
36+
template={template}
37+
errors={{
38+
getTemplateError,
39+
saveTemplateSettingsError,
40+
}}
41+
onCancel={()=>{
42+
navigate(`/templates/${templateName}`)
43+
}}
44+
onSubmit={(templateSettings)=>{
45+
send({type:"SAVE", templateSettings})
46+
}}
47+
/>
48+
</>
49+
)
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import{action}from"@storybook/addon-actions"
2+
import{Story}from"@storybook/react"
3+
import*asMocksfrom"../../testHelpers/renderHelpers"
4+
import{makeMockApiError}from"../../testHelpers/renderHelpers"
5+
import{TemplateSettingsPageView,TemplateSettingsPageViewProps}from"./TemplateSettingsPageView"
6+
7+
exportdefault{
8+
title:"pages/TemplateSettingsPageView",
9+
component:TemplateSettingsPageView,
10+
}
11+
12+
constTemplate:Story<TemplateSettingsPageViewProps>=(args)=>(
13+
<TemplateSettingsPageView{...args}/>
14+
)
15+
16+
exportconstExample=Template.bind({})
17+
Example.args={
18+
template:Mocks.MockTemplate,
19+
onSubmit:action("onSubmit"),
20+
onCancel:action("cancel"),
21+
}
22+
23+
exportconstGetTemplateError=Template.bind({})
24+
GetTemplateError.args={
25+
template:undefined,
26+
errors:{
27+
getTemplateError:makeMockApiError({
28+
message:"Failed to fetch the template.",
29+
detail:"You do not have permission to access this resource.",
30+
}),
31+
},
32+
onSubmit:action("onSubmit"),
33+
onCancel:action("cancel"),
34+
}
35+
36+
exportconstSaveTemplateSettingsError=Template.bind({})
37+
SaveTemplateSettingsError.args={
38+
template:Mocks.MockTemplate,
39+
errors:{
40+
saveTemplateSettingsError:makeMockApiError({
41+
message:'Template "test" already exists.',
42+
validations:[
43+
{
44+
field:"name",
45+
detail:"This value is already in use and should be unique.",
46+
},
47+
],
48+
}),
49+
},
50+
initialTouched:{
51+
name:true,
52+
},
53+
onSubmit:action("onSubmit"),
54+
onCancel:action("cancel"),
55+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp