Skip to content

Commit

Permalink
refactor(site): replace UserContext with userXService (#465)
Browse files Browse the repository at this point in the history
* 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 <bryan@coder.com>
  • Loading branch information
presleyp and bryphe-coder committed Mar 18, 2022
1 parent 8fde3ed commit 22f820c
Show file tree
Hide file tree
Showing 43 changed files with 1,028 additions and 512 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ site/storybook-static/
site/test-results/
site/yarn-error.log
coverage/
site/**/*.typegen.ts

# Build
dist/
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ provisionersdk/proto: provisionersdk/proto/provisioner.proto

site/out:
./scripts/yarn_install.sh
cd site && yarn typegen
cd site && yarn build
# Restores GITKEEP files!
git checkout HEAD site/out
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ To manually run the server and go through first-time set up, run the following c

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

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

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

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

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.

While we're working on automating XState typegen, you may need to run `yarn typegen` from `site`.

## Front-End Plan

For the front-end team, we're planning on 2 phases to the 'v2' work:
Expand Down
1 change: 1 addition & 0 deletions site/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ coverage
.next
storybook-static
test-results
**/*.typegen.ts
16 changes: 16 additions & 0 deletions site/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
import { server } from "./src/test_helpers/server"

// Establish API mocking before all tests through MSW.
beforeAll(() =>
server.listen({
onUnhandledRequest: "warn",
}),
)

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())

// Clean up after the tests are finished.
afterAll(() => server.close())

// Helper utility to fail jest tests if a console.error is logged
// Pulled from this blog post:
// https://www.benmvp.com/blog/catch-warnings-jest-tests/
Expand Down
10 changes: 8 additions & 2 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"repository": "https://github.com/coder/coder",
"private": true,
"scripts": {
"postinstall": "yarn typegen",
"build": "NODE_ENV=production webpack build --config=webpack.prod.ts",
"build:analyze": "NODE_ENV=production webpack --profile --progress --json --config=webpack.prod.ts > out/stats.json && webpack-bundle-analyzer out/stats.json out",
"dev": "webpack-dev-server --config=webpack.dev.ts",
Expand All @@ -18,19 +19,22 @@
"storybook:build": "build-storybook",
"test": "jest --selectProjects test",
"test:coverage": "jest --selectProjects test --collectCoverage",
"test:watch": "jest --selectProjects test --watch"
"test:watch": "jest --selectProjects test --watch",
"typegen": "xstate typegen 'src/**/*.ts'"
},
"dependencies": {
"@material-ui/core": "4.9.4",
"@material-ui/icons": "4.5.1",
"@material-ui/lab": "4.0.0-alpha.42",
"@xstate/react": "^2.0.1",
"axios": "0.26.1",
"formik": "2.2.9",
"history": "5.3.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-router-dom": "6.2.2",
"swr": "1.2.2",
"xstate": "^4.30.6",
"yup": "0.32.11"
},
"devDependencies": {
Expand All @@ -43,13 +47,14 @@
"@storybook/react": "6.4.19",
"@testing-library/react": "12.1.4",
"@types/express": "4.17.13",
"@types/jest": "27.4.1",
"@types/jest": "^27.4.1",
"@types/node": "14.18.12",
"@types/react": "17.0.40",
"@types/react-dom": "17.0.13",
"@types/superagent": "4.1.15",
"@typescript-eslint/eslint-plugin": "5.15.0",
"@typescript-eslint/parser": "5.15.0",
"@xstate/cli": "^0.1.4",
"copy-webpack-plugin": "10.2.4",
"eslint": "8.11.0",
"eslint-config-prettier": "8.5.0",
Expand All @@ -66,6 +71,7 @@
"jest": "27.5.1",
"jest-junit": "13.0.0",
"jest-runner-eslint": "1.0.0",
"msw": "^0.39.2",
"prettier": "2.6.0",
"react-hot-loader": "4.13.0",
"sql-formatter": "4.0.2",
Expand Down
3 changes: 2 additions & 1 deletion site/src/api.test.ts → site/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from "axios"
import { APIKeyResponse, getApiKey, login, LoginResponse, logout } from "./api"
import { getApiKey, login, logout } from "."
import { LoginResponse, APIKeyResponse } from "./types"

// Mock the axios module so that no real network requests are made, but rather
// we swap in a resolved or rejected value
Expand Down
71 changes: 11 additions & 60 deletions site/src/api.ts → site/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import axios, { AxiosRequestHeaders } from "axios"
import { mutate } from "swr"
import * as Types from "./types"

const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
"Content-Type": "application/json",
}

/**
* `Organization` must be kept in sync with the go struct in organizations.go
*/
export interface Organization {
id: string
name: string
created_at: string
updated_at: string
}

export interface Provisioner {
id: string
name: string
}

export const provisioners: Provisioner[] = [
export const provisioners: Types.Provisioner[] = [
{
id: "terraform",
name: "Terraform",
Expand All @@ -31,25 +17,8 @@ export const provisioners: Provisioner[] = [
},
]

// This must be kept in sync with the `Project` struct in the back-end
export interface Project {
id: string
created_at: string
updated_at: string
organization_id: string
name: string
provisioner: string
active_version_id: string
}

export interface CreateProjectRequest {
name: string
organizationId: string
provisioner: string
}

export namespace Project {
export const create = async (request: CreateProjectRequest): Promise<Project> => {
export const create = async (request: Types.CreateProjectRequest): Promise<Types.Project> => {
const response = await fetch(`/api/v2/projects/${request.organizationId}/`, {
method: "POST",
headers: {
Expand All @@ -68,23 +37,8 @@ export namespace Project {
}
}

export interface CreateWorkspaceRequest {
name: string
project_id: string
}

// Must be kept in sync with backend Workspace struct
export interface Workspace {
id: string
created_at: string
updated_at: string
owner_id: string
project_id: string
name: string
}

export namespace Workspace {
export const create = async (request: CreateWorkspaceRequest): Promise<Workspace> => {
export const create = async (request: Types.CreateWorkspaceRequest): Promise<Types.Workspace> => {
const response = await fetch(`/api/v2/users/me/workspaces`, {
method: "POST",
headers: {
Expand All @@ -108,17 +62,13 @@ export namespace Workspace {
}
}

export interface LoginResponse {
session_token: string
}

export const login = async (email: string, password: string): Promise<LoginResponse> => {
export const login = async (email: string, password: string): Promise<Types.LoginResponse> => {
const payload = JSON.stringify({
email,
password,
})

const response = await axios.post<LoginResponse>("/api/v2/users/login", payload, {
const response = await axios.post<Types.LoginResponse>("/api/v2/users/login", payload, {
headers: { ...CONTENT_TYPE_JSON },
})

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

export interface APIKeyResponse {
key: string
export const getUser = async (): Promise<Types.UserResponse> => {
const response = await axios.get<Types.UserResponse>("/api/v2/users/me")
return response.data
}

export const getApiKey = async (): Promise<APIKeyResponse> => {
const response = await axios.post<APIKeyResponse>("/api/v2/users/me/keys")
export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
return response.data
}
61 changes: 61 additions & 0 deletions site/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export interface LoginResponse {
session_token: string
}

export interface UserResponse {
readonly id: string
readonly username: string
readonly email: string
readonly created_at: string
}

/**
* `Organization` must be kept in sync with the go struct in organizations.go
*/
export interface Organization {
id: string
name: string
created_at: string
updated_at: string
}

export interface Provisioner {
id: string
name: string
}

// This must be kept in sync with the `Project` struct in the back-end
export interface Project {
id: string
created_at: string
updated_at: string
organization_id: string
name: string
provisioner: string
active_version_id: string
}

export interface CreateProjectRequest {
name: string
organizationId: string
provisioner: string
}

export interface CreateWorkspaceRequest {
name: string
project_id: string
}

// Must be kept in sync with backend Workspace struct
export interface Workspace {
id: string
created_at: string
updated_at: string
owner_id: string
project_id: string
name: string
}

export interface APIKeyResponse {
key: string
}
52 changes: 44 additions & 8 deletions site/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react"
import CssBaseline from "@material-ui/core/CssBaseline"
import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { SWRConfig } from "swr"
import { UserProvider } from "./contexts/UserContext"
import { light } from "./theme"
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"

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

export const App: React.FC = () => {
return (
Expand All @@ -37,28 +38,63 @@ export const App: React.FC = () => {
},
}}
>
<UserProvider>
<XServiceProvider>
<ThemeProvider theme={light}>
<CssBaseline />

<Routes>
<Route path="/">
<Route index element={<IndexPage />} />
<Route
index
element={
<RequireAuth>
<IndexPage />
</RequireAuth>
}
/>

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

<Route path="projects">
<Route index element={<ProjectsPage />} />
<Route
index
element={
<AuthAndNav>
<ProjectsPage />
</AuthAndNav>
}
/>
<Route path=":organization/:project">
<Route index element={<ProjectPage />} />
<Route path="create" element={<CreateWorkspacePage />} />
<Route
index
element={
<AuthAndNav>
<ProjectPage />
</AuthAndNav>
}
/>
<Route
path="create"
element={
<RequireAuth>
<CreateWorkspacePage />
</RequireAuth>
}
/>
</Route>
</Route>

<Route path="workspaces">
<Route path=":workspace" element={<WorkspacePage />} />
<Route
path=":workspace"
element={
<AuthAndNav>
<WorkspacePage />
</AuthAndNav>
}
/>
</Route>

{/* Using path="*"" means "match anything", so this route
Expand All @@ -68,7 +104,7 @@ export const App: React.FC = () => {
</Route>
</Routes>
</ThemeProvider>
</UserProvider>
</XServiceProvider>
</SWRConfig>
</Router>
)
Expand Down

0 comments on commit 22f820c

Please sign in to comment.