From e769722cedfe10fd38008cc23090c73c00d7099d Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 1 Jun 2022 23:13:27 +0000 Subject: [PATCH 01/11] feat: Workspaces filtering --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 91 ++++++++++- .../WorkspacesPage/WorkspacesPageView.tsx | 141 ++++++++---------- site/src/util/workspace.test.ts | 13 +- site/src/util/workspace.ts | 24 ++- .../workspaces/workspacesXService.ts | 29 +++- 5 files changed, 204 insertions(+), 94 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 3d20b318c4e26..5e143771e9805 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,20 +1,95 @@ +import Button from "@material-ui/core/Button" +import Link from "@material-ui/core/Link" +import Menu from "@material-ui/core/Menu" +import MenuItem from "@material-ui/core/MenuItem" +import { makeStyles } from "@material-ui/core/styles" +import TextField from "@material-ui/core/TextField" +import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import { useMachine } from "@xstate/react" -import { FC } from "react" +import { FormikContextType, FormikErrors, useFormik } from "formik" +import { FC, useState } from "react" +import { Link as RouterLink } from "react-router-dom" +import { Margins } from "../../components/Margins/Margins" +import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { workspacesMachine } from "../../xServices/workspaces/workspacesXService" -import { WorkspacesPageView } from "./WorkspacesPageView" +import { Language, WorkspacesPageView } from "./WorkspacesPageView" + +interface FilterFormValues { + query: string +} + +export type FilterFormErrors = FormikErrors const WorkspacesPage: FC = () => { - const [workspacesState] = useMachine(workspacesMachine) + const styles = useStyles() + const [workspacesState, send] = useMachine(workspacesMachine) + + const form: FormikContextType = useFormik({ + initialValues: { query: workspacesState.context.filter || "" }, + onSubmit: (data) => { + send({ + type: "SET_FILTER", + query: data.query, + }) + }, + }) + + const getFieldHelpers = getFormHelpers(form, {}) + + const [anchorEl, setAnchorEl] = useState(null) + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + const setYourWorkspaces = () => { + form.setFieldValue("query", "owner:me") + void form.submitForm() + handleClose() + } + const setAllWorkspaces = () => { + form.setFieldValue("query", "") + void form.submitForm() + handleClose() + } return ( <> - + +
+ + + Your workspaces + All workspaces + +
+ + + + + +
+ +
) } +const useStyles = makeStyles((theme) => ({ + actions: { + marginTop: theme.spacing(3), + marginBottom: theme.spacing(3), + display: "flex", + justifyContent: "space-between", + height: theme.spacing(6), + }, +})) + export default WorkspacesPage diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 2a412116be7bb..71a4289753b75 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -15,7 +15,7 @@ import { Link as RouterLink } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" import { EmptyState } from "../../components/EmptyState/EmptyState" -import { Margins } from "../../components/Margins/Margins" +import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { Stack } from "../../components/Stack/Stack" import { TableLoader } from "../../components/TableLoader/TableLoader" import { getDisplayStatus } from "../../util/workspace" @@ -34,93 +34,78 @@ export interface WorkspacesPageViewProps { error?: unknown } -export const WorkspacesPageView: FC = (props) => { - const styles = useStyles() +export const WorkspacesPageView: FC = ({ loading, workspaces, error }) => { + useStyles() const theme: Theme = useTheme() + return ( - -
- - - -
- - + {error && } +
+ + + Name + Template + Version + Last Built + Status + + + + {loading && } + {workspaces && workspaces.length === 0 && ( - Name - Template - Version - Last Built - Status + + + + + } + /> + - - - {props.loading && } - {props.workspaces && props.workspaces.length === 0 && ( - - - - - - } - /> - - - )} - {props.workspaces && - props.workspaces.map((workspace) => { - const status = getDisplayStatus(theme, workspace.latest_build) - return ( - - - - - {workspace.template_name} - - {workspace.outdated ? ( - outdated - ) : ( - up to date - )} - - - - {dayjs().to(dayjs(workspace.latest_build.created_at))} - - - - {status.status} - - - ) - })} - -
-
+ )} + {workspaces && + workspaces.map((workspace) => { + const status = getDisplayStatus(theme, workspace.latest_build) + return ( + + + + + {workspace.template_name} + + {workspace.outdated ? ( + outdated + ) : ( + up to date + )} + + + + {dayjs().to(dayjs(workspace.latest_build.created_at))} + + + + {status.status} + + + ) + })} + +
) } const useStyles = makeStyles((theme) => ({ - actions: { - marginTop: theme.spacing(3), - marginBottom: theme.spacing(3), - display: "flex", - height: theme.spacing(6), - - "& > *": { - marginLeft: "auto", - }, - }, welcome: { paddingTop: theme.spacing(12), paddingBottom: theme.spacing(12), diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 69657a75015bd..e044aba172219 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -1,6 +1,6 @@ import * as TypesGen from "../api/typesGenerated" import * as Mocks from "../testHelpers/entities" -import { isWorkspaceOn } from "./workspace" +import { isWorkspaceOn, workspaceQueryToFilter } from "./workspace" describe("util > workspace", () => { describe("isWorkspaceOn", () => { @@ -40,4 +40,15 @@ describe("util > workspace", () => { expect(isWorkspaceOn(workspace)).toBe(isOn) }) }) + describe("workspaceQueryToFilter", () => { + it.each<[string | undefined, TypesGen.WorkspaceFilter]>([ + [undefined, { Owner: "", OrganizationID: "" }], + ["", { Owner: "", OrganizationID: "" }], + ["asdkfvjn", { Owner: "", OrganizationID: "" }], + ["owner:me", { Owner: "me", OrganizationID: "" }], + ["owner:me owner:me2", { Owner: "me2", OrganizationID: "" }], + ])(`query=%p, filter=%p`, (query, filter) => { + expect(workspaceQueryToFilter(query)).toBe(filter) + }) + }) }) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 5e4f678b48680..6a83a534fbad4 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -1,7 +1,7 @@ import { Theme } from "@material-ui/core/styles" import dayjs from "dayjs" import { WorkspaceBuildTransition } from "../api/types" -import { Workspace, WorkspaceAgent, WorkspaceBuild } from "../api/typesGenerated" +import { Workspace, WorkspaceAgent, WorkspaceBuild, WorkspaceFilter } from "../api/typesGenerated" export type WorkspaceStatus = | "queued" @@ -191,3 +191,25 @@ export const isWorkspaceOn = (workspace: Workspace): boolean => { const status = workspace.latest_build.job.status return transition === "start" && status === "succeeded" } + +export const workspaceQueryToFilter = (query?: string): WorkspaceFilter => { + let filter: WorkspaceFilter = { + Owner: "", + OrganizationID: "", + } + + if (query) { + const parts = query.split(" ") + + parts.map((part) => { + if (part.startsWith("owner:")) { + filter = { + Owner: part.slice("owner:".length), + OrganizationID: "", + } + } + }) + } + + return filter +} diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index 353e71b234a73..a5e94c9559a04 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -1,13 +1,15 @@ import { assign, createMachine } from "xstate" import * as API from "../../api/api" import * as TypesGen from "../../api/typesGenerated" +import { workspaceQueryToFilter } from "../../util/workspace" interface WorkspaceContext { workspaces?: TypesGen.Workspace[] + filter?: string getWorkspacesError?: Error | unknown } -type WorkspaceEvent = { type: "GET_WORKSPACE"; workspaceId: string } +type WorkspaceEvent = { type: "GET_WORKSPACE"; workspaceId: string } | { type: "SET_FILTER"; query: string } export const workspacesMachine = createMachine( { @@ -22,26 +24,38 @@ export const workspacesMachine = createMachine( }, }, id: "workspaceState", + context: { + filter: "owner:me", + }, initial: "gettingWorkspaces", states: { + ready: { + on: { + SET_FILTER: "extractingFilter", + }, + }, + extractingFilter: { + entry: "assignFilter", + always: { + target: "gettingWorkspaces", + }, + }, gettingWorkspaces: { entry: "clearGetWorkspacesError", invoke: { src: "getWorkspaces", id: "getWorkspaces", onDone: { - target: "done", + target: "ready", actions: ["assignWorkspaces", "clearGetWorkspacesError"], }, onError: { - target: "error", + target: "ready", actions: "assignGetWorkspacesError", }, }, tags: "loading", }, - done: {}, - error: {}, }, }, { @@ -49,13 +63,16 @@ export const workspacesMachine = createMachine( assignWorkspaces: assign({ workspaces: (_, event) => event.data, }), + assignFilter: assign({ + filter: (_, event) => event.query, + }), assignGetWorkspacesError: assign({ getWorkspacesError: (_, event) => event.data, }), clearGetWorkspacesError: (context) => assign({ ...context, getWorkspacesError: undefined }), }, services: { - getWorkspaces: () => API.getWorkspaces(), + getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)), }, }, ) From 2c576a6a5f370d29f7d975083c5b724001adb80e Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 1 Jun 2022 23:16:18 +0000 Subject: [PATCH 02/11] Remove error handling --- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 71a4289753b75..0929b6e241acc 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -15,7 +15,6 @@ import { Link as RouterLink } from "react-router-dom" import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" import { EmptyState } from "../../components/EmptyState/EmptyState" -import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { Stack } from "../../components/Stack/Stack" import { TableLoader } from "../../components/TableLoader/TableLoader" import { getDisplayStatus } from "../../util/workspace" @@ -34,13 +33,12 @@ export interface WorkspacesPageViewProps { error?: unknown } -export const WorkspacesPageView: FC = ({ loading, workspaces, error }) => { +export const WorkspacesPageView: FC = ({ loading, workspaces }) => { useStyles() const theme: Theme = useTheme() return ( - {error && } From df15018cdb314ff8f6d3787524a7710cc245b793 Mon Sep 17 00:00:00 2001 From: Garrett Date: Wed, 1 Jun 2022 23:25:10 +0000 Subject: [PATCH 03/11] styling --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 5e143771e9805..da431d5fd7f6e 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -10,6 +10,7 @@ import { FormikContextType, FormikErrors, useFormik } from "formik" import { FC, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { Margins } from "../../components/Margins/Margins" +import { Stack } from "../../components/Stack/Stack" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { workspacesMachine } from "../../xServices/workspaces/workspacesXService" import { Language, WorkspacesPageView } from "./WorkspacesPageView" @@ -58,16 +59,19 @@ const WorkspacesPage: FC = () => { <>
- - - Your workspaces - All workspaces - -
- - + + + + Your workspaces + All workspaces + +
+ + +
+ From bc2ed3389f8d5245c877ebfe3f2a7a3ac0d2aebc Mon Sep 17 00:00:00 2001 From: Garrett Delfosse Date: Thu, 2 Jun 2022 11:11:10 -0500 Subject: [PATCH 04/11] Apply suggestions from code review Co-authored-by: G r e y --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 23 +++++++++++------ site/src/util/workspace.test.ts | 4 +-- site/src/util/workspace.ts | 25 +++++++++++-------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index da431d5fd7f6e..2268971adcb40 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -25,30 +25,36 @@ const WorkspacesPage: FC = () => { const styles = useStyles() const [workspacesState, send] = useMachine(workspacesMachine) - const form: FormikContextType = useFormik({ - initialValues: { query: workspacesState.context.filter || "" }, - onSubmit: (data) => { +const form = useFormik({ + initialValues: { + query: workspacesState.context.filter || "", + }, + onSubmit: (values) => { send({ type: "SET_FILTER", - query: data.query, + query: values.query, }) }, }) - const getFieldHelpers = getFormHelpers(form, {}) + const getFieldHelpers = getFormHelpers(form) const [anchorEl, setAnchorEl] = useState(null) + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget) } + const handleClose = () => { setAnchorEl(null) } + const setYourWorkspaces = () => { form.setFieldValue("query", "owner:me") void form.submitForm() handleClose() } + const setAllWorkspaces = () => { form.setFieldValue("query", "") void form.submitForm() @@ -60,13 +66,15 @@ const WorkspacesPage: FC = () => {
- - + + Your workspaces All workspaces +
@@ -76,6 +84,7 @@ const WorkspacesPage: FC = () => {
+ workspace", () => { ["", { Owner: "", OrganizationID: "" }], ["asdkfvjn", { Owner: "", OrganizationID: "" }], ["owner:me", { Owner: "me", OrganizationID: "" }], - ["owner:me owner:me2", { Owner: "me2", OrganizationID: "" }], + ["owner:me owner:me2", { Owner: "me", OrganizationID: "" }], ])(`query=%p, filter=%p`, (query, filter) => { - expect(workspaceQueryToFilter(query)).toBe(filter) + expect(workspaceQueryToFilter(query)).toEqual(filter) }) }) }) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 6a83a534fbad4..602aeb0549e9b 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -193,23 +193,28 @@ export const isWorkspaceOn = (workspace: Workspace): boolean => { } export const workspaceQueryToFilter = (query?: string): WorkspaceFilter => { - let filter: WorkspaceFilter = { + const defaultFilter: WorkspaceFilter = { Owner: "", OrganizationID: "", } - if (query) { - const parts = query.split(" ") + const preparedQuery = query?.replace(/ +/g, " ") - parts.map((part) => { - if (part.startsWith("owner:")) { - filter = { - Owner: part.slice("owner:".length), + if (!preparedQuery) { + return defaultFilter + } else { + const parts = preparedQuery.split(" ") + + for (const part of parts) { + const [key, val] = part.split(":") + if (key === "owner") { + return { + Owner: val, OrganizationID: "", } } - }) - } + } - return filter + return defaultFilter + } } From d43a15aa0059376179181a69c3793053e439422b Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 16:18:48 +0000 Subject: [PATCH 05/11] pr fixes --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 9 ++++---- .../WorkspacesPage/WorkspacesPageView.tsx | 1 - site/src/util/workspace.ts | 21 +++++++++++-------- .../workspaces/workspacesXService.ts | 3 ++- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 2268971adcb40..b64903edac2a1 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -6,7 +6,7 @@ import { makeStyles } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import { useMachine } from "@xstate/react" -import { FormikContextType, FormikErrors, useFormik } from "formik" +import { FormikErrors, useFormik } from "formik" import { FC, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { Margins } from "../../components/Margins/Margins" @@ -25,7 +25,7 @@ const WorkspacesPage: FC = () => { const styles = useStyles() const [workspacesState, send] = useMachine(workspacesMachine) -const form = useFormik({ + const form = useFormik({ initialValues: { query: workspacesState.context.filter || "", }, @@ -50,13 +50,13 @@ const form = useFormik({ } const setYourWorkspaces = () => { - form.setFieldValue("query", "owner:me") + void form.setFieldValue("query", "owner:me") void form.submitForm() handleClose() } const setAllWorkspaces = () => { - form.setFieldValue("query", "") + void form.setFieldValue("query", "") void form.submitForm() handleClose() } @@ -88,7 +88,6 @@ const form = useFormik({
diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 0929b6e241acc..bc49893e2e94b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -30,7 +30,6 @@ export const Language = { export interface WorkspacesPageViewProps { loading?: boolean workspaces?: TypesGen.Workspace[] - error?: unknown } export const WorkspacesPageView: FC = ({ loading, workspaces }) => { diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 602aeb0549e9b..8f75eddb01990 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -1,7 +1,7 @@ import { Theme } from "@material-ui/core/styles" import dayjs from "dayjs" import { WorkspaceBuildTransition } from "../api/types" -import { Workspace, WorkspaceAgent, WorkspaceBuild, WorkspaceFilter } from "../api/typesGenerated" +import * as TypeGen from "../api/typesGenerated" export type WorkspaceStatus = | "queued" @@ -29,7 +29,7 @@ const succeededToStatus: Record = { } // Converts a workspaces status to a human-readable form. -export const getWorkspaceStatus = (workspaceBuild?: WorkspaceBuild): WorkspaceStatus => { +export const getWorkspaceStatus = (workspaceBuild?: TypeGen.WorkspaceBuild): WorkspaceStatus => { const transition = workspaceBuild?.transition as WorkspaceBuildTransition const jobStatus = workspaceBuild?.job.status switch (jobStatus) { @@ -66,7 +66,7 @@ export const DisplayStatusLanguage = { export const getDisplayStatus = ( theme: Theme, - build: WorkspaceBuild, + build: TypeGen.WorkspaceBuild, ): { color: string status: string @@ -132,7 +132,7 @@ export const getDisplayStatus = ( throw new Error("unknown status " + status) } -export const getWorkspaceBuildDurationInSeconds = (build: WorkspaceBuild): number | undefined => { +export const getWorkspaceBuildDurationInSeconds = (build: TypeGen.WorkspaceBuild): number | undefined => { const isCompleted = build.job.started_at && build.job.completed_at if (!isCompleted) { @@ -144,7 +144,10 @@ export const getWorkspaceBuildDurationInSeconds = (build: WorkspaceBuild): numbe return completedAt.diff(startedAt, "seconds") } -export const displayWorkspaceBuildDuration = (build: WorkspaceBuild, inProgressLabel = "In progress"): string => { +export const displayWorkspaceBuildDuration = ( + build: TypeGen.WorkspaceBuild, + inProgressLabel = "In progress", +): string => { const duration = getWorkspaceBuildDurationInSeconds(build) return duration ? `${duration} seconds` : inProgressLabel } @@ -157,7 +160,7 @@ export const DisplayAgentStatusLanguage = { export const getDisplayAgentStatus = ( theme: Theme, - agent: WorkspaceAgent, + agent: TypeGen.WorkspaceAgent, ): { color: string status: string @@ -186,14 +189,14 @@ export const getDisplayAgentStatus = ( } } -export const isWorkspaceOn = (workspace: Workspace): boolean => { +export const isWorkspaceOn = (workspace: TypeGen.Workspace): boolean => { const transition = workspace.latest_build.transition const status = workspace.latest_build.job.status return transition === "start" && status === "succeeded" } -export const workspaceQueryToFilter = (query?: string): WorkspaceFilter => { - const defaultFilter: WorkspaceFilter = { +export const workspaceQueryToFilter = (query?: string): TypeGen.WorkspaceFilter => { + const defaultFilter: TypeGen.WorkspaceFilter = { Owner: "", OrganizationID: "", } diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index a5e94c9559a04..d4367b7ad95a3 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -51,7 +51,7 @@ export const workspacesMachine = createMachine( }, onError: { target: "ready", - actions: "assignGetWorkspacesError", + actions: ["assignGetWorkspacesError", "clearWorkspaces"], }, }, tags: "loading", @@ -70,6 +70,7 @@ export const workspacesMachine = createMachine( getWorkspacesError: (_, event) => event.data, }), clearGetWorkspacesError: (context) => assign({ ...context, getWorkspacesError: undefined }), + clearWorkspaces: (context) => assign({ ...context, workspaces: undefined }), }, services: { getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)), From 49c9bb16c990b1c1a5d5a9d295db80f8b0b44135 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 16:22:40 +0000 Subject: [PATCH 06/11] language --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index b64903edac2a1..d360fedb6dc82 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -13,12 +13,18 @@ import { Margins } from "../../components/Margins/Margins" import { Stack } from "../../components/Stack/Stack" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { workspacesMachine } from "../../xServices/workspaces/workspacesXService" -import { Language, WorkspacesPageView } from "./WorkspacesPageView" +import { WorkspacesPageView } from "./WorkspacesPageView" interface FilterFormValues { query: string } +const Language = { + createWorkspaceButton: "Create workspace", + yourWorkspacesButton: "Your workspaces", + allWorkspacesButton: "All workspaces" +} + export type FilterFormErrors = FormikErrors const WorkspacesPage: FC = () => { @@ -71,8 +77,8 @@ const WorkspacesPage: FC = () => { - Your workspaces - All workspaces + {Language.yourWorkspacesButton} + {Language.allWorkspacesButton}
@@ -81,7 +87,7 @@ const WorkspacesPage: FC = () => { - +
From 7117ac572c6b33052494c6c81cabe5b05955e39e Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 18:58:48 +0000 Subject: [PATCH 07/11] push up what I have --- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index d360fedb6dc82..23d1503cf6332 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -14,12 +14,14 @@ import { Stack } from "../../components/Stack/Stack" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { workspacesMachine } from "../../xServices/workspaces/workspacesXService" import { WorkspacesPageView } from "./WorkspacesPageView" +import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows" interface FilterFormValues { query: string } const Language = { + filterName: "Filters", createWorkspaceButton: "Create workspace", yourWorkspacesButton: "Your workspaces", allWorkspacesButton: "All workspaces" @@ -73,10 +75,15 @@ const WorkspacesPage: FC = () => {
- + {Language.yourWorkspacesButton} {Language.allWorkspacesButton} From 9597345b7a919759428efbfec29670c936cbe42b Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 20:00:00 +0000 Subject: [PATCH 08/11] Add name filter to backend --- coderd/database/queries.sql.go | 14 +++++++++++++- coderd/database/queries/workspaces.sql | 6 ++++++ coderd/workspaces.go | 4 ++++ codersdk/workspaces.go | 11 ++++++++++- site/src/api/typesGenerated.ts | 16 +++++++++------- site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 11 +++-------- 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index be5f9216eff2c..55639ed4cf74a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3498,16 +3498,28 @@ WHERE owner_id = $3 ELSE true END + -- Filter by name + AND CASE + WHEN $4 :: string != '' THEN + name = LOWER($4) + ELSE true + END ` type GetWorkspacesWithFilterParams struct { Deleted bool `db:"deleted" json:"deleted"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` + Name string `db:"name" json:"name"` } func (q *sqlQuerier) GetWorkspacesWithFilter(ctx context.Context, arg GetWorkspacesWithFilterParams) ([]Workspace, error) { - rows, err := q.db.QueryContext(ctx, getWorkspacesWithFilter, arg.Deleted, arg.OrganizationID, arg.OwnerID) + rows, err := q.db.QueryContext(ctx, getWorkspacesWithFilter, + arg.Deleted, + arg.OrganizationID, + arg.OwnerID, + arg.Name, + ) if err != nil { return nil, err } diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 000e4e92ce5a9..e406d3c62238a 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -28,6 +28,12 @@ WHERE owner_id = @owner_id ELSE true END + -- Filter by name + AND CASE + WHEN @name :: string != '' THEN + name = LOWER(@name) + ELSE true + END ; -- name: GetWorkspacesByOrganizationIDs :many diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 7de4e0a806e15..b23e52868adf6 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -137,6 +137,7 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { // Empty strings mean no filter orgFilter := r.URL.Query().Get("organization_id") ownerFilter := r.URL.Query().Get("owner") + nameFilter := r.URL.Query().Get("name") filter := database.GetWorkspacesWithFilterParams{Deleted: false} if orgFilter != "" { @@ -170,6 +171,9 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { } filter.OwnerID = userID } + if nameFilter != "" { + filter.Name = nameFilter + } workspaces, err := api.Database.GetWorkspacesWithFilter(r.Context(), filter) if err != nil { diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 3ad1ac42fb830..4439612e1d783 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "time" "github.com/google/uuid" @@ -200,7 +201,9 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx type WorkspaceFilter struct { OrganizationID uuid.UUID // Owner can be a user_id (uuid), "me", or a username - Owner string + Owner string + Name string + Deleted bool } // asRequestOption returns a function that can be used in (*Client).Request. @@ -214,6 +217,12 @@ func (f WorkspaceFilter) asRequestOption() requestOption { if f.Owner != "" { q.Set("owner", f.Owner) } + if f.Name != "" { + q.Set("name", f.Name) + } + if f.Deleted { + q.Set("deleted", strconv.FormatBool(f.Deleted)) + } r.URL.RawQuery = q.Encode() } } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3c5d5df800fa6..3a33a37d720ef 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -88,7 +88,7 @@ export interface CreateUserRequest { readonly organization_id: string } -// From codersdk/workspaces.go:34:6 +// From codersdk/workspaces.go:35:6 export interface CreateWorkspaceBuildRequest { readonly template_version_id?: string readonly transition: WorkspaceTransition @@ -222,7 +222,7 @@ export interface ProvisionerJobLog { readonly output: string } -// From codersdk/workspaces.go:182:6 +// From codersdk/workspaces.go:183:6 export interface PutExtendWorkspaceRequest { readonly deadline: string } @@ -299,12 +299,12 @@ export interface UpdateUserProfileRequest { readonly username: string } -// From codersdk/workspaces.go:141:6 +// From codersdk/workspaces.go:142:6 export interface UpdateWorkspaceAutostartRequest { readonly schedule: string } -// From codersdk/workspaces.go:161:6 +// From codersdk/workspaces.go:162:6 export interface UpdateWorkspaceTTLRequest { // This is likely an enum in an external package ("time.Duration") readonly ttl?: number @@ -360,7 +360,7 @@ export interface UsersRequest extends Pagination { readonly status?: string } -// From codersdk/workspaces.go:18:6 +// From codersdk/workspaces.go:19:6 export interface Workspace { readonly id: string readonly created_at: string @@ -438,15 +438,17 @@ export interface WorkspaceBuild { readonly deadline: string } -// From codersdk/workspaces.go:64:6 +// From codersdk/workspaces.go:65:6 export interface WorkspaceBuildsRequest extends Pagination { readonly WorkspaceID: string } -// From codersdk/workspaces.go:200:6 +// From codersdk/workspaces.go:201:6 export interface WorkspaceFilter { readonly OrganizationID: string readonly Owner: string + readonly Name: string + readonly Deleted: boolean } // From codersdk/workspaceresources.go:21:6 diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 23d1503cf6332..37b10589dbc2b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -9,12 +9,12 @@ import { useMachine } from "@xstate/react" import { FormikErrors, useFormik } from "formik" import { FC, useState } from "react" import { Link as RouterLink } from "react-router-dom" +import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows" import { Margins } from "../../components/Margins/Margins" import { Stack } from "../../components/Stack/Stack" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { workspacesMachine } from "../../xServices/workspaces/workspacesXService" import { WorkspacesPageView } from "./WorkspacesPageView" -import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows" interface FilterFormValues { query: string @@ -24,7 +24,7 @@ const Language = { filterName: "Filters", createWorkspaceButton: "Create workspace", yourWorkspacesButton: "Your workspaces", - allWorkspacesButton: "All workspaces" + allWorkspacesButton: "All workspaces", } export type FilterFormErrors = FormikErrors @@ -78,12 +78,7 @@ const WorkspacesPage: FC = () => { {Language.filterName} {anchorEl ? : } - + {Language.yourWorkspacesButton} {Language.allWorkspacesButton} From 6a6a7e6bb0a4df99b60602a204a2ed1a0278fd1c Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 20:29:27 +0000 Subject: [PATCH 09/11] Add name filter --- site/src/api/api.test.ts | 6 +++--- site/src/api/api.ts | 11 +++++++---- site/src/api/typesGenerated.ts | 7 +++---- site/src/util/workspace.test.ts | 11 ++++++----- site/src/util/workspace.ts | 20 +++++++++++++------- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/site/src/api/api.test.ts b/site/src/api/api.test.ts index 5714c080d384e..083eb177fb6ac 100644 --- a/site/src/api/api.test.ts +++ b/site/src/api/api.test.ts @@ -118,10 +118,10 @@ describe("api.ts", () => { it.each<[TypesGen.WorkspaceFilter | undefined, string]>([ [undefined, "/api/v2/workspaces"], - [{ OrganizationID: "1", Owner: "" }, "/api/v2/workspaces?organization_id=1"], - [{ OrganizationID: "", Owner: "1" }, "/api/v2/workspaces?owner=1"], + [{ organization_id: "1", owner: "" }, "/api/v2/workspaces?organization_id=1"], + [{ organization_id: "", owner: "1" }, "/api/v2/workspaces?owner=1"], - [{ OrganizationID: "1", Owner: "me" }, "/api/v2/workspaces?organization_id=1&owner=me"], + [{ organization_id: "1", owner: "me" }, "/api/v2/workspaces?organization_id=1&owner=me"], ])(`getWorkspacesURL(%p) returns %p`, (filter, expected) => { expect(getWorkspacesURL(filter)).toBe(expected) }) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 68c41b77e2ae7..6ee267b6f7aed 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -116,11 +116,14 @@ export const getWorkspacesURL = (filter?: TypesGen.WorkspaceFilter): string => { const basePath = "/api/v2/workspaces" const searchParams = new URLSearchParams() - if (filter?.OrganizationID) { - searchParams.append("organization_id", filter.OrganizationID) + if (filter?.organization_id) { + searchParams.append("organization_id", filter.organization_id) } - if (filter?.Owner) { - searchParams.append("owner", filter.Owner) + if (filter?.owner) { + searchParams.append("owner", filter.owner) + } + if (filter?.name) { + searchParams.append("name", filter.name) } const searchString = searchParams.toString() diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 3a33a37d720ef..56a4ea5155c1d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -445,10 +445,9 @@ export interface WorkspaceBuildsRequest extends Pagination { // From codersdk/workspaces.go:201:6 export interface WorkspaceFilter { - readonly OrganizationID: string - readonly Owner: string - readonly Name: string - readonly Deleted: boolean + readonly organization_id?: string + readonly owner?: string + readonly name?: string } // From codersdk/workspaceresources.go:21:6 diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 809917612d956..846f3b46b2a5d 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -42,11 +42,12 @@ describe("util > workspace", () => { }) describe("workspaceQueryToFilter", () => { it.each<[string | undefined, TypesGen.WorkspaceFilter]>([ - [undefined, { Owner: "", OrganizationID: "" }], - ["", { Owner: "", OrganizationID: "" }], - ["asdkfvjn", { Owner: "", OrganizationID: "" }], - ["owner:me", { Owner: "me", OrganizationID: "" }], - ["owner:me owner:me2", { Owner: "me", OrganizationID: "" }], + [undefined, {}], + ["", {}], + ["asdkfvjn", { name: "asdkfvjn" }], + ["owner:me", { owner: "me" }], + ["owner:me owner:me2", { owner: "me" }], + ["me/dev", { owner: "me", name: "dev" }], ])(`query=%p, filter=%p`, (query, filter) => { expect(workspaceQueryToFilter(query)).toEqual(filter) }) diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index 8f75eddb01990..08b7f4fc58b43 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -196,11 +196,7 @@ export const isWorkspaceOn = (workspace: TypeGen.Workspace): boolean => { } export const workspaceQueryToFilter = (query?: string): TypeGen.WorkspaceFilter => { - const defaultFilter: TypeGen.WorkspaceFilter = { - Owner: "", - OrganizationID: "", - } - + const defaultFilter: TypeGen.WorkspaceFilter = {} const preparedQuery = query?.replace(/ +/g, " ") if (!preparedQuery) { @@ -212,10 +208,20 @@ export const workspaceQueryToFilter = (query?: string): TypeGen.WorkspaceFilter const [key, val] = part.split(":") if (key === "owner") { return { - Owner: val, - OrganizationID: "", + owner: val, + } + } + + const [username, name] = part.split("/") + if (username && name) { + return { + owner: username, + name: name, } } + return { + name: part, + } } return defaultFilter From e47b6010e4f7393cdeac22e19a5538ea794aa1a2 Mon Sep 17 00:00:00 2001 From: Garrett Date: Thu, 2 Jun 2022 20:33:24 +0000 Subject: [PATCH 10/11] add backend --- coderd/workspaces.go | 19 ++++++------------- codersdk/workspaces.go | 11 +++-------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index b23e52868adf6..933a87c54a747 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -142,13 +142,9 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { filter := database.GetWorkspacesWithFilterParams{Deleted: false} if orgFilter != "" { orgID, err := uuid.Parse(orgFilter) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("organization_id must be a uuid: %s", err.Error()), - }) - return + if err == nil { + filter.OrganizationID = orgID } - filter.OrganizationID = orgID } if ownerFilter == "me" { filter.OwnerID = apiKey.UserID @@ -161,15 +157,12 @@ func (api *API) workspaces(rw http.ResponseWriter, r *http.Request) { Username: ownerFilter, Email: ownerFilter, }) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "owner must be a uuid or username", - }) - return + if err == nil { + filter.OwnerID = user.ID } - userID = user.ID + } else { + filter.OwnerID = userID } - filter.OwnerID = userID } if nameFilter != "" { filter.Name = nameFilter diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 4439612e1d783..2535644d695e7 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "net/http" - "strconv" "time" "github.com/google/uuid" @@ -199,11 +198,10 @@ func (c *Client) PutExtendWorkspace(ctx context.Context, id uuid.UUID, req PutEx } type WorkspaceFilter struct { - OrganizationID uuid.UUID + OrganizationID uuid.UUID `json:"organization_id,omitempty"` // Owner can be a user_id (uuid), "me", or a username - Owner string - Name string - Deleted bool + Owner string `json:"owner,omitempty"` + Name string `json:"name,omitempty"` } // asRequestOption returns a function that can be used in (*Client).Request. @@ -220,9 +218,6 @@ func (f WorkspaceFilter) asRequestOption() requestOption { if f.Name != "" { q.Set("name", f.Name) } - if f.Deleted { - q.Set("deleted", strconv.FormatBool(f.Deleted)) - } r.URL.RawQuery = q.Encode() } } From 114c9bce9d77a12a4fa2cbe1a13494f2f5b6aefd Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Fri, 3 Jun 2022 01:03:12 +0000 Subject: [PATCH 11/11] some styling ideas --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 104 +++++++++++++----- 1 file changed, 79 insertions(+), 25 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 37b10589dbc2b..00ae3bff7bcf3 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,10 +1,13 @@ import Button from "@material-ui/core/Button" +import Fade from "@material-ui/core/Fade" +import InputAdornment from "@material-ui/core/InputAdornment" import Link from "@material-ui/core/Link" import Menu from "@material-ui/core/Menu" import MenuItem from "@material-ui/core/MenuItem" import { makeStyles } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" +import SearchIcon from "@material-ui/icons/Search" import { useMachine } from "@xstate/react" import { FormikErrors, useFormik } from "formik" import { FC, useState } from "react" @@ -70,45 +73,96 @@ const WorkspacesPage: FC = () => { } return ( - <> - -
- - - + + + + + ), + }} + /> + + + {Language.yourWorkspacesButton} {Language.allWorkspacesButton} - -
- - - - - - -
- - -
- + + + + + + + + ) } const useStyles = makeStyles((theme) => ({ - actions: { + workspacesHeaderContainer: { marginTop: theme.spacing(3), marginBottom: theme.spacing(3), - display: "flex", justifyContent: "space-between", - height: theme.spacing(6), + }, + filterColumn: { + width: "60%", + cursor: "text", + }, + filterContainer: { + border: `1px solid ${theme.palette.divider}`, + borderRadius: "6px", + }, + filterForm: { + width: "100%", + }, + buttonRoot: { + border: "none", + borderRight: `1px solid ${theme.palette.divider}`, + borderRadius: "6px 0px 0px 6px", + }, + textFieldRoot: { + margin: "0px", + "& fieldset": { + border: "none", + }, }, }))