Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 22f820c

Browse files
refactor(site): replace UserContext with userXService (#465)
* Install and configure XState * userXService - typegen not working yet * Lint, fix error transitions * Lint * Change initial state to handle loss of state * Fix gitignore * Fix types by hook or by crook * Use xservice in all pages * Glue/visual component separation * Fix dependency merge * Lint * Remove UserContext * Remove inspector * Add typegen command to site/out * Fix index page redirects * DRY up nav and redirects * Moves based on merge * Moving Page helpers into Page dir * Move xservice into src, update script * Move and storybook navbarview * Update docs * Install MSW * Reorganization, with apologies * Missed spots * Add mock handlers * Configure jest for msw * Fix typos * Shift unit test to NavbarView * Fix test types * Rename NavbarView test * Attempt at test, wip * Fix config * Be logged out, only warn * Conditionally show text to help test * Use a Context for MSW's sake * mocks -> test_helpers * Enable dev tools * Format * Fix import * Fixes * Lint * run typegen postinstall Co-authored-by: Bryan Phelps <[email protected]>
1 parent 8fde3ed commit 22f820c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1028
-512
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ site/storybook-static/
2323
site/test-results/
2424
site/yarn-error.log
2525
coverage/
26+
site/**/*.typegen.ts
2627

2728
# Build
2829
dist/

Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto
8484

8585
site/out:
8686
./scripts/yarn_install.sh
87+
cd site && yarn typegen
8788
cd site && yarn build
8889
# Restores GITKEEP files!
8990
git checkout HEAD site/out

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ To manually run the server and go through first-time set up, run the following c
4848

4949
You'll now be able to login and access the server.
5050

51-
To create a project, run:
5251
- `dist/coder_linux_amd64/coder projects create -d /path/to/project`
5352

5453
### Development
@@ -63,6 +62,10 @@ The `develop.sh` script does three things:
6362

6463
This is the recommend flow for working on the front-end, as hot-reload is set up as part of the webpack config.
6564

65+
Note that `./develop.sh` creates a user and allows you to log into the UI, but does not log you into the CLI, which is required for creating a project. Use the `login` command above before the `projects create` command.
66+
67+
While we're working on automating XState typegen, you may need to run `yarn typegen` from `site`.
68+
6669
## Front-End Plan
6770

6871
For the front-end team, we're planning on 2 phases to the 'v2' work:

site/.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ coverage
88
.next
99
storybook-static
1010
test-results
11+
**/*.typegen.ts

site/jest.setup.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
import { server } from "./src/test_helpers/server"
2+
3+
// Establish API mocking before all tests through MSW.
4+
beforeAll(() =>
5+
server.listen({
6+
onUnhandledRequest: "warn",
7+
}),
8+
)
9+
10+
// Reset any request handlers that we may add during the tests,
11+
// so they don't affect other tests.
12+
afterEach(() => server.resetHandlers())
13+
14+
// Clean up after the tests are finished.
15+
afterAll(() => server.close())
16+
117
// Helper utility to fail jest tests if a console.error is logged
218
// Pulled from this blog post:
319
// https://www.benmvp.com/blog/catch-warnings-jest-tests/

site/package.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"repository": "https://github.com/coder/coder",
55
"private": true,
66
"scripts": {
7+
"postinstall": "yarn typegen",
78
"build": "NODE_ENV=production webpack build --config=webpack.prod.ts",
89
"build:analyze": "NODE_ENV=production webpack --profile --progress --json --config=webpack.prod.ts > out/stats.json && webpack-bundle-analyzer out/stats.json out",
910
"dev": "webpack-dev-server --config=webpack.dev.ts",
@@ -18,19 +19,22 @@
1819
"storybook:build": "build-storybook",
1920
"test": "jest --selectProjects test",
2021
"test:coverage": "jest --selectProjects test --collectCoverage",
21-
"test:watch": "jest --selectProjects test --watch"
22+
"test:watch": "jest --selectProjects test --watch",
23+
"typegen": "xstate typegen 'src/**/*.ts'"
2224
},
2325
"dependencies": {
2426
"@material-ui/core": "4.9.4",
2527
"@material-ui/icons": "4.5.1",
2628
"@material-ui/lab": "4.0.0-alpha.42",
29+
"@xstate/react": "^2.0.1",
2730
"axios": "0.26.1",
2831
"formik": "2.2.9",
2932
"history": "5.3.0",
3033
"react": "17.0.2",
3134
"react-dom": "17.0.2",
3235
"react-router-dom": "6.2.2",
3336
"swr": "1.2.2",
37+
"xstate": "^4.30.6",
3438
"yup": "0.32.11"
3539
},
3640
"devDependencies": {
@@ -43,13 +47,14 @@
4347
"@storybook/react": "6.4.19",
4448
"@testing-library/react": "12.1.4",
4549
"@types/express": "4.17.13",
46-
"@types/jest": "27.4.1",
50+
"@types/jest": "^27.4.1",
4751
"@types/node": "14.18.12",
4852
"@types/react": "17.0.40",
4953
"@types/react-dom": "17.0.13",
5054
"@types/superagent": "4.1.15",
5155
"@typescript-eslint/eslint-plugin": "5.15.0",
5256
"@typescript-eslint/parser": "5.15.0",
57+
"@xstate/cli": "^0.1.4",
5358
"copy-webpack-plugin": "10.2.4",
5459
"eslint": "8.11.0",
5560
"eslint-config-prettier": "8.5.0",
@@ -66,6 +71,7 @@
6671
"jest": "27.5.1",
6772
"jest-junit": "13.0.0",
6873
"jest-runner-eslint": "1.0.0",
74+
"msw": "^0.39.2",
6975
"prettier": "2.6.0",
7076
"react-hot-loader": "4.13.0",
7177
"sql-formatter": "4.0.2",

site/src/api.test.ts renamed to site/src/api/index.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from "axios"
2-
import { APIKeyResponse, getApiKey, login, LoginResponse, logout } from "./api"
2+
import { getApiKey, login, logout } from "."
3+
import { LoginResponse, APIKeyResponse } from "./types"
34

45
// Mock the axios module so that no real network requests are made, but rather
56
// we swap in a resolved or rejected value
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,12 @@
11
import axios, { AxiosRequestHeaders } from "axios"
22
import { mutate } from "swr"
3+
import * as Types from "./types"
34

45
const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
56
"Content-Type": "application/json",
67
}
78

8-
/**
9-
* `Organization` must be kept in sync with the go struct in organizations.go
10-
*/
11-
export interface Organization {
12-
id: string
13-
name: string
14-
created_at: string
15-
updated_at: string
16-
}
17-
18-
export interface Provisioner {
19-
id: string
20-
name: string
21-
}
22-
23-
export const provisioners: Provisioner[] = [
9+
export const provisioners: Types.Provisioner[] = [
2410
{
2511
id: "terraform",
2612
name: "Terraform",
@@ -31,25 +17,8 @@ export const provisioners: Provisioner[] = [
3117
},
3218
]
3319

34-
// This must be kept in sync with the `Project` struct in the back-end
35-
export interface Project {
36-
id: string
37-
created_at: string
38-
updated_at: string
39-
organization_id: string
40-
name: string
41-
provisioner: string
42-
active_version_id: string
43-
}
44-
45-
export interface CreateProjectRequest {
46-
name: string
47-
organizationId: string
48-
provisioner: string
49-
}
50-
5120
export namespace Project {
52-
export const create = async (request: CreateProjectRequest): Promise<Project> => {
21+
export const create = async (request: Types.CreateProjectRequest): Promise<Types.Project> => {
5322
const response = await fetch(`/api/v2/projects/${request.organizationId}/`, {
5423
method: "POST",
5524
headers: {
@@ -68,23 +37,8 @@ export namespace Project {
6837
}
6938
}
7039

71-
export interface CreateWorkspaceRequest {
72-
name: string
73-
project_id: string
74-
}
75-
76-
// Must be kept in sync with backend Workspace struct
77-
export interface Workspace {
78-
id: string
79-
created_at: string
80-
updated_at: string
81-
owner_id: string
82-
project_id: string
83-
name: string
84-
}
85-
8640
export namespace Workspace {
87-
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
41+
export const create = async (request: Types.CreateWorkspaceRequest): Promise<Types.Workspace> => {
8842
const response = await fetch(`/api/v2/users/me/workspaces`, {
8943
method: "POST",
9044
headers: {
@@ -108,17 +62,13 @@ export namespace Workspace {
10862
}
10963
}
11064

111-
export interface LoginResponse {
112-
session_token: string
113-
}
114-
115-
export const login = async (email: string, password: string): Promise<LoginResponse> => {
65+
export const login = async (email: string, password: string): Promise<Types.LoginResponse> => {
11666
const payload = JSON.stringify({
11767
email,
11868
password,
11969
})
12070

121-
const response = await axios.post<LoginResponse>("/api/v2/users/login", payload, {
71+
const response = await axios.post<Types.LoginResponse>("/api/v2/users/login", payload, {
12272
headers: { ...CONTENT_TYPE_JSON },
12373
})
12474

@@ -129,11 +79,12 @@ export const logout = async (): Promise<void> => {
12979
await axios.post("/api/v2/users/logout")
13080
}
13181

132-
export interface APIKeyResponse {
133-
key: string
82+
export const getUser = async (): Promise<Types.UserResponse> => {
83+
const response = await axios.get<Types.UserResponse>("/api/v2/users/me")
84+
return response.data
13485
}
13586

136-
export const getApiKey = async (): Promise<APIKeyResponse> => {
137-
const response = await axios.post<APIKeyResponse>("/api/v2/users/me/keys")
87+
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
88+
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
13889
return response.data
13990
}

site/src/api/types.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export interface LoginResponse {
2+
session_token: string
3+
}
4+
5+
export interface UserResponse {
6+
readonly id: string
7+
readonly username: string
8+
readonly email: string
9+
readonly created_at: string
10+
}
11+
12+
/**
13+
* `Organization` must be kept in sync with the go struct in organizations.go
14+
*/
15+
export interface Organization {
16+
id: string
17+
name: string
18+
created_at: string
19+
updated_at: string
20+
}
21+
22+
export interface Provisioner {
23+
id: string
24+
name: string
25+
}
26+
27+
// This must be kept in sync with the `Project` struct in the back-end
28+
export interface Project {
29+
id: string
30+
created_at: string
31+
updated_at: string
32+
organization_id: string
33+
name: string
34+
provisioner: string
35+
active_version_id: string
36+
}
37+
38+
export interface CreateProjectRequest {
39+
name: string
40+
organizationId: string
41+
provisioner: string
42+
}
43+
44+
export interface CreateWorkspaceRequest {
45+
name: string
46+
project_id: string
47+
}
48+
49+
// Must be kept in sync with backend Workspace struct
50+
export interface Workspace {
51+
id: string
52+
created_at: string
53+
updated_at: string
54+
owner_id: string
55+
project_id: string
56+
name: string
57+
}
58+
59+
export interface APIKeyResponse {
60+
key: string
61+
}

site/src/app.tsx

+44-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React from "react"
22
import CssBaseline from "@material-ui/core/CssBaseline"
33
import ThemeProvider from "@material-ui/styles/ThemeProvider"
44
import { SWRConfig } from "swr"
5-
import { UserProvider } from "./contexts/UserContext"
65
import { light } from "./theme"
76
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
87

@@ -15,6 +14,8 @@ import { ProjectPage } from "./pages/projects/[organization]/[project]"
1514
import { CreateWorkspacePage } from "./pages/projects/[organization]/[project]/create"
1615
import { WorkspacePage } from "./pages/workspaces/[workspace]"
1716
import { HealthzPage } from "./pages/healthz"
17+
import { AuthAndNav, RequireAuth } from "./components/Page"
18+
import { XServiceProvider } from "./xServices/StateContext"
1819

1920
export const App: React.FC = () => {
2021
return (
@@ -37,28 +38,63 @@ export const App: React.FC = () => {
3738
},
3839
}}
3940
>
40-
<UserProvider>
41+
<XServiceProvider>
4142
<ThemeProvider theme={light}>
4243
<CssBaseline />
4344

4445
<Routes>
4546
<Route path="/">
46-
<Route index element={<IndexPage />} />
47+
<Route
48+
index
49+
element={
50+
<RequireAuth>
51+
<IndexPage />
52+
</RequireAuth>
53+
}
54+
/>
4755

4856
<Route path="login" element={<SignInPage />} />
4957
<Route path="healthz" element={<HealthzPage />} />
5058
<Route path="cli-auth" element={<CliAuthenticationPage />} />
5159

5260
<Route path="projects">
53-
<Route index element={<ProjectsPage />} />
61+
<Route
62+
index
63+
element={
64+
<AuthAndNav>
65+
<ProjectsPage />
66+
</AuthAndNav>
67+
}
68+
/>
5469
<Route path=":organization/:project">
55-
<Route index element={<ProjectPage />} />
56-
<Route path="create" element={<CreateWorkspacePage />} />
70+
<Route
71+
index
72+
element={
73+
<AuthAndNav>
74+
<ProjectPage />
75+
</AuthAndNav>
76+
}
77+
/>
78+
<Route
79+
path="create"
80+
element={
81+
<RequireAuth>
82+
<CreateWorkspacePage />
83+
</RequireAuth>
84+
}
85+
/>
5786
</Route>
5887
</Route>
5988

6089
<Route path="workspaces">
61-
<Route path=":workspace" element={<WorkspacePage />} />
90+
<Route
91+
path=":workspace"
92+
element={
93+
<AuthAndNav>
94+
<WorkspacePage />
95+
</AuthAndNav>
96+
}
97+
/>
6298
</Route>
6399

64100
{/* Using path="*"" means "match anything", so this route
@@ -68,7 +104,7 @@ export const App: React.FC = () => {
68104
</Route>
69105
</Routes>
70106
</ThemeProvider>
71-
</UserProvider>
107+
</XServiceProvider>
72108
</SWRConfig>
73109
</Router>
74110
)

0 commit comments

Comments
 (0)