Skip to content

Commit 9abf6ec

Browse files
authored
feat(site): show favorite workspaces in ui (coder#11875)
* Add Star beside workspace name to indicate favorite status in WorkspacesList * Add button in workspace top row to toggle workspace favorite status
1 parent acd22b2 commit 9abf6ec

File tree

13 files changed

+149
-1
lines changed

13 files changed

+149
-1
lines changed

site/src/api/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,3 +1688,11 @@ export const updateHealthSettings = async (
16881688
);
16891689
return response.data;
16901690
};
1691+
1692+
export const putFavoriteWorkspace = async (workspaceID: string) => {
1693+
await axios.put(`/api/v2/workspaces/${workspaceID}/favorite`);
1694+
};
1695+
1696+
export const deleteFavoriteWorkspace = async (workspaceID: string) => {
1697+
await axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`);
1698+
};

site/src/api/queries/workspaces.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,30 @@ const updateWorkspaceBuild = async (
265265
queryKey: workspaceBuildsKey(build.workspace_id),
266266
});
267267
};
268+
269+
export const toggleFavorite = (
270+
workspace: Workspace,
271+
queryClient: QueryClient,
272+
) => {
273+
return {
274+
mutationFn: () => {
275+
if (workspace.favorite) {
276+
return API.deleteFavoriteWorkspace(workspace.id);
277+
} else {
278+
return API.putFavoriteWorkspace(workspace.id);
279+
}
280+
},
281+
onSuccess: async () => {
282+
queryClient.setQueryData(
283+
workspaceByOwnerAndNameKey(workspace.owner_name, workspace.name),
284+
{ ...workspace, favorite: !workspace.favorite },
285+
);
286+
await queryClient.invalidateQueries({
287+
queryKey: workspaceByOwnerAndNameKey(
288+
workspace.owner_name,
289+
workspace.name,
290+
),
291+
});
292+
},
293+
};
294+
};

site/src/pages/WorkspacePage/Workspace.stories.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ export const Running: Story = {
9090
},
9191
};
9292

93+
export const Favorite: Story = {
94+
args: {
95+
...Running.args,
96+
workspace: Mocks.MockFavoriteWorkspace,
97+
},
98+
};
99+
93100
export const WithoutUpdateAccess: Story = {
94101
args: {
95102
...Running.args,

site/src/pages/WorkspacePage/Workspace.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface WorkspaceProps {
3333
handleSettings: () => void;
3434
handleChangeVersion: () => void;
3535
handleDormantActivate: () => void;
36+
handleToggleFavorite: () => void;
3637
isUpdating: boolean;
3738
isRestarting: boolean;
3839
workspace: TypesGen.Workspace;
@@ -64,6 +65,7 @@ export const Workspace: FC<WorkspaceProps> = ({
6465
handleSettings,
6566
handleChangeVersion,
6667
handleDormantActivate,
68+
handleToggleFavorite,
6769
workspace,
6870
isUpdating,
6971
isRestarting,
@@ -131,6 +133,7 @@ export const Workspace: FC<WorkspaceProps> = ({
131133
handleBuildRetryDebug={handleBuildRetryDebug}
132134
handleChangeVersion={handleChangeVersion}
133135
handleDormantActivate={handleDormantActivate}
136+
handleToggleFavorite={handleToggleFavorite}
134137
canRetryDebugMode={canRetryDebugMode}
135138
canChangeVersions={canChangeVersions}
136139
isUpdating={isUpdating}

site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined";
99
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
1010
import RetryIcon from "@mui/icons-material/BuildOutlined";
1111
import RetryDebugIcon from "@mui/icons-material/BugReportOutlined";
12+
import Star from "@mui/icons-material/Star";
13+
import StarBorder from "@mui/icons-material/StarBorder";
1214
import { type FC } from "react";
1315
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
1416
import { BuildParametersPopover } from "./BuildParametersPopover";
@@ -190,3 +192,24 @@ export const RetryButton: FC<RetryButtonProps> = ({
190192
</TopbarButton>
191193
);
192194
};
195+
196+
interface FavoriteButtonProps {
197+
onToggle: (workspaceID: string) => void;
198+
workspaceID: string;
199+
isFavorite: boolean;
200+
}
201+
202+
export const FavoriteButton: FC<FavoriteButtonProps> = ({
203+
onToggle: onToggle,
204+
workspaceID,
205+
isFavorite,
206+
}) => {
207+
return (
208+
<TopbarButton
209+
startIcon={isFavorite ? <Star /> : <StarBorder />}
210+
onClick={() => onToggle(workspaceID)}
211+
>
212+
{isFavorite ? "Unfavorite" : "Favorite"}
213+
</TopbarButton>
214+
);
215+
};

site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
UpdateButton,
1313
ActivateButton,
1414
RetryButton,
15+
FavoriteButton,
1516
} from "./Buttons";
1617

1718
import Divider from "@mui/material/Divider";
@@ -30,6 +31,7 @@ import MoreVertOutlined from "@mui/icons-material/MoreVertOutlined";
3031

3132
export interface WorkspaceActionsProps {
3233
workspace: Workspace;
34+
handleToggleFavorite: () => void;
3335
handleStart: (buildParameters?: WorkspaceBuildParameter[]) => void;
3436
handleStop: () => void;
3537
handleRestart: (buildParameters?: WorkspaceBuildParameter[]) => void;
@@ -51,6 +53,7 @@ export interface WorkspaceActionsProps {
5153

5254
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
5355
workspace,
56+
handleToggleFavorite,
5457
handleStart,
5558
handleStop,
5659
handleRestart,
@@ -131,6 +134,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
131134
activating: <ActivateButton loading handleAction={handleDormantActivate} />,
132135
retry: <RetryButton handleAction={handleRetry} />,
133136
retryDebug: <RetryButton debug handleAction={handleRetryDebug} />,
137+
toggleFavorite: (
138+
<FavoriteButton
139+
workspaceID={workspace.id}
140+
isFavorite={workspace.favorite}
141+
onToggle={handleToggleFavorite}
142+
/>
143+
),
134144
};
135145

136146
return (
@@ -150,6 +160,8 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
150160

151161
{showCancel && <CancelButton handleAction={handleCancel} />}
152162

163+
{buttonMapping.toggleFavorite}
164+
153165
<MoreMenu>
154166
<MoreMenuTrigger>
155167
<TopbarIconButton

site/src/pages/WorkspacePage/WorkspaceActions/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const actionTypes = [
1515
"updating",
1616
"activate",
1717
"activating",
18+
"toggleFavorite",
1819

1920
// There's no need for a retrying state because retrying starts a transition
2021
// into one of the starting, stopping, or deleting states (based on the

site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
updateWorkspace,
2121
stopWorkspace,
2222
startWorkspace,
23+
toggleFavorite,
2324
cancelBuild,
2425
} from "api/queries/workspaces";
2526
import { Alert } from "components/Alert/Alert";
@@ -144,6 +145,11 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
144145
startWorkspace(workspace, queryClient),
145146
);
146147

148+
// Toggle workspace favorite
149+
const toggleFavoriteMutation = useMutation(
150+
toggleFavorite(workspace, queryClient),
151+
);
152+
147153
// Cancel build
148154
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));
149155

@@ -217,6 +223,9 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
217223
displayError(message);
218224
}
219225
}}
226+
handleToggleFavorite={() => {
227+
toggleFavoriteMutation.mutate();
228+
}}
220229
latestVersion={latestVersion}
221230
canChangeVersions={canChangeVersions}
222231
hideSSHButton={featureVisibility["browser_only"]}

site/src/pages/WorkspacePage/WorkspaceTopbar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export interface WorkspaceProps {
6363
template: TypesGen.Template;
6464
permissions: WorkspacePermissions;
6565
latestVersion?: TypesGen.TemplateVersion;
66+
handleToggleFavorite: () => void;
6667
}
6768

6869
export const WorkspaceTopbar: FC<WorkspaceProps> = ({
@@ -75,6 +76,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
7576
handleSettings,
7677
handleChangeVersion,
7778
handleDormantActivate,
79+
handleToggleFavorite,
7880
workspace,
7981
isUpdating,
8082
isRestarting,
@@ -278,6 +280,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
278280
handleRetryDebug={handleBuildRetryDebug}
279281
handleChangeVersion={handleChangeVersion}
280282
handleDormantActivate={handleDormantActivate}
283+
handleToggleFavorite={handleToggleFavorite}
281284
canRetryDebug={canRetryDebugMode}
282285
canChangeVersions={canChangeVersions}
283286
isUpdating={isUpdating}

site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,17 @@ export const AllStates: Story = {
165165
},
166166
};
167167

168+
export const AllStatesWithFavorites: Story = {
169+
args: {
170+
workspaces: allWorkspaces.map((workspace, i) => ({
171+
...workspace,
172+
// NOTE: testing sort order is not relevant here.
173+
favorite: i % 2 === 0,
174+
})),
175+
count: allWorkspaces.length,
176+
},
177+
};
178+
168179
const icons = [
169180
"/icon/code.svg",
170181
"/icon/aws.svg",

site/src/pages/WorkspacesPage/WorkspacesTable.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import TableRow from "@mui/material/TableRow";
77
import Checkbox from "@mui/material/Checkbox";
88
import Skeleton from "@mui/material/Skeleton";
99
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
10+
import Star from "@mui/icons-material/Star";
1011
import { useTheme } from "@emotion/react";
1112
import { type FC, type ReactNode } from "react";
1213
import { useNavigate } from "react-router-dom";
@@ -150,6 +151,9 @@ export const WorkspacesTable: FC<WorkspacesTableProps> = ({
150151
alignItems="center"
151152
>
152153
{workspace.name}
154+
{workspace.favorite && (
155+
<Star css={{ width: 16, height: 16 }} />
156+
)}
153157
{workspace.outdated && (
154158
<WorkspaceOutdatedTooltip
155159
templateName={workspace.template_name}

site/src/pages/WorkspacesPage/batchActions.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { useMutation } from "react-query";
22
import {
33
deleteWorkspace,
4+
deleteFavoriteWorkspace,
5+
putFavoriteWorkspace,
46
startWorkspace,
57
stopWorkspace,
68
updateWorkspace,
@@ -63,12 +65,44 @@ export function useBatchActions(options: UseBatchActionsProps) {
6365
},
6466
});
6567

68+
const favoriteAllMutation = useMutation({
69+
mutationFn: (workspaces: Workspace[]) => {
70+
return Promise.all(
71+
workspaces
72+
.filter((w) => !w.favorite)
73+
.map((w) => putFavoriteWorkspace(w.id)),
74+
);
75+
},
76+
onSuccess,
77+
onError: () => {
78+
displayError("Failed to favorite some workspaces");
79+
},
80+
});
81+
82+
const unfavoriteAllMutation = useMutation({
83+
mutationFn: (workspaces: Workspace[]) => {
84+
return Promise.all(
85+
workspaces
86+
.filter((w) => w.favorite)
87+
.map((w) => deleteFavoriteWorkspace(w.id)),
88+
);
89+
},
90+
onSuccess,
91+
onError: () => {
92+
displayError("Failed to unfavorite some workspaces");
93+
},
94+
});
95+
6696
return {
97+
favoriteAll: favoriteAllMutation.mutateAsync,
98+
unfavoriteAll: unfavoriteAllMutation.mutateAsync,
6799
startAll: startAllMutation.mutateAsync,
68100
stopAll: stopAllMutation.mutateAsync,
69101
deleteAll: deleteAllMutation.mutateAsync,
70102
updateAll: updateAllMutation.mutateAsync,
71103
isLoading:
104+
favoriteAllMutation.isLoading ||
105+
unfavoriteAllMutation.isLoading ||
72106
startAllMutation.isLoading ||
73107
stopAllMutation.isLoading ||
74108
deleteAllMutation.isLoading,

site/src/testHelpers/entities.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
type DeploymentConfig,
55
} from "api/api";
66
import { FieldError } from "api/errors";
7-
import type * as TypesGen from "api/typesGenerated";
7+
import * as TypesGen from "api/typesGenerated";
88
import range from "lodash/range";
99
import type { Permissions } from "contexts/auth/permissions";
1010
import { TemplateVersionFiles } from "utils/templateVersion";
@@ -1020,6 +1020,12 @@ export const MockWorkspace: TypesGen.Workspace = {
10201020
},
10211021
automatic_updates: "never",
10221022
allow_renames: true,
1023+
favorite: false,
1024+
};
1025+
1026+
export const MockFavoriteWorkspace: TypesGen.Workspace = {
1027+
...MockWorkspace,
1028+
id: "test-favorite-workspace",
10231029
favorite: true,
10241030
};
10251031

0 commit comments

Comments
 (0)