From ad65d06213d53f137f696c8c04188a536c9b17b9 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 22 Sep 2022 18:20:02 +0000 Subject: [PATCH 01/15] Refactor workspaces xservice --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 16 +- .../workspaces/workspacesXService.ts | 177 ++++++++++-------- 2 files changed, 98 insertions(+), 95 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 037549f92c603..41e122cb617ad 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -1,5 +1,5 @@ import { useMachine } from "@xstate/react" -import { FC, useEffect } from "react" +import { FC } from "react" import { Helmet } from "react-helmet-async" import { useSearchParams } from "react-router-dom" import { workspaceFilterQuery } from "util/filters" @@ -9,25 +9,15 @@ import { WorkspacesPageView } from "./WorkspacesPageView" const WorkspacesPage: FC = () => { const [searchParams, setSearchParams] = useSearchParams() - const filter = searchParams.get("filter") - const defaultFilter = filter ?? workspaceFilterQuery.me + const filter = searchParams.get("filter") ?? workspaceFilterQuery.me const [workspacesState, send] = useMachine(workspacesMachine, { context: { - filter: defaultFilter, + filter, }, }) const { workspaceRefs } = workspacesState.context - // On page load, populate the table with workspaces - useEffect(() => { - send({ - type: "GET_WORKSPACES", - query: defaultFilter, - }) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - return ( <> diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index cc2a690cfa4e7..a104b499a6b46 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -192,64 +192,87 @@ interface WorkspacesContext { } type WorkspacesEvent = - | { type: "GET_WORKSPACES"; query: string } + | { type: "GET_WORKSPACES"; query?: string } | { type: "UPDATE_VERSION"; workspaceId: string } -export const workspacesMachine = createMachine( +export const workspacesMachine = +/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGM4BlAFz1LADoZTSBLAOygHUNt8jYBiCVR6kwBuqTNVpssuQnESgcqWPQb85IAB6IAbABYdVAIwB2AwE4ATAFYAzAA5TO+zoA0IAJ6ID1o1VN-Ttra6Olrm5gAMtgC+Ua5oUpwk5JQ0YHRMrOzSXNxg6OgYVDgANhQAZhgAtqmkkhwy8EggCkoqjGqaCMGGJhY2Tk6uHgjWOua+-lqBpuF+lg4xcVmJsGQU1ACuOBAUGXXZYABKYGU8fAJUwqKb2+v7icenai3K9KpNnd3GZlZ2DoPuRBjWwTPxaSxhHQGSwQxYgeL1LhrFLIPDKAAqqEe6DgAAt7g1uOpYMlqHgypR0AAKSzhOkASm4CIOq1JVFRGKxJxxsHxywaz0Ur3eoE6RhMVHBWms5ls1nCOiMplGQ0Q9lBc1Co0cszhzJWyLA3AA4gBRdEAfRYAHlDgBpYgABQAggBhU3EQWtN7tD6ISxGVUIMz6fwBcJacIGMILWLw-lI0ncACqjoAIs70aaLQA1U2HYgASWtADkvcLfaL-dGqDpTFoDHorOKjCEg5YtKYNVK7NDrNYYnHGKgILImvqGobLhBimBy20Op5IlQ5fNrAFwUZwt5zEHzDMNUZO6M7EEtHqE0l1jUGMwCVx5z7FwhzNLa62tA2xgrbBFLEGDF0DUZjpOsoyMSwLwSSc2S2HZb0yaCiEeRp5CFBc-RfPQegMewwk7aUjHMawgx0BVuwMRt+0A8woMRK8UTRUhMWxPF7zHNDvRFDREAsEEGyIpVwghIwTwAqMVzDeZP0VSxozollDUfbjOn7dtgLDTSB0HIA */ +createMachine( { - predictableActionArguments: true, - tsTypes: {} as import("./workspacesXService.typegen").Typegen1, - schema: { - context: {} as WorkspacesContext, - events: {} as WorkspacesEvent, - services: {} as { - getWorkspaces: { - data: TypesGen.Workspace[] + tsTypes: {} as import("./workspacesXService.typegen").Typegen1, + schema: { + context: {} as WorkspacesContext, + events: {} as WorkspacesEvent, + services: {} as { + getWorkspaces: { + data: TypesGen.Workspace[] + } + updateWorkspaceRefs: { + data: { + refsToKeep: WorkspaceItemMachineRef[] + newWorkspaces: TypesGen.Workspace[] } - }, + } }, - id: "workspacesState", - on: { - GET_WORKSPACES: { - actions: "assignFilter", - target: "gettingWorkspaces", - }, - UPDATE_VERSION: { - actions: "triggerUpdateVersion", + }, + predictableActionArguments: true, + id: "workspacesState", + on: { + GET_WORKSPACES: { + actions: "assignFilter", + target: ".gettingWorkspaces", + internal: false, + }, + UPDATE_VERSION: { + actions: "triggerUpdateVersion", + }, + }, + initial: "gettingWorkspaces", + states: { + gettingWorkspaces: { + entry: "clearGetWorkspacesError", + invoke: { + src: "getWorkspaces", + id: "getWorkspaces", + onDone: [ + { + actions: "assignWorkspaceRefs", + cond: "isEmpty", + target: "waitToRefreshWorkspaces", + }, + { + target: "updatingWorkspaceRefs", + }, + ], + onError: [ + { + actions: "assignGetWorkspacesError", + target: "waitToRefreshWorkspaces", + }, + ], }, }, - initial: "idle", - states: { - idle: {}, - gettingWorkspaces: { - entry: "clearGetWorkspacesError", - invoke: { - src: "getWorkspaces", - id: "getWorkspaces", - onDone: [ - { - target: "waitToRefreshWorkspaces", - actions: ["assignWorkspaceRefs"], - cond: "isEmpty", - }, - { - target: "waitToRefreshWorkspaces", - actions: ["updateWorkspaceRefs"], - }, - ], - onError: { + updatingWorkspaceRefs: { + invoke: { + src: "updateWorkspaceRefs", + id: "updateWorkspaceRefs", + onDone: [ + { + actions: "assignUpdatedWorkspaceRefs", target: "waitToRefreshWorkspaces", - actions: ["assignGetWorkspacesError"], }, - }, + ], }, - waitToRefreshWorkspaces: { - after: { - 5000: "gettingWorkspaces", + }, + waitToRefreshWorkspaces: { + after: { + "5000": { + target: "gettingWorkspaces", }, }, }, }, +}, { guards: { isEmpty: (context) => !context.workspaceRefs, @@ -262,7 +285,7 @@ export const workspacesMachine = createMachine( }), }), assignFilter: assign({ - filter: (_, event) => event.query, + filter: (context, event) => event.query ?? context.filter, }), assignGetWorkspacesError: assign({ getWorkspacesError: (_, event) => event.data, @@ -277,48 +300,38 @@ export const workspacesMachine = createMachine( workspaceRef.send("UPDATE_VERSION") }, - // Opened discussion on XState https://github.com/statelyai/xstate/discussions/3406 - updateWorkspaceRefs: assign({ - workspaceRefs: (context, event) => { - let workspaceRefs = context.workspaceRefs - - if (!workspaceRefs) { - throw new Error("No workspaces loaded.") - } - - // Update the existent workspaces or create the new ones - for (const data of event.data) { - const ref = workspaceRefs.find((ref) => ref.id === data.id) - - if (!ref) { - workspaceRefs.push(spawn(workspaceItemMachine.withContext({ data }), data.id)) - } else { - ref.send({ type: "UPDATE_DATA", data }) - } - } - - // Remove workspaces that were deleted - for (const ref of workspaceRefs) { - const refData = event.data.find((workspaceData) => workspaceData.id === ref.id) - - // If there is no refData, it is because the workspace was deleted - if (!refData) { - // Stop the actor before remove it from the array - if (ref.stop) { - ref.stop() - } - - // Remove ref from the array - workspaceRefs = workspaceRefs.filter((oldRef) => oldRef.id !== ref.id) - } - } - - return workspaceRefs + assignUpdatedWorkspaceRefs: assign({ + workspaceRefs: (_, event) => { + const newWorkspaceRefs = event.data.newWorkspaces.map((workspace) => + spawn(workspaceItemMachine.withContext({ data: workspace }), workspace.id), + ) + return event.data.refsToKeep.concat(newWorkspaceRefs) }, }), }, services: { getWorkspaces: (context) => API.getWorkspaces(queryToFilter(context.filter)), + updateWorkspaceRefs: (context, event) => { + const refsToKeep: WorkspaceItemMachineRef[] = [] + context.workspaceRefs?.forEach((ref) => { + const matchingWorkspace = event.data.find((workspace) => ref.id === workspace.id) + if (matchingWorkspace) { + ref.send({ type: "UPDATE_DATA", data: matchingWorkspace }) + refsToKeep.push(ref) + } else { + ref.stop && ref.stop() + } + }) + + const newWorkspaces = event.data.filter( + (workspace) => !context.workspaceRefs?.find((ref) => ref.id === workspace.id), + ) + + return Promise.resolve({ + refsToKeep, + newWorkspaces, + }) + }, }, }, ) From 64b6ad02d53ced037632b36ee166d00989c60205 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 22 Sep 2022 18:32:24 +0000 Subject: [PATCH 02/15] Remove layout comment --- site/src/xServices/workspaces/workspacesXService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index a104b499a6b46..1122db2966814 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -196,7 +196,6 @@ type WorkspacesEvent = | { type: "UPDATE_VERSION"; workspaceId: string } export const workspacesMachine = -/** @xstate-layout N4IgpgJg5mDOIC5QHcD2AnA1rADgQwGM4BlAFz1LADoZTSBLAOygHUNt8jYBiCVR6kwBuqTNVpssuQnESgcqWPQb85IAB6IAbABYdVAIwB2AwE4ATAFYAzAA5TO+zoA0IAJ6ID1o1VN-Ttra6Olrm5gAMtgC+Ua5oUpwk5JQ0YHRMrOzSXNxg6OgYVDgANhQAZhgAtqmkkhwy8EggCkoqjGqaCMGGJhY2Tk6uHgjWOua+-lqBpuF+lg4xcVmJsGQU1ACuOBAUGXXZYABKYGU8fAJUwqKb2+v7icenai3K9KpNnd3GZlZ2DoPuRBjWwTPxaSxhHQGSwQxYgeL1LhrFLIPDKAAqqEe6DgAAt7g1uOpYMlqHgypR0AAKSzhOkASm4CIOq1JVFRGKxJxxsHxywaz0Ur3eoE6RhMVHBWms5ls1nCOiMplGQ0Q9lBc1Co0cszhzJWyLA3AA4gBRdEAfRYAHlDgBpYgABQAggBhU3EQWtN7tD6ISxGVUIMz6fwBcJacIGMILWLw-lI0ncACqjoAIs70aaLQA1U2HYgASWtADkvcLfaL-dGqDpTFoDHorOKjCEg5YtKYNVK7NDrNYYnHGKgILImvqGobLhBimBy20Op5IlQ5fNrAFwUZwt5zEHzDMNUZO6M7EEtHqE0l1jUGMwCVx5z7FwhzNLa62tA2xgrbBFLEGDF0DUZjpOsoyMSwLwSSc2S2HZb0yaCiEeRp5CFBc-RfPQegMewwk7aUjHMawgx0BVuwMRt+0A8woMRK8UTRUhMWxPF7zHNDvRFDREAsEEGyIpVwghIwTwAqMVzDeZP0VSxozollDUfbjOn7dtgLDTSB0HIA */ createMachine( { tsTypes: {} as import("./workspacesXService.typegen").Typegen1, From 0b061775fb0f7b42062290fff936822f10763cdf Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 22 Sep 2022 18:44:50 +0000 Subject: [PATCH 03/15] Format --- .../workspaces/workspacesXService.ts | 133 +++++++++--------- 1 file changed, 66 insertions(+), 67 deletions(-) diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index 1122db2966814..36ee1918768f3 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -195,83 +195,82 @@ type WorkspacesEvent = | { type: "GET_WORKSPACES"; query?: string } | { type: "UPDATE_VERSION"; workspaceId: string } -export const workspacesMachine = -createMachine( +export const workspacesMachine = createMachine( { - tsTypes: {} as import("./workspacesXService.typegen").Typegen1, - schema: { - context: {} as WorkspacesContext, - events: {} as WorkspacesEvent, - services: {} as { - getWorkspaces: { - data: TypesGen.Workspace[] - } - updateWorkspaceRefs: { - data: { - refsToKeep: WorkspaceItemMachineRef[] - newWorkspaces: TypesGen.Workspace[] + tsTypes: {} as import("./workspacesXService.typegen").Typegen1, + schema: { + context: {} as WorkspacesContext, + events: {} as WorkspacesEvent, + services: {} as { + getWorkspaces: { + data: TypesGen.Workspace[] + } + updateWorkspaceRefs: { + data: { + refsToKeep: WorkspaceItemMachineRef[] + newWorkspaces: TypesGen.Workspace[] + } } - } - }, - }, - predictableActionArguments: true, - id: "workspacesState", - on: { - GET_WORKSPACES: { - actions: "assignFilter", - target: ".gettingWorkspaces", - internal: false, - }, - UPDATE_VERSION: { - actions: "triggerUpdateVersion", - }, - }, - initial: "gettingWorkspaces", - states: { - gettingWorkspaces: { - entry: "clearGetWorkspacesError", - invoke: { - src: "getWorkspaces", - id: "getWorkspaces", - onDone: [ - { - actions: "assignWorkspaceRefs", - cond: "isEmpty", - target: "waitToRefreshWorkspaces", - }, - { - target: "updatingWorkspaceRefs", - }, - ], - onError: [ - { - actions: "assignGetWorkspacesError", - target: "waitToRefreshWorkspaces", - }, - ], }, }, - updatingWorkspaceRefs: { - invoke: { - src: "updateWorkspaceRefs", - id: "updateWorkspaceRefs", - onDone: [ - { - actions: "assignUpdatedWorkspaceRefs", - target: "waitToRefreshWorkspaces", - }, - ], + predictableActionArguments: true, + id: "workspacesState", + on: { + GET_WORKSPACES: { + actions: "assignFilter", + target: ".gettingWorkspaces", + internal: false, + }, + UPDATE_VERSION: { + actions: "triggerUpdateVersion", }, }, - waitToRefreshWorkspaces: { - after: { - "5000": { - target: "gettingWorkspaces", + initial: "gettingWorkspaces", + states: { + gettingWorkspaces: { + entry: "clearGetWorkspacesError", + invoke: { + src: "getWorkspaces", + id: "getWorkspaces", + onDone: [ + { + actions: "assignWorkspaceRefs", + cond: "isEmpty", + target: "waitToRefreshWorkspaces", + }, + { + target: "updatingWorkspaceRefs", + }, + ], + onError: [ + { + actions: "assignGetWorkspacesError", + target: "waitToRefreshWorkspaces", + }, + ], + }, + }, + updatingWorkspaceRefs: { + invoke: { + src: "updateWorkspaceRefs", + id: "updateWorkspaceRefs", + onDone: [ + { + actions: "assignUpdatedWorkspaceRefs", + target: "waitToRefreshWorkspaces", + }, + ], + }, + }, + waitToRefreshWorkspaces: { + after: { + "5000": { + target: "gettingWorkspaces", + }, }, }, }, }, -}, { guards: { isEmpty: (context) => !context.workspaceRefs, From ac75c717eb6af74d792753b721f457f597e07b58 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 27 Sep 2022 23:42:04 +0000 Subject: [PATCH 04/15] Add comments --- site/src/xServices/workspaces/workspacesXService.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index 36ee1918768f3..543e19b86fa13 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -314,9 +314,12 @@ export const workspacesMachine = createMachine( context.workspaceRefs?.forEach((ref) => { const matchingWorkspace = event.data.find((workspace) => ref.id === workspace.id) if (matchingWorkspace) { + // if a workspace machine reference describes a workspace that has not been deleted, + // update its data and mark it as a refToKeep ref.send({ type: "UPDATE_DATA", data: matchingWorkspace }) refsToKeep.push(ref) } else { + // if it describes a workspace that has been deleted, stop the machine ref.stop && ref.stop() } }) From d9d81f874521dae1e651bd23e79f6293444838dc Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 3 Oct 2022 18:46:21 +0000 Subject: [PATCH 05/15] Add running workspaces filter to frontend --- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 2 ++ site/src/util/filters.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index d127dafea3cdc..fc93c710cf0ec 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -18,6 +18,7 @@ export const Language = { pageTitle: "Workspaces", yourWorkspacesButton: "Your workspaces", allWorkspacesButton: "All workspaces", + runningWorkspacesButton: "Running workspaces", createANewWorkspace: `Create a new workspace from a `, template: "Template", } @@ -38,6 +39,7 @@ export const WorkspacesPageView: FC Date: Tue, 4 Oct 2022 21:27:59 +0000 Subject: [PATCH 06/15] Start on backend - add status to filter --- coderd/workspaces.go | 1 + codersdk/workspaces.go | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 0cb72cdcec65e..c524a169d0ee1 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1155,6 +1155,7 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersd OwnerUsername: parser.String(searchParams, "", "owner"), TemplateName: parser.String(searchParams, "", "template"), Name: parser.String(searchParams, "", "name"), + Status: parser.String(searchParams, "", "status"), } return filter, parser.Errors diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index 2757a9a085c0d..6889d4bc3b9fa 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -264,6 +264,8 @@ type WorkspaceFilter struct { Template string `json:"template,omitempty" typescript:"-"` // Name will return partial matches Name string `json:"name,omitempty" typescript:"-"` + // Status is a workspace status, which is really the status of the latest build + Status string `json:"status,omitempty" typescript:"-"` // FilterQuery supports a raw filter query string FilterQuery string `json:"q,omitempty"` } @@ -284,6 +286,9 @@ func (f WorkspaceFilter) asRequestOption() RequestOption { if f.Template != "" { params = append(params, fmt.Sprintf("template:%q", f.Template)) } + if f.Status != "" { + params = append(params, fmt.Sprintf("status:%q", f.Status)) + } if f.FilterQuery != "" { // If custom stuff is added, just add it on here. params = append(params, f.FilterQuery) From 8500dbfaf798cd8c0fa1a7db10c01f022df789db Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 5 Oct 2022 15:17:49 +0000 Subject: [PATCH 07/15] Update sql and add test - wip --- coderd/database/queries/workspaces.sql | 25 +++++++++++--- coderd/workspaces_test.go | 47 ++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 8e9e0b60902eb..2b827ec521e71 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -10,19 +10,36 @@ LIMIT -- name: GetWorkspaces :many SELECT - * + workspaces.* FROM workspaces +LEFT JOIN LATERAL ( + SELECT + * + FROM + workspace_builds + WHERE + workspace_builds.workspace_id = workspaces.id + ORDER BY + build_number DESC + LIMIT + 1 +) latest_build ON TRUE WHERE -- Optionally include deleted workspaces workspaces.deleted = @deleted + AND CASE + WHEN @status :: text != '' THEN + latest_build.transition = convertStatus(@status) + ELSE true + END -- Filter by owner_id AND CASE WHEN @owner_id :: uuid != '00000000-00000000-00000000-00000000' THEN owner_id = @owner_id ELSE true END - -- Filter by owner_name + -- Filter by owner_name AND CASE WHEN @owner_username :: text != '' THEN owner_id = (SELECT id FROM users WHERE lower(username) = lower(@owner_username)) @@ -30,7 +47,7 @@ WHERE END -- Filter by template_name -- There can be more than 1 template with the same name across organizations. - -- Use the organization filter to restrict to 1 org if needed. + -- Use the organization filter to restrict to 1 org if needed. AND CASE WHEN @template_name :: text != '' THEN template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower(@template_name)) @@ -45,7 +62,7 @@ WHERE -- Filter by name, matching on substring AND CASE WHEN @name :: text != '' THEN - name ILIKE '%' || @name || '%' + name ILIKE '%' || @name || '%' ELSE true END ; diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index d295a48875652..bdd018f7318dd 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -711,6 +711,53 @@ func TestWorkspaceFilterManual(t *testing.T) { require.Len(t, ws, 1) require.Equal(t, workspace.ID, ws[0].ID) }) + t.Run("Status", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace1 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + workspace2 := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID) + + // wait for workspaces to be "running" + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace1.LatestBuild.ID) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace2.LatestBuild.ID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // filter finds both running workspaces + ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Status: "running", + }) + require.NoError(t, err) + require.Len(t, ws, 2) + + // stop workspace1 + build1 := coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStop) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, build1.ID) + + // filter finds one running workspace + ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Status: "running", + }) + require.NoError(t, err) + require.Len(t, ws, 1) + require.Equal(t, workspace2.ID, ws[0].ID) + + // stop workspace2 + build2 := coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop) + _ = coderdtest.AwaitWorkspaceBuildJob(t, client, build2.ID) + + // filter finds no running workspaces + ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ + Status: "running", + }) + require.NoError(t, err) + require.Len(t, ws, 0) + }) t.Run("FilterQuery", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) From 3716cef17bd97801f2a78c1f4ae771b343883fd2 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 5 Oct 2022 21:38:53 +0000 Subject: [PATCH 08/15] Attempt to unconvert status for easier querying --- coderd/workspaces.go | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index c524a169d0ee1..ce82536ff60f0 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1034,6 +1034,20 @@ func convertStatus(build codersdk.WorkspaceBuild) codersdk.WorkspaceStatus { return codersdk.WorkspaceStatusFailed } +func unconvertStatus(status codersdk.WorkspaceStatus) (codersdk.WorkspaceTransition, codersdk.ProvisionerJobStatus) { + switch status { + case codersdk.WorkspaceStatusDeleted: + return codersdk.WorkspaceTransitionDelete, codersdk.ProvisionerJobSucceeded + case codersdk.WorkspaceStatusRunning + return codersdk.WorkspaceTransitionStart, codersdk.ProvisionerJobSucceeded + case codersdk.WorkspaceStatusStopped + return codersdk.WorkspaceTransitionStop, codersdk.ProvisionerJobSucceeded + } + + // it's either not a valid status, or one we can't reverse engineer with certainty + return "" "" +} + func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 { if !i.Valid { return nil @@ -1150,12 +1164,14 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersd // Using the query param parser here just returns consistent errors with // other parsing. parser := httpapi.NewQueryParamParser() + transition, job_status := unconvertStatus(parser.String(searchParams, "", "status")) filter := database.GetWorkspacesParams{ Deleted: false, OwnerUsername: parser.String(searchParams, "", "owner"), TemplateName: parser.String(searchParams, "", "template"), Name: parser.String(searchParams, "", "name"), - Status: parser.String(searchParams, "", "status"), + Transition: transition, + JobStatus: job_status, } return filter, parser.Errors From 0cc1024887dbd23c1497e49b42eeb50aed19e9dc Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Wed, 5 Oct 2022 21:39:41 +0000 Subject: [PATCH 09/15] Fix syntax --- coderd/workspaces.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/workspaces.go b/coderd/workspaces.go index ce82536ff60f0..878d8c2b7689a 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1038,14 +1038,14 @@ func unconvertStatus(status codersdk.WorkspaceStatus) (codersdk.WorkspaceTransit switch status { case codersdk.WorkspaceStatusDeleted: return codersdk.WorkspaceTransitionDelete, codersdk.ProvisionerJobSucceeded - case codersdk.WorkspaceStatusRunning + case codersdk.WorkspaceStatusRunning: return codersdk.WorkspaceTransitionStart, codersdk.ProvisionerJobSucceeded - case codersdk.WorkspaceStatusStopped + case codersdk.WorkspaceStatusStopped: return codersdk.WorkspaceTransitionStop, codersdk.ProvisionerJobSucceeded } // it's either not a valid status, or one we can't reverse engineer with certainty - return "" "" + return "", "" } func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 { From 384115328ace5f3ad2e8f51feaad3d029a983de2 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Thu, 6 Oct 2022 18:47:14 +0000 Subject: [PATCH 10/15] Join jobs table, untested --- coderd/database/queries/workspaces.sql | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 2b827ec521e71..7762af92b889e 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -16,8 +16,13 @@ FROM LEFT JOIN LATERAL ( SELECT * - FROM - workspace_builds + FROM ( + workspace_builds + LEFT JOIN + provisioner_jobs + ON + provisioner_jobs.id = workspace_builds.job_id + ) WHERE workspace_builds.workspace_id = workspaces.id ORDER BY @@ -30,7 +35,10 @@ WHERE workspaces.deleted = @deleted AND CASE WHEN @status :: text != '' THEN - latest_build.transition = convertStatus(@status) + CASE + WHEN latest_build.transition = THEN + latest_build.transition = @status + END ELSE true END -- Filter by owner_id From 1a2cc6d072034b23df483459a0c90ec8500ae7b3 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Thu, 6 Oct 2022 22:38:38 +0000 Subject: [PATCH 11/15] sql --- coderd/database/queries.sql.go | 107 +++++++++++++++++++++---- coderd/database/queries/workspaces.sql | 76 +++++++++++++++--- coderd/provisionerjobs.go | 3 + coderd/workspaces.go | 18 +---- coderd/workspaces_test.go | 17 ++-- codersdk/provisionerdaemons.go | 1 + site/src/api/typesGenerated.ts | 1 + 7 files changed, 169 insertions(+), 54 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3f846e7bebf69..28244c7299669 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5033,48 +5033,124 @@ func (q *sqlQuerier) GetWorkspaceOwnerCountsByTemplateIDs(ctx context.Context, i const getWorkspaces = `-- name: GetWorkspaces :many SELECT - id, created_at, updated_at, owner_id, organization_id, template_id, deleted, name, autostart_schedule, ttl, last_used_at + workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at FROM - workspaces + workspaces +LEFT JOIN LATERAL ( + SELECT + workspace_builds.transition, + provisioner_jobs.started_at, + provisioner_jobs.updated_at, + provisioner_jobs.canceled_at, + provisioner_jobs.completed_at, + provisioner_jobs.error + FROM + workspace_builds + LEFT JOIN + provisioner_jobs + ON + provisioner_jobs.id = workspace_builds.job_id + WHERE + workspace_builds.workspace_id = workspaces.id + ORDER BY + build_number DESC + LIMIT + 1 +) latest_build ON TRUE WHERE - -- Optionally include deleted workspaces + -- Optionally include deleted workspaces workspaces.deleted = $1 + AND CASE + WHEN $2 :: text != '' THEN + CASE + WHEN $2 = 'pending' THEN + latest_build.started_at IS NULL + WHEN $2 = 'starting' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'start'::workspace_transition + WHEN $2 = 'running' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error IS NULL AND + latest_build.transition = 'start'::workspace_transition + WHEN $2 = 'stopping' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'stop'::workspace_transition + WHEN $2 = 'stopped' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error IS NULL AND + latest_build.transition = 'stop'::workspace_transition + WHEN $2 = 'failed' THEN + (latest_build.canceled_at IS NOT NULL AND + latest_build.error IS NOT NULL) OR + (latest_build.completed_at IS NOT NULL AND + latest_build.error IS NOT NULL) + WHEN $2 = 'canceling' THEN + latest_build.canceled_at IS NOT NULL AND + latest_build.completed_at IS NULL + WHEN $2 = 'canceled' THEN + latest_build.canceled_at IS NOT NULL AND + latest_build.completed_at IS NOT NULL + WHEN $2 = 'deleted' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'delete'::workspace_transition + WHEN $2 = 'deleting' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error = '' AND + latest_build.transition = 'delete'::workspace_transition + ELSE + true + END + ELSE true + END -- Filter by owner_id AND CASE - WHEN $2 :: uuid != '00000000-00000000-00000000-00000000' THEN - owner_id = $2 + WHEN $3 :: uuid != '00000000-00000000-00000000-00000000' THEN + owner_id = $3 ELSE true END - -- Filter by owner_name + -- Filter by owner_name AND CASE - WHEN $3 :: text != '' THEN - owner_id = (SELECT id FROM users WHERE lower(username) = lower($3)) + WHEN $4 :: text != '' THEN + owner_id = (SELECT id FROM users WHERE lower(username) = lower($4)) ELSE true END -- Filter by template_name -- There can be more than 1 template with the same name across organizations. - -- Use the organization filter to restrict to 1 org if needed. + -- Use the organization filter to restrict to 1 org if needed. AND CASE - WHEN $4 :: text != '' THEN - template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($4)) + WHEN $5 :: text != '' THEN + template_id = ANY(SELECT id FROM templates WHERE lower(name) = lower($5)) ELSE true END -- Filter by template_ids AND CASE - WHEN array_length($5 :: uuid[], 1) > 0 THEN - template_id = ANY($5) + WHEN array_length($6 :: uuid[], 1) > 0 THEN + template_id = ANY($6) ELSE true END -- Filter by name, matching on substring AND CASE - WHEN $6 :: text != '' THEN - name ILIKE '%' || $6 || '%' + WHEN $7 :: text != '' THEN + name ILIKE '%' || $7 || '%' ELSE true END ` type GetWorkspacesParams struct { Deleted bool `db:"deleted" json:"deleted"` + Status string `db:"status" json:"status"` OwnerID uuid.UUID `db:"owner_id" json:"owner_id"` OwnerUsername string `db:"owner_username" json:"owner_username"` TemplateName string `db:"template_name" json:"template_name"` @@ -5085,6 +5161,7 @@ type GetWorkspacesParams struct { func (q *sqlQuerier) GetWorkspaces(ctx context.Context, arg GetWorkspacesParams) ([]Workspace, error) { rows, err := q.db.QueryContext(ctx, getWorkspaces, arg.Deleted, + arg.Status, arg.OwnerID, arg.OwnerUsername, arg.TemplateName, diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 7762af92b889e..b0c13f88aa280 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -10,19 +10,23 @@ LIMIT -- name: GetWorkspaces :many SELECT - workspaces.* + workspaces.* FROM - workspaces + workspaces LEFT JOIN LATERAL ( SELECT - * - FROM ( - workspace_builds - LEFT JOIN - provisioner_jobs - ON - provisioner_jobs.id = workspace_builds.job_id - ) + workspace_builds.transition, + provisioner_jobs.started_at, + provisioner_jobs.updated_at, + provisioner_jobs.canceled_at, + provisioner_jobs.completed_at, + provisioner_jobs.error + FROM + workspace_builds + LEFT JOIN + provisioner_jobs + ON + provisioner_jobs.id = workspace_builds.job_id WHERE workspace_builds.workspace_id = workspaces.id ORDER BY @@ -31,13 +35,59 @@ LEFT JOIN LATERAL ( 1 ) latest_build ON TRUE WHERE - -- Optionally include deleted workspaces + -- Optionally include deleted workspaces workspaces.deleted = @deleted AND CASE WHEN @status :: text != '' THEN CASE - WHEN latest_build.transition = THEN - latest_build.transition = @status + WHEN @status = 'pending' THEN + latest_build.started_at IS NULL + WHEN @status = 'starting' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'start'::workspace_transition + WHEN @status = 'running' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error IS NULL AND + latest_build.transition = 'start'::workspace_transition + WHEN @status = 'stopping' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'stop'::workspace_transition + WHEN @status = 'stopped' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error IS NULL AND + latest_build.transition = 'stop'::workspace_transition + WHEN @status = 'failed' THEN + (latest_build.canceled_at IS NOT NULL AND + latest_build.error IS NOT NULL) OR + (latest_build.completed_at IS NOT NULL AND + latest_build.error IS NOT NULL) + WHEN @status = 'canceling' THEN + latest_build.canceled_at IS NOT NULL AND + latest_build.completed_at IS NULL + WHEN @status = 'canceled' THEN + latest_build.canceled_at IS NOT NULL AND + latest_build.completed_at IS NOT NULL + WHEN @status = 'deleted' THEN + latest_build.started_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.completed_at IS NOT NULL AND + latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND + latest_build.transition = 'delete'::workspace_transition + WHEN @status = 'deleting' THEN + latest_build.completed_at IS NOT NULL AND + latest_build.canceled_at IS NULL AND + latest_build.error = '' AND + latest_build.transition = 'delete'::workspace_transition + ELSE + true END ELSE true END diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 63a512f78bc99..6cbea8877ef76 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -327,6 +327,9 @@ func convertProvisionerJob(provisionerJob database.ProvisionerJob) codersdk.Prov if provisionerJob.CompletedAt.Valid { job.CompletedAt = &provisionerJob.CompletedAt.Time } + if provisionerJob.CanceledAt.Valid { + job.CanceledAt = &provisionerJob.CanceledAt.Time + } if provisionerJob.WorkerID.Valid { job.WorkerID = &provisionerJob.WorkerID.UUID } diff --git a/coderd/workspaces.go b/coderd/workspaces.go index 878d8c2b7689a..c524a169d0ee1 100644 --- a/coderd/workspaces.go +++ b/coderd/workspaces.go @@ -1034,20 +1034,6 @@ func convertStatus(build codersdk.WorkspaceBuild) codersdk.WorkspaceStatus { return codersdk.WorkspaceStatusFailed } -func unconvertStatus(status codersdk.WorkspaceStatus) (codersdk.WorkspaceTransition, codersdk.ProvisionerJobStatus) { - switch status { - case codersdk.WorkspaceStatusDeleted: - return codersdk.WorkspaceTransitionDelete, codersdk.ProvisionerJobSucceeded - case codersdk.WorkspaceStatusRunning: - return codersdk.WorkspaceTransitionStart, codersdk.ProvisionerJobSucceeded - case codersdk.WorkspaceStatusStopped: - return codersdk.WorkspaceTransitionStop, codersdk.ProvisionerJobSucceeded - } - - // it's either not a valid status, or one we can't reverse engineer with certainty - return "", "" -} - func convertWorkspaceTTLMillis(i sql.NullInt64) *int64 { if !i.Valid { return nil @@ -1164,14 +1150,12 @@ func workspaceSearchQuery(query string) (database.GetWorkspacesParams, []codersd // Using the query param parser here just returns consistent errors with // other parsing. parser := httpapi.NewQueryParamParser() - transition, job_status := unconvertStatus(parser.String(searchParams, "", "status")) filter := database.GetWorkspacesParams{ Deleted: false, OwnerUsername: parser.String(searchParams, "", "owner"), TemplateName: parser.String(searchParams, "", "template"), Name: parser.String(searchParams, "", "name"), - Transition: transition, - JobStatus: job_status, + Status: parser.String(searchParams, "", "status"), } return filter, parser.Errors diff --git a/coderd/workspaces_test.go b/coderd/workspaces_test.go index bdd018f7318dd..4e99174b8d96c 100644 --- a/coderd/workspaces_test.go +++ b/coderd/workspaces_test.go @@ -713,6 +713,7 @@ func TestWorkspaceFilterManual(t *testing.T) { }) t.Run("Status", func(t *testing.T) { t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) user := coderdtest.CreateFirstUser(t, client) version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil) @@ -729,34 +730,32 @@ func TestWorkspaceFilterManual(t *testing.T) { defer cancel() // filter finds both running workspaces - ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ - Status: "running", - }) + ws1, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{}) require.NoError(t, err) - require.Len(t, ws, 2) + require.Len(t, ws1, 2) // stop workspace1 build1 := coderdtest.CreateWorkspaceBuild(t, client, workspace1, database.WorkspaceTransitionStop) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, build1.ID) // filter finds one running workspace - ws, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ + ws2, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ Status: "running", }) require.NoError(t, err) - require.Len(t, ws, 1) - require.Equal(t, workspace2.ID, ws[0].ID) + require.Len(t, ws2, 1) + require.Equal(t, workspace2.ID, ws2[0].ID) // stop workspace2 build2 := coderdtest.CreateWorkspaceBuild(t, client, workspace2, database.WorkspaceTransitionStop) _ = coderdtest.AwaitWorkspaceBuildJob(t, client, build2.ID) // filter finds no running workspaces - ws, err = client.Workspaces(ctx, codersdk.WorkspaceFilter{ + ws3, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{ Status: "running", }) require.NoError(t, err) - require.Len(t, ws, 0) + require.Len(t, ws3, 0) }) t.Run("FilterQuery", func(t *testing.T) { t.Parallel() diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index f8307b1adee1d..b2895bff8b5db 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -68,6 +68,7 @@ type ProvisionerJob struct { CreatedAt time.Time `json:"created_at"` StartedAt *time.Time `json:"started_at,omitempty"` CompletedAt *time.Time `json:"completed_at,omitempty"` + CanceledAt *time.Time `json:"canceled_at,omitempty"` Error string `json:"error,omitempty"` Status ProvisionerJobStatus `json:"status"` WorkerID *uuid.UUID `json:"worker_id,omitempty"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index f8f34b4eaa7e9..3b7cf0452c861 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -370,6 +370,7 @@ export interface ProvisionerJob { readonly created_at: string readonly started_at?: string readonly completed_at?: string + readonly canceled_at?: string readonly error?: string readonly status: ProvisionerJobStatus readonly worker_id?: string From b7f4c92712f2cae741034fbfc756900b64828ab5 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 10 Oct 2022 15:59:32 +0000 Subject: [PATCH 12/15] Add Status to GetAuthorizedWorkspaces --- coderd/database/custom_queries.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/database/custom_queries.go b/coderd/database/custom_queries.go index 219a6cdb13c7b..de5be7e7da759 100644 --- a/coderd/database/custom_queries.go +++ b/coderd/database/custom_queries.go @@ -22,6 +22,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.DefaultConfig())) rows, err := q.db.QueryContext(ctx, query, arg.Deleted, + arg.Status, arg.OwnerID, arg.OwnerUsername, arg.TemplateName, From 71be34986f70281d4e425e644bb83edbef258b7d Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Mon, 10 Oct 2022 18:17:09 +0000 Subject: [PATCH 13/15] Update job tests to have canceled time --- coderd/provisionerjobs_internal_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index a5a760757baf3..6ac466a57644a 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -185,7 +185,8 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { CompletedAt: invalidNullTimeMock, }, expected: codersdk.ProvisionerJob{ - Status: codersdk.ProvisionerJobCanceling, + CanceledAt: &validNullTimeMock.Time, + Status: codersdk.ProvisionerJobCanceling, }, }, { @@ -196,6 +197,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { Error: errorMock, }, expected: codersdk.ProvisionerJob{ + CanceledAt: &validNullTimeMock.Time, CompletedAt: &validNullTimeMock.Time, Status: codersdk.ProvisionerJobFailed, Error: errorMock.String, @@ -208,6 +210,7 @@ func TestConvertProvisionerJob_Unit(t *testing.T) { CompletedAt: validNullTimeMock, }, expected: codersdk.ProvisionerJob{ + CanceledAt: &validNullTimeMock.Time, CompletedAt: &validNullTimeMock.Time, Status: codersdk.ProvisionerJobCanceled, }, From ae7b62fd3a3135152d51e62a89c2692dba4a2b2c Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 10 Oct 2022 16:00:38 -0500 Subject: [PATCH 14/15] fmt --- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 5 ++++- .../xServices/workspaces/workspacesXService.ts | 15 +++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 4a73ddd22264b..097f24d732406 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -36,7 +36,10 @@ export const WorkspacesPageView: FC< const presetFilters = [ { query: workspaceFilterQuery.me, name: Language.yourWorkspacesButton }, { query: workspaceFilterQuery.all, name: Language.allWorkspacesButton }, - { query: workspaceFilterQuery.running, name: Language.runningWorkspacesButton }, + { + query: workspaceFilterQuery.running, + name: Language.runningWorkspacesButton, + }, ] return ( diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index e38479cc498d2..7b0844dddc152 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -321,18 +321,24 @@ export const workspacesMachine = createMachine( assignUpdatedWorkspaceRefs: assign({ workspaceRefs: (_, event) => { const newWorkspaceRefs = event.data.newWorkspaces.map((workspace) => - spawn(workspaceItemMachine.withContext({ data: workspace }), workspace.id), + spawn( + workspaceItemMachine.withContext({ data: workspace }), + workspace.id, + ), ) return event.data.refsToKeep.concat(newWorkspaceRefs) }, }), }, services: { - getWorkspaces: (context) => API.getWorkspaces(queryToFilter(context.filter)), + getWorkspaces: (context) => + API.getWorkspaces(queryToFilter(context.filter)), updateWorkspaceRefs: (context, event) => { const refsToKeep: WorkspaceItemMachineRef[] = [] context.workspaceRefs?.forEach((ref) => { - const matchingWorkspace = event.data.find((workspace) => ref.id === workspace.id) + const matchingWorkspace = event.data.find( + (workspace) => ref.id === workspace.id, + ) if (matchingWorkspace) { // if a workspace machine reference describes a workspace that has not been deleted, // update its data and mark it as a refToKeep @@ -345,7 +351,8 @@ export const workspacesMachine = createMachine( }) const newWorkspaces = event.data.filter( - (workspace) => !context.workspaceRefs?.find((ref) => ref.id === workspace.id), + (workspace) => + !context.workspaceRefs?.find((ref) => ref.id === workspace.id), ) return Promise.resolve({ From 0a9f66776851436ed26f6f2c55f8e739d7737aa4 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Mon, 10 Oct 2022 16:57:58 -0500 Subject: [PATCH 15/15] add status filter to database fake --- coderd/database/databasefake/databasefake.go | 104 ++++++++++++++++++- coderd/database/modelqueries.go | 1 + coderd/database/queries.sql.go | 15 ++- coderd/database/queries/workspaces.sql | 15 ++- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 1a2a919925ec2..5a652164d3a6f 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -553,7 +553,8 @@ func (q *fakeQuerier) GetWorkspaces(ctx context.Context, arg database.GetWorkspa return workspaces, err } -func (q *fakeQuerier) GetAuthorizedWorkspaces(_ context.Context, arg database.GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]database.Workspace, error) { +//nolint:gocyclo +func (q *fakeQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg database.GetWorkspacesParams, authorizedFilter rbac.AuthorizeFilter) ([]database.Workspace, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -562,18 +563,21 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(_ context.Context, arg database.Ge if arg.OwnerID != uuid.Nil && workspace.OwnerID != arg.OwnerID { continue } + if arg.OwnerUsername != "" { - owner, err := q.GetUserByID(context.Background(), workspace.OwnerID) + owner, err := q.GetUserByID(ctx, workspace.OwnerID) if err == nil && !strings.EqualFold(arg.OwnerUsername, owner.Username) { continue } } + if arg.TemplateName != "" { - template, err := q.GetTemplateByID(context.Background(), workspace.TemplateID) + template, err := q.GetTemplateByID(ctx, workspace.TemplateID) if err == nil && !strings.EqualFold(arg.TemplateName, template.Name) { continue } } + if !arg.Deleted && workspace.Deleted { continue } @@ -581,6 +585,96 @@ func (q *fakeQuerier) GetAuthorizedWorkspaces(_ context.Context, arg database.Ge if arg.Name != "" && !strings.Contains(strings.ToLower(workspace.Name), strings.ToLower(arg.Name)) { continue } + + if arg.Status != "" { + build, err := q.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest build: %w", err) + } + + job, err := q.GetProvisionerJobByID(ctx, build.JobID) + if err != nil { + return nil, xerrors.Errorf("get provisioner job: %w", err) + } + + switch arg.Status { + case "pending": + if !job.StartedAt.Valid { + continue + } + + case "starting": + if !job.StartedAt.Valid && + !job.CanceledAt.Valid && + job.CompletedAt.Valid && + time.Since(job.UpdatedAt) > 30*time.Second || + build.Transition != database.WorkspaceTransitionStart { + continue + } + + case "running": + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid || + build.Transition != database.WorkspaceTransitionStart { + continue + } + + case "stopping": + if !job.StartedAt.Valid && + !job.CanceledAt.Valid && + job.CompletedAt.Valid && + time.Since(job.UpdatedAt) > 30*time.Second || + build.Transition != database.WorkspaceTransitionStop { + continue + } + + case "stopped": + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid || + build.Transition != database.WorkspaceTransitionStop { + continue + } + + case "failed": + if (!job.CanceledAt.Valid && !job.Error.Valid) || + (!job.CompletedAt.Valid && !job.Error.Valid) { + continue + } + + case "canceling": + if !job.CanceledAt.Valid && job.CompletedAt.Valid { + continue + } + + case "canceled": + if !job.CanceledAt.Valid && !job.CompletedAt.Valid { + continue + } + + case "deleted": + if !job.StartedAt.Valid && + job.CanceledAt.Valid && + !job.CompletedAt.Valid && + time.Since(job.UpdatedAt) > 30*time.Second || + build.Transition != database.WorkspaceTransitionDelete { + continue + } + + case "deleting": + if !job.CompletedAt.Valid && + job.CanceledAt.Valid && + job.Error.Valid && + build.Transition != database.WorkspaceTransitionDelete { + continue + } + + default: + return nil, xerrors.Errorf("unknown workspace status in filter: %q", arg.Status) + } + } + if len(arg.TemplateIds) > 0 { match := false for _, id := range arg.TemplateIds { @@ -771,7 +865,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildByWorkspaceID(_ context.Context, wo var row database.WorkspaceBuild var buildNum int32 = -1 for _, workspaceBuild := range q.workspaceBuilds { - if workspaceBuild.WorkspaceID.String() == workspaceID.String() && workspaceBuild.BuildNumber > buildNum { + if workspaceBuild.WorkspaceID == workspaceID && workspaceBuild.BuildNumber > buildNum { row = workspaceBuild buildNum = workspaceBuild.BuildNumber } @@ -816,7 +910,7 @@ func (q *fakeQuerier) GetLatestWorkspaceBuildsByWorkspaceIDs(_ context.Context, buildNumbers := make(map[uuid.UUID]int32) for _, workspaceBuild := range q.workspaceBuilds { for _, id := range ids { - if id.String() == workspaceBuild.WorkspaceID.String() && workspaceBuild.BuildNumber > buildNumbers[id] { + if id == workspaceBuild.WorkspaceID && workspaceBuild.BuildNumber > buildNumbers[id] { builds[id] = workspaceBuild buildNumbers[id] = workspaceBuild.BuildNumber } diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 109f74db3852d..9fc6e37176dc1 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -168,6 +168,7 @@ func (q *sqlQuerier) GetAuthorizedWorkspaces(ctx context.Context, arg GetWorkspa query := fmt.Sprintf("-- name: GetAuthorizedWorkspaces :many\n%s AND %s", getWorkspaces, authorizedFilter.SQLString(rbac.NoACLConfig())) rows, err := q.db.QueryContext(ctx, query, arg.Deleted, + arg.Status, arg.OwnerID, arg.OwnerUsername, arg.TemplateName, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f0e64733c39c3..15a5080e309ae 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -5466,47 +5466,56 @@ WHERE WHEN $2 = 'starting' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND + latest_build.completed_at IS NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'start'::workspace_transition + WHEN $2 = 'running' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.error IS NULL AND latest_build.transition = 'start'::workspace_transition + WHEN $2 = 'stopping' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND + latest_build.completed_at IS NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'stop'::workspace_transition + WHEN $2 = 'stopped' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.error IS NULL AND latest_build.transition = 'stop'::workspace_transition + WHEN $2 = 'failed' THEN (latest_build.canceled_at IS NOT NULL AND latest_build.error IS NOT NULL) OR (latest_build.completed_at IS NOT NULL AND latest_build.error IS NOT NULL) + WHEN $2 = 'canceling' THEN latest_build.canceled_at IS NOT NULL AND latest_build.completed_at IS NULL + WHEN $2 = 'canceled' THEN latest_build.canceled_at IS NOT NULL AND latest_build.completed_at IS NOT NULL + WHEN $2 = 'deleted' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.completed_at IS NOT NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'delete'::workspace_transition + WHEN $2 = 'deleting' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.error = '' AND + latest_build.error IS NULL AND latest_build.transition = 'delete'::workspace_transition + ELSE true END diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index b0c13f88aa280..69a61375c2073 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -45,47 +45,56 @@ WHERE WHEN @status = 'starting' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND + latest_build.completed_at IS NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'start'::workspace_transition + WHEN @status = 'running' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.error IS NULL AND latest_build.transition = 'start'::workspace_transition + WHEN @status = 'stopping' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.completed_at IS NOT NULL AND + latest_build.completed_at IS NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'stop'::workspace_transition + WHEN @status = 'stopped' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.error IS NULL AND latest_build.transition = 'stop'::workspace_transition + WHEN @status = 'failed' THEN (latest_build.canceled_at IS NOT NULL AND latest_build.error IS NOT NULL) OR (latest_build.completed_at IS NOT NULL AND latest_build.error IS NOT NULL) + WHEN @status = 'canceling' THEN latest_build.canceled_at IS NOT NULL AND latest_build.completed_at IS NULL + WHEN @status = 'canceled' THEN latest_build.canceled_at IS NOT NULL AND latest_build.completed_at IS NOT NULL + WHEN @status = 'deleted' THEN latest_build.started_at IS NOT NULL AND latest_build.canceled_at IS NULL AND latest_build.completed_at IS NOT NULL AND latest_build.updated_at - INTERVAL '30 seconds' < NOW() AND latest_build.transition = 'delete'::workspace_transition + WHEN @status = 'deleting' THEN latest_build.completed_at IS NOT NULL AND latest_build.canceled_at IS NULL AND - latest_build.error = '' AND + latest_build.error IS NULL AND latest_build.transition = 'delete'::workspace_transition + ELSE true END