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

Commit4a17e0d

Browse files
feat: Add setup page (#3476)
1 parent604f211 commit4a17e0d

File tree

15 files changed

+624
-124
lines changed

15 files changed

+624
-124
lines changed

‎site/e2e/globalSetup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
importaxiosfrom"axios"
2-
import{postFirstUser}from"../src/api/api"
2+
import{createFirstUser}from"../src/api/api"
33
import*asconstantsfrom"./constants"
44

55
constglobalSetup=async():Promise<void>=>{
66
axios.defaults.baseURL=`http://localhost:${constants.basePort}`
7-
awaitpostFirstUser({
7+
awaitcreateFirstUser({
88
email:constants.email,
99
organization:constants.organization,
1010
username:constants.username,

‎site/src/AppRouter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import{useSelector}from"@xstate/react"
2+
import{SetupPage}from"pages/SetupPage/SetupPage"
23
import{FC,lazy,Suspense,useContext}from"react"
34
import{Navigate,Route,Routes}from"react-router-dom"
45
import{selectPermissions}from"xServices/auth/authSelectors"
@@ -47,6 +48,7 @@ export const AppRouter: FC = () => {
4748
/>
4849

4950
<Routepath="login"element={<LoginPage/>}/>
51+
<Routepath="setup"element={<SetupPage/>}/>
5052
<Routepath="healthz"element={<HealthzPage/>}/>
5153
<Route
5254
path="cli-auth"

‎site/src/api/api.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,24 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
282282
returnresponse.data
283283
}
284284

285-
exportconstpostFirstUser=async(
285+
// API definition:
286+
// https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53
287+
exportconsthasFirstUser=async():Promise<boolean>=>{
288+
try{
289+
// If it is success, it is true
290+
awaitaxios.get("/api/v2/users/first")
291+
returntrue
292+
}catch(error){
293+
// If it returns a 404, it is false
294+
if(axios.isAxiosError(error)&&error.response?.status===404){
295+
returnfalse
296+
}
297+
298+
throwerror
299+
}
300+
}
301+
302+
exportconstcreateFirstUser=async(
286303
req:TypesGen.CreateFirstUserRequest,
287304
):Promise<TypesGen.CreateFirstUserResponse>=>{
288305
constresponse=awaitaxios.post(`/api/v2/users/first`,req)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import{makeStyles}from"@material-ui/core/styles"
2+
import{FC}from"react"
3+
import{Footer}from"../../components/Footer/Footer"
4+
5+
exportconstuseStyles=makeStyles((theme)=>({
6+
root:{
7+
height:"100vh",
8+
display:"flex",
9+
justifyContent:"center",
10+
alignItems:"center",
11+
},
12+
layout:{
13+
display:"flex",
14+
flexDirection:"column",
15+
alignItems:"center",
16+
},
17+
container:{
18+
marginTop:theme.spacing(-8),
19+
minWidth:"320px",
20+
maxWidth:"320px",
21+
},
22+
}))
23+
24+
exportconstSignInLayout:FC=({ children})=>{
25+
conststyles=useStyles()
26+
27+
return(
28+
<divclassName={styles.root}>
29+
<divclassName={styles.layout}>
30+
<divclassName={styles.container}>{children}</div>
31+
<Footer/>
32+
</div>
33+
</div>
34+
)
35+
}

‎site/src/components/Welcome/Welcome.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@ import Typography from "@material-ui/core/Typography"
33
import{FC}from"react"
44
import{CoderIcon}from"../Icons/CoderIcon"
55

6-
exportconstWelcome:FC=()=>{
6+
constLanguage={
7+
defaultMessage:(
8+
<>
9+
Welcome to<strong>Coder</strong>
10+
</>
11+
),
12+
}
13+
14+
exportconstWelcome:FC<{message?:JSX.Element}>=({ message=Language.defaultMessage})=>{
715
conststyles=useStyles()
816

917
return(
@@ -12,7 +20,7 @@ export const Welcome: FC = () => {
1220
<CoderIconclassName={styles.logo}/>
1321
</div>
1422
<TypographyclassName={styles.title}variant="h1">
15-
Welcome to<strong>Coder</strong>
23+
{message}
1624
</Typography>
1725
</div>
1826
)

‎site/src/pages/LoginPage/LoginPage.test.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import{act,screen}from"@testing-library/react"
1+
import{act,screen,waitFor}from"@testing-library/react"
22
importuserEventfrom"@testing-library/user-event"
33
import{rest}from"msw"
44
import{Language}from"../../components/SignInForm/SignInForm"
@@ -89,4 +89,19 @@ describe("LoginPage", () => {
8989
awaitscreen.findByText(Language.passwordSignIn)
9090
awaitscreen.findByText(Language.githubSignIn)
9191
})
92+
93+
it("redirects to the setup page if there is no first user",async()=>{
94+
// Given
95+
server.use(
96+
rest.get("/api/v2/users/first",async(req,res,ctx)=>{
97+
returnres(ctx.status(404))
98+
}),
99+
)
100+
101+
// When
102+
render(<LoginPage/>)
103+
104+
// Then
105+
awaitwaitFor(()=>expect(history.location.pathname).toEqual("/setup"))
106+
})
92107
})
Lines changed: 18 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,54 @@
1-
import{makeStyles}from"@material-ui/core/styles"
21
import{useActor}from"@xstate/react"
2+
import{SignInLayout}from"components/SignInLayout/SignInLayout"
33
importReact,{useContext}from"react"
44
import{Helmet}from"react-helmet"
55
import{Navigate,useLocation}from"react-router-dom"
6-
import{Footer}from"../../components/Footer/Footer"
76
import{LoginErrors,SignInForm}from"../../components/SignInForm/SignInForm"
87
import{pageTitle}from"../../util/page"
98
import{retrieveRedirect}from"../../util/redirect"
109
import{XServiceContext}from"../../xServices/StateContext"
1110

12-
exportconstuseStyles=makeStyles((theme)=>({
13-
root:{
14-
height:"100vh",
15-
display:"flex",
16-
justifyContent:"center",
17-
alignItems:"center",
18-
},
19-
layout:{
20-
display:"flex",
21-
flexDirection:"column",
22-
alignItems:"center",
23-
},
24-
container:{
25-
marginTop:theme.spacing(-8),
26-
minWidth:"320px",
27-
maxWidth:"320px",
28-
},
29-
}))
30-
3111
interfaceLocationState{
3212
isRedirect:boolean
3313
}
3414

3515
exportconstLoginPage:React.FC=()=>{
36-
conststyles=useStyles()
3716
constlocation=useLocation()
3817
constxServices=useContext(XServiceContext)
3918
const[authState,authSend]=useActor(xServices.authXService)
4019
constisLoading=authState.hasTag("loading")
4120
constredirectTo=retrieveRedirect(location.search)
4221
constlocationState=location.state ?(location.stateasLocationState) :null
4322
constisRedirected=locationState ?locationState.isRedirect :false
23+
const{ authError, getUserError, checkPermissionsError, getMethodsError}=authState.context
4424

4525
constonSubmit=async({ email, password}:{email:string;password:string})=>{
4626
authSend({type:"SIGN_IN", email, password})
4727
}
4828

49-
const{ authError, getUserError, checkPermissionsError, getMethodsError}=authState.context
50-
5129
if(authState.matches("signedIn")){
5230
return<Navigateto={redirectTo}replace/>
5331
}else{
5432
return(
55-
<divclassName={styles.root}>
33+
<>
5634
<Helmet>
5735
<title>{pageTitle("Login")}</title>
5836
</Helmet>
59-
<divclassName={styles.layout}>
60-
<divclassName={styles.container}>
61-
<SignInForm
62-
authMethods={authState.context.methods}
63-
redirectTo={redirectTo}
64-
isLoading={isLoading}
65-
loginErrors={{
66-
[LoginErrors.AUTH_ERROR]:authError,
67-
[LoginErrors.GET_USER_ERROR]:isRedirected ?getUserError :null,
68-
[LoginErrors.CHECK_PERMISSIONS_ERROR]:checkPermissionsError,
69-
[LoginErrors.GET_METHODS_ERROR]:getMethodsError,
70-
}}
71-
onSubmit={onSubmit}
72-
/>
73-
</div>
74-
75-
<Footer/>
76-
</div>
77-
</div>
37+
<SignInLayout>
38+
<SignInForm
39+
authMethods={authState.context.methods}
40+
redirectTo={redirectTo}
41+
isLoading={isLoading}
42+
loginErrors={{
43+
[LoginErrors.AUTH_ERROR]:authError,
44+
[LoginErrors.GET_USER_ERROR]:isRedirected ?getUserError :null,
45+
[LoginErrors.CHECK_PERMISSIONS_ERROR]:checkPermissionsError,
46+
[LoginErrors.GET_METHODS_ERROR]:getMethodsError,
47+
}}
48+
onSubmit={onSubmit}
49+
/>
50+
</SignInLayout>
51+
</>
7852
)
7953
}
8054
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import{screen,waitFor}from"@testing-library/react"
2+
importuserEventfrom"@testing-library/user-event"
3+
import*asAPIfrom"api/api"
4+
import{rest}from"msw"
5+
import{history,MockUser,render}from"testHelpers/renderHelpers"
6+
import{server}from"testHelpers/server"
7+
import{LanguageasSetupLanguage}from"xServices/setup/setupXService"
8+
import{SetupPage}from"./SetupPage"
9+
import{LanguageasPageViewLanguage}from"./SetupPageView"
10+
11+
constfillForm=async({
12+
username="someuser",
13+
email="someone@coder.com",
14+
password="password",
15+
organization="Coder",
16+
}:{
17+
username?:string
18+
email?:string
19+
password?:string
20+
organization?:string
21+
}={})=>{
22+
constusernameField=screen.getByLabelText(PageViewLanguage.usernameLabel)
23+
constemailField=screen.getByLabelText(PageViewLanguage.emailLabel)
24+
constpasswordField=screen.getByLabelText(PageViewLanguage.passwordLabel)
25+
constorganizationField=screen.getByLabelText(PageViewLanguage.organizationLabel)
26+
awaituserEvent.type(organizationField,organization)
27+
awaituserEvent.type(usernameField,username)
28+
awaituserEvent.type(emailField,email)
29+
awaituserEvent.type(passwordField,password)
30+
constsubmitButton=screen.getByRole("button",{name:PageViewLanguage.create})
31+
submitButton.click()
32+
}
33+
34+
describe("Setup Page",()=>{
35+
beforeEach(()=>{
36+
history.replace("/setup")
37+
// appear logged out
38+
server.use(
39+
rest.get("/api/v2/users/me",(req,res,ctx)=>{
40+
returnres(ctx.status(401),ctx.json({message:"no user here"}))
41+
}),
42+
)
43+
})
44+
45+
it("shows validation error message",async()=>{
46+
render(<SetupPage/>)
47+
awaitfillForm({email:"test"})
48+
consterrorMessage=awaitscreen.findByText(PageViewLanguage.emailInvalid)
49+
expect(errorMessage).toBeDefined()
50+
})
51+
52+
it("shows generic error message",async()=>{
53+
jest.spyOn(API,"createFirstUser").mockRejectedValueOnce({
54+
data:"unknown error",
55+
})
56+
render(<SetupPage/>)
57+
awaitfillForm()
58+
consterrorMessage=awaitscreen.findByText(SetupLanguage.createFirstUserError)
59+
expect(errorMessage).toBeDefined()
60+
})
61+
62+
it("shows API error message",async()=>{
63+
constfieldErrorMessage="invalid username"
64+
server.use(
65+
rest.post("/api/v2/users/first",async(req,res,ctx)=>{
66+
returnres(
67+
ctx.status(400),
68+
ctx.json({
69+
message:"invalid field",
70+
validations:[
71+
{
72+
detail:fieldErrorMessage,
73+
field:"username",
74+
},
75+
],
76+
}),
77+
)
78+
}),
79+
)
80+
render(<SetupPage/>)
81+
awaitfillForm()
82+
consterrorMessage=awaitscreen.findByText(fieldErrorMessage)
83+
expect(errorMessage).toBeDefined()
84+
})
85+
86+
it("redirects to workspaces page when success",async()=>{
87+
render(<SetupPage/>)
88+
89+
// simulates the user will be authenticated
90+
server.use(
91+
rest.get("/api/v2/users/me",(req,res,ctx)=>{
92+
returnres(ctx.status(200),ctx.json(MockUser))
93+
}),
94+
)
95+
96+
awaitfillForm()
97+
awaitwaitFor(()=>expect(history.location.pathname).toEqual("/workspaces"))
98+
})
99+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import{useActor,useMachine}from"@xstate/react"
2+
import{FC,useContext,useEffect}from"react"
3+
import{Helmet}from"react-helmet"
4+
import{useNavigate}from"react-router-dom"
5+
import{pageTitle}from"util/page"
6+
import{setupMachine}from"xServices/setup/setupXService"
7+
import{XServiceContext}from"xServices/StateContext"
8+
import{SetupPageView}from"./SetupPageView"
9+
10+
exportconstSetupPage:FC=()=>{
11+
constnavigate=useNavigate()
12+
constxServices=useContext(XServiceContext)
13+
const[authState,authSend]=useActor(xServices.authXService)
14+
const[setupState,setupSend]=useMachine(setupMachine,{
15+
actions:{
16+
onCreateFirstUser:({ firstUser})=>{
17+
if(!firstUser){
18+
thrownewError("First user was not defined.")
19+
}
20+
authSend({type:"SIGN_IN",email:firstUser.email,password:firstUser.password})
21+
},
22+
},
23+
})
24+
const{ createFirstUserFormErrors, createFirstUserErrorMessage}=setupState.context
25+
26+
useEffect(()=>{
27+
if(authState.matches("signedIn")){
28+
returnnavigate("/workspaces")
29+
}
30+
},[authState,navigate])
31+
32+
return(
33+
<>
34+
<Helmet>
35+
<title>{pageTitle("Set up your account")}</title>
36+
</Helmet>
37+
<SetupPageView
38+
isLoading={setupState.hasTag("loading")}
39+
formErrors={createFirstUserFormErrors}
40+
genericError={createFirstUserErrorMessage}
41+
onSubmit={(firstUser)=>{
42+
setupSend({type:"CREATE_FIRST_USER", firstUser})
43+
}}
44+
/>
45+
</>
46+
)
47+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp