diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index b15e06fb710e0..79d3d1b580c21 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -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 = () => ( diff --git a/site/src/api/index.ts b/site/src/api/index.ts index 135ac91646339..c6c28cff273c9 100644 --- a/site/src/api/index.ts +++ b/site/src/api/index.ts @@ -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 = { @@ -69,6 +70,15 @@ export const getApiKey = async (): Promise => { return response.data } +export const getUsers = async (): Promise => { + // const response = await axios.get("/api/v2/users") + // return response.data + return Promise.resolve({ + page: [MockUser, MockUser2], + pager: MockPager, + }) +} + export const getBuildInfo = async (): Promise => { const response = await axios.get("/api/v2/buildinfo") return response.data diff --git a/site/src/api/types.ts b/site/src/api/types.ts index e61faf9d99634..05c3a5cf07355 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -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 } diff --git a/site/src/components/ErrorSummary/ErrorSummary.stories.tsx b/site/src/components/ErrorSummary/ErrorSummary.stories.tsx new file mode 100644 index 0000000000000..33b13106f25e3 --- /dev/null +++ b/site/src/components/ErrorSummary/ErrorSummary.stories.tsx @@ -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 + +const Template: Story = (args) => + +export const WithError = Template.bind({}) +WithError.args = { + error: new Error("Something went wrong!"), +} + +export const WithUndefined = Template.bind({}) diff --git a/site/src/components/ErrorSummary/index.tsx b/site/src/components/ErrorSummary/index.tsx index 13555dcd996dd..9c528273f5571 100644 --- a/site/src/components/ErrorSummary/index.tsx +++ b/site/src/components/ErrorSummary/index.tsx @@ -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 = ({ error }) => { // TODO: More interesting error page - if (typeof error === "undefined") { - return
{"Unknown error"}
+ if (!(error instanceof Error)) { + return
{Language.unknownErrorMessage}
+ } else { + return
{error.toString()}
} - - return
{error.toString()}
} diff --git a/site/src/components/UsersTable/UsersTable.stories.tsx b/site/src/components/UsersTable/UsersTable.stories.tsx new file mode 100644 index 0000000000000..394f31e662680 --- /dev/null +++ b/site/src/components/UsersTable/UsersTable.stories.tsx @@ -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 + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + users: [MockUser, MockUser2], +} + +export const Empty = Template.bind({}) +Empty.args = { + users: [], +} diff --git a/site/src/components/UsersTable/UsersTable.tsx b/site/src/components/UsersTable/UsersTable.tsx new file mode 100644 index 0000000000000..7fd1bfe8b6a41 --- /dev/null +++ b/site/src/components/UsersTable/UsersTable.tsx @@ -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 = + +const columns: Column[] = [ + { + key: "username", + name: Language.usernameLabel, + renderer: (field, data) => { + return + }, + }, +] + +export interface UsersTableProps { + users: UserResponse[] +} + +export const UsersTable: React.FC = ({ users }) => { + return +} diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx new file mode 100644 index 0000000000000..51cd844f83378 --- /dev/null +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -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() + const total = await screen.findByText(/\d+ total/) + expect(total.innerHTML).toEqual(Language.pageSubtitle(MockPager)) + }) + it("shows users", async () => { + render() + const users = await screen.findAllByText(/.*@coder.com/) + expect(users.length).toEqual(2) + }) +}) diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx new file mode 100644 index 0000000000000..ca0c4bd71fed8 --- /dev/null +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -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 + } else { + return + } +} diff --git a/site/src/pages/UsersPage/UsersPageView.stories.tsx b/site/src/pages/UsersPage/UsersPageView.stories.tsx new file mode 100644 index 0000000000000..afb51b9ea0b41 --- /dev/null +++ b/site/src/pages/UsersPage/UsersPageView.stories.tsx @@ -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 + +const Template: Story = (args) => + +export const Ready = Template.bind({}) +Ready.args = { + users: [MockUser, MockUser2], + pager: MockPager, +} +export const Empty = Template.bind({}) +Empty.args = { + users: [], +} diff --git a/site/src/pages/UsersPage/UsersPageView.tsx b/site/src/pages/UsersPage/UsersPageView.tsx new file mode 100644 index 0000000000000..082a9c9403650 --- /dev/null +++ b/site/src/pages/UsersPage/UsersPageView.tsx @@ -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 = ({ users, pager }) => { + const styles = useStyles() + return ( +
+
+ +
+ ) +} + +const useStyles = makeStyles(() => ({ + flexColumn: { + display: "flex", + flexDirection: "column", + }, +})) diff --git a/site/src/pages/login.test.tsx b/site/src/pages/login.test.tsx index 8f1bcdeec1157..f54379f7490fb 100644 --- a/site/src/pages/login.test.tsx +++ b/site/src/pages/login.test.tsx @@ -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()) diff --git a/site/src/pages/users.tsx b/site/src/pages/users.tsx deleted file mode 100644 index a04e1ab5b4646..0000000000000 --- a/site/src/pages/users.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react" - -export const UsersPage: React.FC = () => { - return
Coming soon!
-} diff --git a/site/src/test_helpers/entities.ts b/site/src/test_helpers/entities.ts index 661608cef3077..bdff6266861ad 100644 --- a/site/src/test_helpers/entities.ts +++ b/site/src/test_helpers/entities.ts @@ -1,6 +1,7 @@ import { BuildInfoResponse, Organization, + Pager, Provisioner, Template, UserAgent, @@ -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", diff --git a/site/src/test_helpers/handlers.ts b/site/src/test_helpers/handlers.ts index 2875a9ed86320..0a6df84fba287 100644 --- a/site/src/test_helpers/handlers.ts +++ b/site/src/test_helpers/handlers.ts @@ -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)) }), diff --git a/site/src/xServices/StateContext.tsx b/site/src/xServices/StateContext.tsx index 211f1991bb14c..ecd16739224f8 100644 --- a/site/src/xServices/StateContext.tsx +++ b/site/src/xServices/StateContext.tsx @@ -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 authXService: ActorRefFrom + buildInfoXService: ActorRefFrom + usersXService: ActorRefFrom } /** @@ -23,8 +25,9 @@ export const XServiceProvider: React.FC = ({ children }) => { return ( {children} diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts new file mode 100644 index 0000000000000..d02c98b84bd35 --- /dev/null +++ b/site/src/xServices/users/usersXService.ts @@ -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, + })), + }, + }, +)