Skip to content

Commit 4324fcb

Browse files
authored
[papi] migrate ListWorkspaces and WatchWorkspaceStatus (#19022)
* [papi] migrate ListWorkspaces and WatchWorkspaceStatus * bump up cache version
1 parent 0352c45 commit 4324fcb

31 files changed

+1057
-426
lines changed

components/dashboard/src/admin/WorkspaceDetail.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getProjectPath } from "../workspaces/WorkspaceEntry";
1515
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
1616
import Property from "./Property";
1717
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
18+
import { converter } from "../service/public-api";
1819

1920
export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance }) {
2021
const [workspace, setWorkspace] = useState(props.workspace);
@@ -58,10 +59,24 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
5859
<div className="flex">
5960
<Heading2>{workspace.workspaceId}</Heading2>
6061
<span className="my-auto ml-3">
61-
<WorkspaceStatusIndicator instance={WorkspaceAndInstance.toInstance(workspace)} />
62+
<WorkspaceStatusIndicator
63+
status={
64+
converter.toWorkspace({
65+
workspace: WorkspaceAndInstance.toWorkspace(workspace),
66+
latestInstance: WorkspaceAndInstance.toInstance(workspace),
67+
}).status
68+
}
69+
/>
6270
</span>
6371
</div>
64-
<Subheading>{getProjectPath(WorkspaceAndInstance.toWorkspace(workspace))}</Subheading>
72+
<Subheading>
73+
{getProjectPath(
74+
converter.toWorkspace({
75+
workspace: WorkspaceAndInstance.toWorkspace(workspace),
76+
latestInstance: WorkspaceAndInstance.toInstance(workspace),
77+
}),
78+
)}
79+
</Subheading>
6580
</div>
6681
<button
6782
className="secondary ml-3"
@@ -172,7 +187,7 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
172187
return (
173188
<div className="px-6 py-3 flex justify-between text-sm text-gray-400 mb-2">
174189
<span className="my-1 ml-3">
175-
<WorkspaceStatusIndicator instance={wsi} />
190+
<WorkspaceStatusIndicator status={converter.toWorkspace(wsi).status} />
176191
</span>
177192
<div className="w-4/12">{wsi.id}</div>
178193
<div className="w-2/12">{dayjs(wsi.startedTime).fromNow()}</div>

components/dashboard/src/admin/WorkspacesSearch.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { SpinnerLoader } from "../components/Loader";
3030
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
3131
import searchIcon from "../icons/search.svg";
3232
import Tooltip from "../components/Tooltip";
33+
import { converter } from "../service/public-api";
3334

3435
interface Props {
3536
user?: User;
@@ -166,6 +167,10 @@ export function WorkspaceSearch(props: Props) {
166167
}
167168

168169
function WorkspaceEntry(p: { ws: WorkspaceAndInstance }) {
170+
const workspace = converter.toWorkspace({
171+
workspace: WorkspaceAndInstance.toWorkspace(p.ws),
172+
latestInstance: WorkspaceAndInstance.toInstance(p.ws),
173+
});
169174
return (
170175
<Link
171176
key={"ws-" + p.ws.workspaceId}
@@ -174,14 +179,14 @@ function WorkspaceEntry(p: { ws: WorkspaceAndInstance }) {
174179
>
175180
<div className="rounded-xl whitespace-nowrap flex py-6 px-6 w-full justify-between hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-kumquat-light group">
176181
<div className="pr-3 self-center w-8">
177-
<WorkspaceStatusIndicator instance={WorkspaceAndInstance.toInstance(p.ws)} />
182+
<WorkspaceStatusIndicator status={workspace.status} />
178183
</div>
179184
<div className="flex flex-col w-5/12 truncate">
180185
<div className="font-medium text-gray-800 dark:text-gray-100 truncate hover:text-blue-600 dark:hover:text-blue-400 truncate">
181186
{p.ws.workspaceId}
182187
</div>
183188
<div className="text-sm overflow-ellipsis truncate text-gray-400 truncate">
184-
{getProjectPath(WorkspaceAndInstance.toWorkspace(p.ws))}
189+
{getProjectPath(workspace)}
185190
</div>
186191
</div>
187192
<div className="flex flex-col w-5/12 self-center truncate">

components/dashboard/src/components/PrebuildLogs.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
} from "@gitpod/gitpod-protocol";
1616
import { getGitpodService } from "../service/service";
1717
import { PrebuildStatus } from "../projects/Prebuilds";
18-
import { converter, workspaceClient } from "../service/public-api";
18+
import { workspaceClient } from "../service/public-api";
1919
import { GetWorkspaceRequest, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
20+
import { disposableWatchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
2021

2122
const WorkspaceLogs = React.lazy(() => import("./WorkspaceLogs"));
2223

@@ -92,17 +93,18 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
9293
setError(err);
9394
}
9495

96+
const watchDispose = disposableWatchWorkspaceStatus(props.workspaceId, (resp) => {
97+
if (resp.status?.instanceId && resp.status?.phase?.name) {
98+
setWorkspace({
99+
instanceId: resp.status.instanceId,
100+
phase: resp.status.phase.name,
101+
});
102+
}
103+
});
95104
// Register for future updates
105+
disposables.push(watchDispose);
96106
disposables.push(
97107
getGitpodService().registerClient({
98-
onInstanceUpdate: (instance) => {
99-
if (props.workspaceId === instance.workspaceId) {
100-
setWorkspace({
101-
instanceId: instance.id,
102-
phase: converter.toPhase(instance),
103-
});
104-
}
105-
},
106108
onWorkspaceImageBuildLogs: (
107109
info: WorkspaceImageBuild.StateInfo,
108110
content?: WorkspaceImageBuild.LogContent,

components/dashboard/src/data/setup.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovi
2626
// This is used to version the cache
2727
// If data we cache changes in a non-backwards compatible way, increment this version
2828
// That will bust any previous cache versions a client may have stored
29-
const CACHE_VERSION = "4";
29+
const CACHE_VERSION = "5";
3030

3131
export function noPersistence(queryKey: QueryKey): QueryKey {
3232
return [...queryKey, "no-persistence"];

components/dashboard/src/data/workspaces/delete-inactive-workspaces-mutation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const useDeleteInactiveWorkspacesMutation = () => {
4040
// Using the result of the mutationFn so we only remove workspaces that were delete
4141
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
4242
return oldWorkspacesData?.filter((info) => {
43-
return !deletedWorkspaceIds.includes(info.workspace.id);
43+
return !deletedWorkspaceIds.includes(info.id);
4444
});
4545
});
4646

components/dashboard/src/data/workspaces/delete-workspace-mutation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const useDeleteWorkspaceMutation = () => {
2727
// Remove workspace from cache so it's reflected right away
2828
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
2929
return oldWorkspacesData?.filter((info) => {
30-
return info.workspace.id !== workspaceId;
30+
return info.id !== workspaceId;
3131
});
3232
});
3333

components/dashboard/src/data/workspaces/list-workspaces-query.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { WorkspaceInfo } from "@gitpod/gitpod-protocol";
87
import { useQuery } from "@tanstack/react-query";
9-
import { getGitpodService } from "../../service/service";
108
import { useCurrentOrg } from "../organizations/orgs-query";
9+
import { workspaceClient } from "../../service/public-api";
10+
import { Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
1111

12-
export type ListWorkspacesQueryResult = WorkspaceInfo[];
12+
export type ListWorkspacesQueryResult = Workspace[];
1313

1414
type UseListWorkspacesQueryArgs = {
1515
limit: number;
@@ -22,24 +22,27 @@ export const useListWorkspacesQuery = ({ limit }: UseListWorkspacesQueryArgs) =>
2222
queryFn: async () => {
2323
// TODO: Can we update the backend api to sort & rank pinned over non-pinned for us?
2424
const [infos, pinned] = await Promise.all([
25-
getGitpodService().server.getWorkspaces({
26-
limit,
27-
includeWithoutProject: true,
25+
workspaceClient.listWorkspaces({
26+
pagination: {
27+
pageSize: limit,
28+
},
29+
pinned: false,
2830
organizationId: currentOrg.data?.id,
2931
}),
3032
// Additional fetch for pinned workspaces
3133
// see also: https://github.com/gitpod-io/gitpod/issues/4488
32-
getGitpodService().server.getWorkspaces({
33-
limit,
34-
pinnedOnly: true,
35-
includeWithoutProject: true,
34+
workspaceClient.listWorkspaces({
35+
pagination: {
36+
pageSize: limit,
37+
},
38+
pinned: true,
3639
organizationId: currentOrg.data?.id,
3740
}),
3841
]);
3942

4043
// Merge both data sets into one unique (by ws id) array
41-
const workspacesMap = new Map(infos.map((ws) => [ws.workspace.id, ws]));
42-
const pinnedWorkspacesMap = new Map(pinned.map((ws) => [ws.workspace.id, ws]));
44+
const workspacesMap = new Map(infos.workspaces.map((ws) => [ws.id, ws]));
45+
const pinnedWorkspacesMap = new Map(pinned.workspaces.map((ws) => [ws.id, ws]));
4346
const workspaces = Array.from(new Map([...workspacesMap, ...pinnedWorkspacesMap]).values());
4447

4548
return workspaces;

components/dashboard/src/data/workspaces/listen-to-workspace-ws-messages.ts

Lines changed: 59 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,81 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { WorkspaceInstance } from "@gitpod/gitpod-protocol";
7+
import { Disposable } from "@gitpod/gitpod-protocol";
88
import { useQueryClient } from "@tanstack/react-query";
99
import { useEffect } from "react";
10-
import { getGitpodService } from "../../service/service";
1110
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
1211
import { useCurrentOrg } from "../organizations/orgs-query";
12+
import { workspaceClient } from "../../service/public-api";
13+
import { WatchWorkspaceStatusResponse, Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
1314

1415
export const useListenToWorkspacesWSMessages = () => {
1516
const queryClient = useQueryClient();
1617
const organizationId = useCurrentOrg().data?.id;
1718

1819
useEffect(() => {
19-
const disposable = getGitpodService().registerClient({
20-
onInstanceUpdate: (instance: WorkspaceInstance) => {
21-
const queryKey = getListWorkspacesQueryKey(organizationId);
22-
let foundWorkspaces = false;
23-
24-
// Update the workspace with the latest instance
25-
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
26-
return oldWorkspacesData?.map((info) => {
27-
if (info.workspace.id !== instance.workspaceId) {
28-
return info;
29-
}
30-
31-
foundWorkspaces = true;
32-
return {
33-
...info,
34-
latestInstance: instance,
35-
};
36-
});
20+
const disposable = disposableWatchWorkspaceStatus(undefined, (status) => {
21+
const queryKey = getListWorkspacesQueryKey(organizationId);
22+
let foundWorkspaces = false;
23+
24+
// Update the workspace with the latest instance
25+
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
26+
return oldWorkspacesData?.map((info) => {
27+
if (info.id !== status.workspaceId) {
28+
return info;
29+
}
30+
foundWorkspaces = true;
31+
const workspace = new Workspace(info);
32+
workspace.status = status.status;
33+
info.status = status.status;
34+
return workspace;
3735
});
36+
});
3837

39-
if (!foundWorkspaces) {
40-
// If the instance was for a workspace we don't have, it should get returned w/ an updated query
41-
queryClient.invalidateQueries({ queryKey });
42-
}
43-
},
38+
if (!foundWorkspaces) {
39+
// If the instance was for a workspace we don't have, it should get returned w/ an updated query
40+
queryClient.invalidateQueries({ queryKey });
41+
}
4442
});
4543

4644
return () => {
4745
disposable.dispose();
4846
};
4947
}, [organizationId, queryClient]);
5048
};
49+
50+
export const disposableWatchWorkspaceStatus = (
51+
workspaceId: string | undefined,
52+
cb: (response: WatchWorkspaceStatusResponse) => void,
53+
): Disposable => {
54+
const MAX_BACKOFF = 60000;
55+
const BASE_BACKOFF = 3000;
56+
let backoff = BASE_BACKOFF;
57+
const abortController = new AbortController();
58+
59+
(async () => {
60+
while (!abortController.signal.aborted) {
61+
try {
62+
const it = workspaceClient.watchWorkspaceStatus(
63+
{ workspaceId },
64+
{
65+
signal: abortController.signal,
66+
},
67+
);
68+
for await (const response of it) {
69+
cb(response);
70+
backoff = BASE_BACKOFF;
71+
}
72+
} catch (e) {
73+
backoff = Math.min(2 * backoff, MAX_BACKOFF);
74+
console.error("failed to watch workspace status, retrying", e);
75+
}
76+
const jitter = Math.random() * 0.3 * backoff;
77+
const delay = backoff + jitter;
78+
await new Promise((resolve) => setTimeout(resolve, delay));
79+
}
80+
})();
81+
return {
82+
dispose: () => abortController.abort(),
83+
};
84+
};

components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
88
import { getGitpodService } from "../../service/service";
99
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
1010
import { useCurrentOrg } from "../organizations/orgs-query";
11+
import { Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
1112

1213
type ToggleWorkspacePinnedArgs = {
1314
workspaceId: string;
@@ -27,17 +28,12 @@ export const useToggleWorkspacedPinnedMutation = () => {
2728
// Update workspace.pinned to account for the toggle so it's reflected immediately
2829
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspaceData) => {
2930
return oldWorkspaceData?.map((info) => {
30-
if (info.workspace.id !== workspaceId) {
31+
if (info.id !== workspaceId) {
3132
return info;
3233
}
33-
34-
return {
35-
...info,
36-
workspace: {
37-
...info.workspace,
38-
pinned: !info.workspace.pinned,
39-
},
40-
};
34+
const workspace = new Workspace(info);
35+
workspace.pinned = !workspace.pinned;
36+
return workspace;
4137
});
4238
});
4339

components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { GitpodServer } from "@gitpod/gitpod-protocol";
87
import { useMutation, useQueryClient } from "@tanstack/react-query";
98
import { getGitpodService } from "../../service/service";
109
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
1110
import { useCurrentOrg } from "../organizations/orgs-query";
11+
import { AdmissionLevel, Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
1212

1313
type ToggleWorkspaceSharedArgs = {
1414
workspaceId: string;
15-
level: GitpodServer.AdmissionLevel;
15+
level: AdmissionLevel;
1616
};
1717

1818
export const useToggleWorkspaceSharedMutation = () => {
@@ -21,25 +21,32 @@ export const useToggleWorkspaceSharedMutation = () => {
2121

2222
return useMutation({
2323
mutationFn: async ({ workspaceId, level }: ToggleWorkspaceSharedArgs) => {
24-
return await getGitpodService().server.controlAdmission(workspaceId, level);
24+
if (level === AdmissionLevel.UNSPECIFIED) {
25+
return;
26+
}
27+
return await getGitpodService().server.controlAdmission(
28+
workspaceId,
29+
level === AdmissionLevel.EVERYONE ? "everyone" : "owner",
30+
);
2531
},
2632
onSuccess: (_, { workspaceId, level }) => {
33+
if (level === AdmissionLevel.UNSPECIFIED) {
34+
return;
35+
}
2736
const queryKey = getListWorkspacesQueryKey(org.data?.id);
2837

2938
// Update workspace.shareable to the level we set so it's reflected immediately
3039
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
3140
return oldWorkspacesData?.map((info) => {
32-
if (info.workspace.id !== workspaceId) {
41+
if (info.id !== workspaceId) {
3342
return info;
3443
}
3544

36-
return {
37-
...info,
38-
workspace: {
39-
...info.workspace,
40-
shareable: level === "everyone" ? true : false,
41-
},
42-
};
45+
const workspace = new Workspace(info);
46+
if (workspace.status) {
47+
workspace.status.admission = level;
48+
}
49+
return workspace;
4350
});
4451
});
4552

0 commit comments

Comments
 (0)