Skip to content

feat(site): Read users into basic UsersTable #981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Apr 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/src/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { SettingsPage } from "./pages/settings"
import { TemplatesPage } from "./pages/templates"
import { TemplatePage } from "./pages/templates/[organization]/[template]"
import { CreateWorkspacePage } from "./pages/templates/[organization]/[template]/create"
import { UsersPage } from "./pages/users"
import { UsersPage } from "./pages/UsersPage/UsersPage"
import { WorkspacePage } from "./pages/workspaces/[workspace]"

export const AppRouter: React.FC = () => (
Expand Down
10 changes: 10 additions & 0 deletions site/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios, { AxiosRequestHeaders } from "axios"
import { mutate } from "swr"
import { MockPager, MockUser, MockUser2 } from "../test_helpers"
import * as Types from "./types"

const CONTENT_TYPE_JSON: AxiosRequestHeaders = {
Expand Down Expand Up @@ -69,6 +70,15 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
return response.data
}

export const getUsers = async (): Promise<Types.PagedUsers> => {
// const response = await axios.get<Types.UserResponse[]>("/api/v2/users")
// return response.data
return Promise.resolve({
page: [MockUser, MockUser2],
pager: MockPager,
})
}

export const getBuildInfo = async (): Promise<Types.BuildInfoResponse> => {
const response = await axios.get("/api/v2/buildinfo")
return response.data
Expand Down
9 changes: 9 additions & 0 deletions site/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ export interface UserAgent {
readonly os: string
}

export interface Pager {
total: number
}

export interface PagedUsers {
page: UserResponse[]
pager: Pager
}

export interface WorkspaceAutostartRequest {
schedule: string
}
Expand Down
17 changes: 17 additions & 0 deletions site/src/components/ErrorSummary/ErrorSummary.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
import { ErrorSummary, ErrorSummaryProps } from "."

export default {
title: "components/ErrorSummary",
component: ErrorSummary,
} as ComponentMeta<typeof ErrorSummary>

const Template: Story<ErrorSummaryProps> = (args) => <ErrorSummary {...args} />

export const WithError = Template.bind({})
WithError.args = {
error: new Error("Something went wrong!"),
}

export const WithUndefined = Template.bind({})
14 changes: 9 additions & 5 deletions site/src/components/ErrorSummary/index.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from "react"

const Language = {
unknownErrorMessage: "Unknown error",
}

export interface ErrorSummaryProps {
error: Error | undefined
error: Error | unknown
}

export const ErrorSummary: React.FC<ErrorSummaryProps> = ({ error }) => {
// TODO: More interesting error page

if (typeof error === "undefined") {
return <div>{"Unknown error"}</div>
if (!(error instanceof Error)) {
return <div>{Language.unknownErrorMessage}</div>
} else {
return <div>{error.toString()}</div>
}

return <div>{error.toString()}</div>
}
21 changes: 21 additions & 0 deletions site/src/components/UsersTable/UsersTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
import { MockUser, MockUser2 } from "../../test_helpers"
import { UsersTable, UsersTableProps } from "./UsersTable"

export default {
title: "Components/UsersTable",
component: UsersTable,
} as ComponentMeta<typeof UsersTable>

const Template: Story<UsersTableProps> = (args) => <UsersTable {...args} />

export const Example = Template.bind({})
Example.args = {
users: [MockUser, MockUser2],
}

export const Empty = Template.bind({})
Empty.args = {
users: [],
}
32 changes: 32 additions & 0 deletions site/src/components/UsersTable/UsersTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react"
import { UserResponse } from "../../api/types"
import { Column, Table } from "../../components/Table"
import { EmptyState } from "../EmptyState"
import { UserCell } from "../Table/Cells/UserCell"

const Language = {
pageTitle: "Users",
usersTitle: "All users",
emptyMessage: "No users found",
usernameLabel: "User",
}

const emptyState = <EmptyState message={Language.emptyMessage} />

const columns: Column<UserResponse>[] = [
{
key: "username",
name: Language.usernameLabel,
renderer: (field, data) => {
return <UserCell Avatar={{ username: data.username }} primaryText={data.username} caption={data.email} />
},
},
]

export interface UsersTableProps {
users: UserResponse[]
}

export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
return <Table columns={columns} data={users} title={Language.usersTitle} emptyState={emptyState} />
}
18 changes: 18 additions & 0 deletions site/src/pages/UsersPage/UsersPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { screen } from "@testing-library/react"
import React from "react"
import { MockPager, render } from "../../test_helpers"
import { UsersPage } from "./UsersPage"
import { Language } from "./UsersPageView"

describe("Users Page", () => {
it("has a header with the total number of users", async () => {
render(<UsersPage />)
const total = await screen.findByText(/\d+ total/)
expect(total.innerHTML).toEqual(Language.pageSubtitle(MockPager))
})
it("shows users", async () => {
render(<UsersPage />)
const users = await screen.findAllByText(/.*@coder.com/)
expect(users.length).toEqual(2)
})
})
17 changes: 17 additions & 0 deletions site/src/pages/UsersPage/UsersPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useActor } from "@xstate/react"
import React, { useContext } from "react"
import { ErrorSummary } from "../../components/ErrorSummary"
import { XServiceContext } from "../../xServices/StateContext"
import { UsersPageView } from "./UsersPageView"

export const UsersPage: React.FC = () => {
const xServices = useContext(XServiceContext)
const [usersState] = useActor(xServices.usersXService)
const { users, pager, getUsersError } = usersState.context

if (usersState.matches("error")) {
return <ErrorSummary error={getUsersError} />
} else {
return <UsersPageView users={users} pager={pager} />
}
}
21 changes: 21 additions & 0 deletions site/src/pages/UsersPage/UsersPageView.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
import { MockPager, MockUser, MockUser2 } from "../../test_helpers"
import { UsersPageView, UsersPageViewProps } from "./UsersPageView"

export default {
title: "pages/UsersPageView",
component: UsersPageView,
} as ComponentMeta<typeof UsersPageView>

const Template: Story<UsersPageViewProps> = (args) => <UsersPageView {...args} />

export const Ready = Template.bind({})
Ready.args = {
users: [MockUser, MockUser2],
pager: MockPager,
}
export const Empty = Template.bind({})
Empty.args = {
users: [],
}
32 changes: 32 additions & 0 deletions site/src/pages/UsersPage/UsersPageView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { Pager, UserResponse } from "../../api/types"
import { Header } from "../../components/Header"
import { UsersTable } from "../../components/UsersTable/UsersTable"

export const Language = {
pageTitle: "Users",
pageSubtitle: (pager: Pager | undefined): string => (pager ? `${pager.total} total` : ""),
}

export interface UsersPageViewProps {
users: UserResponse[]
pager?: Pager
}

export const UsersPageView: React.FC<UsersPageViewProps> = ({ users, pager }) => {
const styles = useStyles()
return (
<div className={styles.flexColumn}>
<Header title={Language.pageTitle} subTitle={Language.pageSubtitle(pager)} />
<UsersTable users={users} />
</div>
)
}

const useStyles = makeStyles(() => ({
flexColumn: {
display: "flex",
flexDirection: "column",
},
}))
1 change: 0 additions & 1 deletion site/src/pages/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ describe("SignInPage", () => {
const password = screen.getByLabelText(Language.passwordLabel)
await userEvent.type(email, "test@coder.com")
await userEvent.type(password, "password")

// Click sign-in
const signInButton = await screen.findByText(Language.signIn)
act(() => signInButton.click())
Expand Down
5 changes: 0 additions & 5 deletions site/src/pages/users.tsx

This file was deleted.

12 changes: 12 additions & 0 deletions site/src/test_helpers/entities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BuildInfoResponse,
Organization,
Pager,
Provisioner,
Template,
UserAgent,
Expand All @@ -25,6 +26,17 @@ export const MockUser: UserResponse = {
created_at: "",
}

export const MockUser2: UserResponse = {
id: "test-user-2",
username: "TestUser2",
email: "test2@coder.com",
created_at: "",
}

export const MockPager: Pager = {
total: 2,
}

export const MockOrganization: Organization = {
id: "test-org",
name: "Test Organization",
Expand Down
3 changes: 3 additions & 0 deletions site/src/test_helpers/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const handlers = [
}),

// users
rest.get("/api/v2/users", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json({ page: [M.MockUser, M.MockUser2], pager: M.MockPager }))
}),
rest.post("/api/v2/users/me/workspaces", async (req, res, ctx) => {
return res(ctx.status(200), ctx.json(M.MockWorkspace))
}),
Expand Down
7 changes: 5 additions & 2 deletions site/src/xServices/StateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import React, { createContext } from "react"
import { ActorRefFrom } from "xstate"
import { authMachine } from "./auth/authXService"
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
import { usersMachine } from "./users/usersXService"

interface XServiceContextType {
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
authXService: ActorRefFrom<typeof authMachine>
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
usersXService: ActorRefFrom<typeof usersMachine>
}

/**
Expand All @@ -23,8 +25,9 @@ export const XServiceProvider: React.FC = ({ children }) => {
return (
<XServiceContext.Provider
value={{
buildInfoXService: useInterpret(buildInfoMachine),
authXService: useInterpret(authMachine),
buildInfoXService: useInterpret(buildInfoMachine),
usersXService: useInterpret(usersMachine),
}}
>
{children}
Expand Down
80 changes: 80 additions & 0 deletions site/src/xServices/users/usersXService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { assign, createMachine } from "xstate"
import * as API from "../../api"
import * as Types from "../../api/types"

export interface UsersContext {
users: Types.UserResponse[]
pager?: Types.Pager
getUsersError?: Error | unknown
}

export type UsersEvent = { type: "GET_USERS" }

export const usersMachine = createMachine(
{
tsTypes: {} as import("./usersXService.typegen").Typegen0,
schema: {
context: {} as UsersContext,
events: {} as UsersEvent,
services: {} as {
getUsers: {
data: Types.PagedUsers
}
},
},
id: "usersState",
context: {
users: [],
},
initial: "gettingUsers",
states: {
gettingUsers: {
invoke: {
src: "getUsers",
id: "getUsers",
onDone: [
{
target: "#usersState.ready",
actions: ["assignUsers", "clearGetUsersError"],
},
],
onError: [
{
actions: "assignGetUsersError",
target: "#usersState.error",
},
],
},
tags: "loading",
},
ready: {
on: {
GET_USERS: "gettingUsers",
},
},
error: {
on: {
GET_USERS: "gettingUsers",
},
},
},
},
{
services: {
getUsers: API.getUsers,
},
actions: {
assignUsers: assign({
users: (_, event) => event.data.page,
pager: (_, event) => event.data.pager,
}),
assignGetUsersError: assign({
getUsersError: (_, event) => event.data,
}),
clearGetUsersError: assign((context: UsersContext) => ({
...context,
getUsersError: undefined,
})),
},
},
)