From 1848b884c6c27dfbcc73e19ba4cd142c9a9309d2 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Wed, 24 Jan 2024 11:52:39 +0000 Subject: [PATCH 1/9] feat(site): show workspace favorite status in list and detail --- site/src/api/api.ts | 8 ++++++++ site/src/pages/WorkspacePage/WorkspaceTopbar.tsx | 4 ++++ site/src/pages/WorkspacesPage/WorkspacesTable.tsx | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6814ad1b624a0..45bb0abcd567c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1688,3 +1688,11 @@ export const updateHealthSettings = async ( ); return response.data; }; + +export const putFavoriteWorkspace = async(workspaceID: string) => { + await axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); +} + +export const deleteFavoriteWorkspace = async(workspaceID: string) => { + await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); +} diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 88e2634ef4418..f3e1102c02999 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -5,6 +5,7 @@ import DeleteOutline from "@mui/icons-material/DeleteOutline"; import PersonOutline from "@mui/icons-material/PersonOutline"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; +import Star from "@mui/icons-material/Star"; import { useTheme } from "@emotion/react"; import { type FC } from "react"; import { useQuery } from "react-query"; @@ -148,6 +149,9 @@ export const WorkspaceTopbar: FC = ({ > {workspace.name} + {workspace.favorite && ( + + )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 3afdc8b257e30..bed5fb3f29b17 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -7,6 +7,7 @@ import TableRow from "@mui/material/TableRow"; import Checkbox from "@mui/material/Checkbox"; import Skeleton from "@mui/material/Skeleton"; import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import Star from "@mui/icons-material/Star"; import { useTheme } from "@emotion/react"; import { type FC, type ReactNode } from "react"; import { useNavigate } from "react-router-dom"; @@ -150,6 +151,9 @@ export const WorkspacesTable: FC = ({ alignItems="center" > {workspace.name} + {workspace.favorite && ( + + )} {workspace.outdated && ( Date: Fri, 26 Jan 2024 19:30:19 +0000 Subject: [PATCH 2/9] add a button to star the workspace --- site/src/api/queries/workspaces.ts | 20 +++++++++++ site/src/pages/WorkspacePage/Workspace.tsx | 3 ++ .../WorkspaceActions/Buttons.tsx | 23 +++++++++++++ .../WorkspaceActions/WorkspaceActions.tsx | 12 +++++++ .../WorkspaceActions/constants.ts | 1 + .../WorkspacePage/WorkspaceReadyPage.tsx | 9 +++++ .../pages/WorkspacePage/WorkspaceTopbar.tsx | 7 ++-- .../pages/WorkspacesPage/WorkspacesTable.tsx | 4 +-- .../src/pages/WorkspacesPage/batchActions.tsx | 34 +++++++++++++++++++ 9 files changed, 107 insertions(+), 6 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 7b1ef8a7e8e0c..e4178fd204fe0 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -265,3 +265,23 @@ const updateWorkspaceBuild = async ( queryKey: workspaceBuildsKey(build.workspace_id), }); }; + +export const toggleFavorite = ( + workspace: Workspace, + queryClient: QueryClient +) => { + return { + mutationFn: () => { + if (workspace.favorite) { + return API.deleteFavoriteWorkspace(workspace.id); + } else { + return API.putFavoriteWorkspace(workspace.id); + } + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: workspaceBuildsKey(workspace.id), + }); + } + } +} diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index b3fb4d1d6e1ee..31efc004c2aa7 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -33,6 +33,7 @@ export interface WorkspaceProps { handleSettings: () => void; handleChangeVersion: () => void; handleDormantActivate: () => void; + handleToggleFavorite: () => void; isUpdating: boolean; isRestarting: boolean; workspace: TypesGen.Workspace; @@ -64,6 +65,7 @@ export const Workspace: FC = ({ handleSettings, handleChangeVersion, handleDormantActivate, + handleToggleFavorite, workspace, isUpdating, isRestarting, @@ -131,6 +133,7 @@ export const Workspace: FC = ({ handleBuildRetryDebug={handleBuildRetryDebug} handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} + handleToggleFavorite={handleToggleFavorite} canRetryDebugMode={canRetryDebugMode} canChangeVersions={canChangeVersions} isUpdating={isUpdating} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index 360d66dbcdc24..61670bc730828 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -9,6 +9,8 @@ import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined"; import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; import RetryIcon from "@mui/icons-material/BuildOutlined"; import RetryDebugIcon from "@mui/icons-material/BugReportOutlined"; +import Star from "@mui/icons-material/Star"; +import StarBorder from "@mui/icons-material/StarBorder"; import { type FC } from "react"; import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { BuildParametersPopover } from "./BuildParametersPopover"; @@ -190,3 +192,24 @@ export const RetryButton: FC = ({ ); }; + +interface FavoriteButtonProps { + handleAction: (workspaceID: string) => void; + workspaceID: string; + isFavorite: boolean; +} + +export const FavoriteButton: FC = ({ + handleAction, + workspaceID, + isFavorite, +}) => { + return ( + : } + onClick={() => handleAction(workspaceID)} + > + {isFavorite ? "Unfavorite" : "Favorite"} + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index a72251978b1e5..2015ef3ab90da 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -12,6 +12,7 @@ import { UpdateButton, ActivateButton, RetryButton, + FavoriteButton, } from "./Buttons"; import Divider from "@mui/material/Divider"; @@ -30,6 +31,7 @@ import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined"; export interface WorkspaceActionsProps { workspace: Workspace; + handleToggleFavorite: () => void; handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void; handleStop: () => void; handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void; @@ -51,6 +53,7 @@ export interface WorkspaceActionsProps { export const WorkspaceActions: FC = ({ workspace, + handleToggleFavorite, handleStart, handleStop, handleRestart, @@ -131,6 +134,13 @@ export const WorkspaceActions: FC = ({ activating: , retry: , retryDebug: , + toggleFavorite: ( + + ), }; return ( @@ -150,6 +160,8 @@ export const WorkspaceActions: FC = ({ {showCancel && } + {buttonMapping.toggleFavorite} + = ({ startWorkspace(workspace, queryClient), ); + // Toggle workspace favorite + const toggleFavoriteMutation = useMutation( + toggleFavorite(workspace, queryClient), + ); + // Cancel build const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); @@ -217,6 +223,9 @@ export const WorkspaceReadyPage: FC = ({ displayError(message); } }} + handleToggleFavorite={() => { + toggleFavoriteMutation.mutate(); + }} latestVersion={latestVersion} canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index f3e1102c02999..0bba8c41a533b 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -5,7 +5,6 @@ import DeleteOutline from "@mui/icons-material/DeleteOutline"; import PersonOutline from "@mui/icons-material/PersonOutline"; import ArrowBackOutlined from "@mui/icons-material/ArrowBackOutlined"; import ScheduleOutlined from "@mui/icons-material/ScheduleOutlined"; -import Star from "@mui/icons-material/Star"; import { useTheme } from "@emotion/react"; import { type FC } from "react"; import { useQuery } from "react-query"; @@ -64,6 +63,7 @@ export interface WorkspaceProps { template: TypesGen.Template; permissions: WorkspacePermissions; latestVersion?: TypesGen.TemplateVersion; + handleToggleFavorite: () => void; } export const WorkspaceTopbar: FC = ({ @@ -76,6 +76,7 @@ export const WorkspaceTopbar: FC = ({ handleSettings, handleChangeVersion, handleDormantActivate, + handleToggleFavorite, workspace, isUpdating, isRestarting, @@ -149,9 +150,6 @@ export const WorkspaceTopbar: FC = ({ > {workspace.name} - {workspace.favorite && ( - - )} @@ -282,6 +280,7 @@ export const WorkspaceTopbar: FC = ({ handleRetryDebug={handleBuildRetryDebug} handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} + handleToggleFavorite={handleToggleFavorite} canRetryDebug={canRetryDebugMode} canChangeVersions={canChangeVersions} isUpdating={isUpdating} diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index bed5fb3f29b17..4e96e4fdc6867 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -151,8 +151,8 @@ export const WorkspacesTable: FC = ({ alignItems="center" > {workspace.name} - {workspace.favorite && ( - + {workspace.favorite && ( + )} {workspace.outdated && ( { + return Promise.all( + workspaces + .filter((w) => !w.favorite) + .map((w) => putFavoriteWorkspace(w.id)), + ); + }, + onSuccess, + onError: () => { + displayError("Failed to favorite some workspaces"); + }, + }); + + const unfavoriteAllMutation = useMutation({ + mutationFn: (workspaces: Workspace[]) => { + return Promise.all( + workspaces + .filter((w) => w.favorite) + .map((w) => deleteFavoriteWorkspace(w.id)), + ); + }, + onSuccess, + onError: () => { + displayError("Failed to unfavorite some workspaces"); + }, + }); + return { + favoriteAll: favoriteAllMutation.mutateAsync, + unfavoriteAll: unfavoriteAllMutation.mutateAsync, startAll: startAllMutation.mutateAsync, stopAll: stopAllMutation.mutateAsync, deleteAll: deleteAllMutation.mutateAsync, updateAll: updateAllMutation.mutateAsync, isLoading: + favoriteAllMutation.isLoading || + unfavoriteAllMutation.isLoading || startAllMutation.isLoading || stopAllMutation.isLoading || deleteAllMutation.isLoading, From dcb61c867558224cc09c899015fc144436c74725 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 09:24:00 +0000 Subject: [PATCH 3/9] make fmt --- site/src/api/api.ts | 12 ++++++------ site/src/api/queries/workspaces.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 45bb0abcd567c..80c2802b387d1 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1689,10 +1689,10 @@ export const updateHealthSettings = async ( return response.data; }; -export const putFavoriteWorkspace = async(workspaceID: string) => { - await axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); -} +export const putFavoriteWorkspace = async (workspaceID: string) => { + await axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); +}; -export const deleteFavoriteWorkspace = async(workspaceID: string) => { - await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); -} +export const deleteFavoriteWorkspace = async (workspaceID: string) => { + await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); +}; diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index e4178fd204fe0..58ac51da112da 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -268,7 +268,7 @@ const updateWorkspaceBuild = async ( export const toggleFavorite = ( workspace: Workspace, - queryClient: QueryClient + queryClient: QueryClient, ) => { return { mutationFn: () => { @@ -282,6 +282,6 @@ export const toggleFavorite = ( await queryClient.invalidateQueries({ queryKey: workspaceBuildsKey(workspace.id), }); - } - } -} + }, + }; +}; From d4b806d5fa8f9dbe3f22074ac6fe008d386dc767 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 10:00:59 +0000 Subject: [PATCH 4/9] update workspace in onSuccess handler --- site/src/api/queries/workspaces.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 58ac51da112da..2e042063701e0 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -279,9 +279,11 @@ export const toggleFavorite = ( } }, onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: workspaceBuildsKey(workspace.id), - }); + queryClient.setQueryData( + workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), + {...workspace, + favorite: !workspace.favorite} + ); }, }; }; From 279e242474e86b5f5df5f2a3dca60f66dcc73dde Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 10:20:47 +0000 Subject: [PATCH 5/9] fixup! update workspace in onSuccess handler --- site/src/api/queries/workspaces.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 2e042063701e0..6e4a63ecb4263 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -281,8 +281,7 @@ export const toggleFavorite = ( onSuccess: async () => { queryClient.setQueryData( workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), - {...workspace, - favorite: !workspace.favorite} + { ...workspace, favorite: !workspace.favorite }, ); }, }; From a1887cac3645601990c474f0d1422a9b7fc3e6b6 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 12:18:34 +0000 Subject: [PATCH 6/9] add stories --- site/src/pages/WorkspacePage/Workspace.stories.tsx | 7 +++++++ .../WorkspacesPage/WorkspacesPageView.stories.tsx | 11 +++++++++++ site/src/testHelpers/entities.ts | 8 +++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 2a289593acdca..cd6cedee0f692 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -90,6 +90,13 @@ export const Running: Story = { }, }; +export const Favorite: Story = { + args: { + ...Running.args, + workspace: Mocks.MockFavoriteWorkspace, + }, +}; + export const WithoutUpdateAccess: Story = { args: { ...Running.args, diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index cfff23823889e..73f5ac74dacf7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -165,6 +165,17 @@ export const AllStates: Story = { }, }; +export const AllStatesWithFavorites: Story = { + args: { + workspaces: allWorkspaces.map((workspace, i) => ({ + ...workspace, + // NOTE: testing sort order is not relevant here. + favorite: i % 2 === 0, + })), + count: allWorkspaces.length, + }, +}; + const icons = [ "/icon/code.svg", "/icon/aws.svg", diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index b3ba287be5060..e35a44b26cfff 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -4,7 +4,7 @@ import { type DeploymentConfig, } from "api/api"; import { FieldError } from "api/errors"; -import type * as TypesGen from "api/typesGenerated"; +import * as TypesGen from "api/typesGenerated"; import range from "lodash/range"; import type { Permissions } from "contexts/auth/permissions"; import { TemplateVersionFiles } from "utils/templateVersion"; @@ -1020,6 +1020,12 @@ export const MockWorkspace: TypesGen.Workspace = { }, automatic_updates: "never", allow_renames: true, + favorite: false, +}; + +export const MockFavoriteWorkspace: TypesGen.Workspace = { + ...MockWorkspace, + id: "test-favorite-workspace", favorite: true, }; From 279e87f1b0cbe996af1f54ea790c3f0349097f03 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 12:42:58 +0000 Subject: [PATCH 7/9] invalidate query data --- site/src/api/queries/workspaces.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 6e4a63ecb4263..44bfc767a1c01 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -283,6 +283,9 @@ export const toggleFavorite = ( workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), { ...workspace, favorite: !workspace.favorite }, ); + await queryClient.invalidateQueries({ + queryKey: workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), + }); }, }; }; From c46832ced5c3136e9f5013d124dbb2e30041d2e0 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 12:43:12 +0000 Subject: [PATCH 8/9] rename field --- site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx | 6 +++--- .../WorkspacePage/WorkspaceActions/WorkspaceActions.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index 61670bc730828..d8cc82be6a7f0 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -194,20 +194,20 @@ export const RetryButton: FC = ({ }; interface FavoriteButtonProps { - handleAction: (workspaceID: string) => void; + onToggle: (workspaceID: string) => void; workspaceID: string; isFavorite: boolean; } export const FavoriteButton: FC = ({ - handleAction, + onToggle: onToggle, workspaceID, isFavorite, }) => { return ( : } - onClick={() => handleAction(workspaceID)} + onClick={() => onToggle(workspaceID)} > {isFavorite ? "Unfavorite" : "Favorite"} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 2015ef3ab90da..fb6536b2a08f1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -138,7 +138,7 @@ export const WorkspaceActions: FC = ({ ), }; From 32f6f5fae3adc9121eee5d67b4f68f135a41e2c5 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 29 Jan 2024 13:25:14 +0000 Subject: [PATCH 9/9] make fmt --- site/src/api/queries/workspaces.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/site/src/api/queries/workspaces.ts b/site/src/api/queries/workspaces.ts index 44bfc767a1c01..25b74984eb0af 100644 --- a/site/src/api/queries/workspaces.ts +++ b/site/src/api/queries/workspaces.ts @@ -284,7 +284,10 @@ export const toggleFavorite = ( { ...workspace, favorite: !workspace.favorite }, ); await queryClient.invalidateQueries({ - queryKey: workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name), + queryKey: workspaceByOwnerAndNameKey( + workspace.owner_name, + workspace.name, + ), }); }, };