Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[papi] migrate ListWorkspaces and WatchWorkspaceStatus #19022

Merged
merged 2 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions components/dashboard/src/admin/WorkspaceDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getProjectPath } from "../workspaces/WorkspaceEntry";
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
import Property from "./Property";
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
import { converter } from "../service/public-api";

export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance }) {
const [workspace, setWorkspace] = useState(props.workspace);
Expand Down Expand Up @@ -58,10 +59,24 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
<div className="flex">
<Heading2>{workspace.workspaceId}</Heading2>
<span className="my-auto ml-3">
<WorkspaceStatusIndicator instance={WorkspaceAndInstance.toInstance(workspace)} />
<WorkspaceStatusIndicator
status={
converter.toWorkspace({
workspace: WorkspaceAndInstance.toWorkspace(workspace),
latestInstance: WorkspaceAndInstance.toInstance(workspace),
}).status
}
/>
</span>
</div>
<Subheading>{getProjectPath(WorkspaceAndInstance.toWorkspace(workspace))}</Subheading>
<Subheading>
{getProjectPath(
converter.toWorkspace({
workspace: WorkspaceAndInstance.toWorkspace(workspace),
latestInstance: WorkspaceAndInstance.toInstance(workspace),
}),
)}
</Subheading>
</div>
<button
className="secondary ml-3"
Expand Down Expand Up @@ -172,7 +187,7 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
return (
<div className="px-6 py-3 flex justify-between text-sm text-gray-400 mb-2">
<span className="my-1 ml-3">
<WorkspaceStatusIndicator instance={wsi} />
<WorkspaceStatusIndicator status={converter.toWorkspace(wsi).status} />
</span>
<div className="w-4/12">{wsi.id}</div>
<div className="w-2/12">{dayjs(wsi.startedTime).fromNow()}</div>
Expand Down
9 changes: 7 additions & 2 deletions components/dashboard/src/admin/WorkspacesSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SpinnerLoader } from "../components/Loader";
import { WorkspaceStatusIndicator } from "../workspaces/WorkspaceStatusIndicator";
import searchIcon from "../icons/search.svg";
import Tooltip from "../components/Tooltip";
import { converter } from "../service/public-api";

interface Props {
user?: User;
Expand Down Expand Up @@ -166,6 +167,10 @@ export function WorkspaceSearch(props: Props) {
}

function WorkspaceEntry(p: { ws: WorkspaceAndInstance }) {
const workspace = converter.toWorkspace({
workspace: WorkspaceAndInstance.toWorkspace(p.ws),
latestInstance: WorkspaceAndInstance.toInstance(p.ws),
});
return (
<Link
key={"ws-" + p.ws.workspaceId}
Expand All @@ -174,14 +179,14 @@ function WorkspaceEntry(p: { ws: WorkspaceAndInstance }) {
>
<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">
<div className="pr-3 self-center w-8">
<WorkspaceStatusIndicator instance={WorkspaceAndInstance.toInstance(p.ws)} />
<WorkspaceStatusIndicator status={workspace.status} />
</div>
<div className="flex flex-col w-5/12 truncate">
<div className="font-medium text-gray-800 dark:text-gray-100 truncate hover:text-blue-600 dark:hover:text-blue-400 truncate">
{p.ws.workspaceId}
</div>
<div className="text-sm overflow-ellipsis truncate text-gray-400 truncate">
{getProjectPath(WorkspaceAndInstance.toWorkspace(p.ws))}
{getProjectPath(workspace)}
</div>
</div>
<div className="flex flex-col w-5/12 self-center truncate">
Expand Down
20 changes: 11 additions & 9 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import {
} from "@gitpod/gitpod-protocol";
import { getGitpodService } from "../service/service";
import { PrebuildStatus } from "../projects/Prebuilds";
import { converter, workspaceClient } from "../service/public-api";
import { workspaceClient } from "../service/public-api";
import { GetWorkspaceRequest, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { disposableWatchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";

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

Expand Down Expand Up @@ -92,17 +93,18 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
setError(err);
}

const watchDispose = disposableWatchWorkspaceStatus(props.workspaceId, (resp) => {
if (resp.status?.instanceId && resp.status?.phase?.name) {
setWorkspace({
instanceId: resp.status.instanceId,
phase: resp.status.phase.name,
});
}
});
// Register for future updates
disposables.push(watchDispose);
disposables.push(
getGitpodService().registerClient({
onInstanceUpdate: (instance) => {
if (props.workspaceId === instance.workspaceId) {
setWorkspace({
instanceId: instance.id,
phase: converter.toPhase(instance),
});
}
},
onWorkspaceImageBuildLogs: (
info: WorkspaceImageBuild.StateInfo,
content?: WorkspaceImageBuild.LogContent,
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/data/setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import * as AuthProviderClasses from "@gitpod/public-api/lib/gitpod/v1/authprovi
// This is used to version the cache
// If data we cache changes in a non-backwards compatible way, increment this version
// That will bust any previous cache versions a client may have stored
const CACHE_VERSION = "4";
const CACHE_VERSION = "5";

export function noPersistence(queryKey: QueryKey): QueryKey {
return [...queryKey, "no-persistence"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const useDeleteInactiveWorkspacesMutation = () => {
// Using the result of the mutationFn so we only remove workspaces that were delete
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.filter((info) => {
return !deletedWorkspaceIds.includes(info.workspace.id);
return !deletedWorkspaceIds.includes(info.id);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const useDeleteWorkspaceMutation = () => {
// Remove workspace from cache so it's reflected right away
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.filter((info) => {
return info.workspace.id !== workspaceId;
return info.id !== workspaceId;
});
});

Expand Down
27 changes: 15 additions & 12 deletions components/dashboard/src/data/workspaces/list-workspaces-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
* See License.AGPL.txt in the project root for license information.
*/

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

export type ListWorkspacesQueryResult = WorkspaceInfo[];
export type ListWorkspacesQueryResult = Workspace[];

type UseListWorkspacesQueryArgs = {
limit: number;
Expand All @@ -22,24 +22,27 @@ export const useListWorkspacesQuery = ({ limit }: UseListWorkspacesQueryArgs) =>
queryFn: async () => {
// TODO: Can we update the backend api to sort & rank pinned over non-pinned for us?
const [infos, pinned] = await Promise.all([
getGitpodService().server.getWorkspaces({
limit,
includeWithoutProject: true,
akosyakov marked this conversation as resolved.
Show resolved Hide resolved
workspaceClient.listWorkspaces({
pagination: {
pageSize: limit,
},
pinned: false,
organizationId: currentOrg.data?.id,
}),
// Additional fetch for pinned workspaces
// see also: https://github.com/gitpod-io/gitpod/issues/4488
getGitpodService().server.getWorkspaces({
limit,
pinnedOnly: true,
includeWithoutProject: true,
workspaceClient.listWorkspaces({
pagination: {
pageSize: limit,
},
pinned: true,
organizationId: currentOrg.data?.id,
}),
]);

// Merge both data sets into one unique (by ws id) array
const workspacesMap = new Map(infos.map((ws) => [ws.workspace.id, ws]));
const pinnedWorkspacesMap = new Map(pinned.map((ws) => [ws.workspace.id, ws]));
const workspacesMap = new Map(infos.workspaces.map((ws) => [ws.id, ws]));
const pinnedWorkspacesMap = new Map(pinned.workspaces.map((ws) => [ws.id, ws]));
const workspaces = Array.from(new Map([...workspacesMap, ...pinnedWorkspacesMap]).values());

return workspaces;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,81 @@
* See License.AGPL.txt in the project root for license information.
*/

import { WorkspaceInstance } from "@gitpod/gitpod-protocol";
import { Disposable } from "@gitpod/gitpod-protocol";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { getGitpodService } from "../../service/service";
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
import { useCurrentOrg } from "../organizations/orgs-query";
import { workspaceClient } from "../../service/public-api";
import { WatchWorkspaceStatusResponse, Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";

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

useEffect(() => {
const disposable = getGitpodService().registerClient({
onInstanceUpdate: (instance: WorkspaceInstance) => {
const queryKey = getListWorkspacesQueryKey(organizationId);
let foundWorkspaces = false;

// Update the workspace with the latest instance
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.map((info) => {
if (info.workspace.id !== instance.workspaceId) {
return info;
}

foundWorkspaces = true;
return {
...info,
latestInstance: instance,
};
});
const disposable = disposableWatchWorkspaceStatus(undefined, (status) => {
const queryKey = getListWorkspacesQueryKey(organizationId);
let foundWorkspaces = false;

// Update the workspace with the latest instance
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.map((info) => {
if (info.id !== status.workspaceId) {
return info;
}
foundWorkspaces = true;
const workspace = new Workspace(info);
workspace.status = status.status;
info.status = status.status;
return workspace;
});
});

if (!foundWorkspaces) {
// If the instance was for a workspace we don't have, it should get returned w/ an updated query
queryClient.invalidateQueries({ queryKey });
}
},
if (!foundWorkspaces) {
// If the instance was for a workspace we don't have, it should get returned w/ an updated query
queryClient.invalidateQueries({ queryKey });
}
});

return () => {
disposable.dispose();
};
}, [organizationId, queryClient]);
};

export const disposableWatchWorkspaceStatus = (
mustard-mh marked this conversation as resolved.
Show resolved Hide resolved
workspaceId: string | undefined,
cb: (response: WatchWorkspaceStatusResponse) => void,
): Disposable => {
const MAX_BACKOFF = 60000;
const BASE_BACKOFF = 3000;
let backoff = BASE_BACKOFF;
const abortController = new AbortController();

(async () => {
while (!abortController.signal.aborted) {
try {
const it = workspaceClient.watchWorkspaceStatus(
{ workspaceId },
{
signal: abortController.signal,
},
);
for await (const response of it) {
cb(response);
backoff = BASE_BACKOFF;
}
} catch (e) {
backoff = Math.min(2 * backoff, MAX_BACKOFF);
console.error("failed to watch workspace status, retrying", e);
}
const jitter = Math.random() * 0.3 * backoff;
const delay = backoff + jitter;
await new Promise((resolve) => setTimeout(resolve, delay));
}
})();
return {
dispose: () => abortController.abort(),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getGitpodService } from "../../service/service";
import { getListWorkspacesQueryKey, ListWorkspacesQueryResult } from "./list-workspaces-query";
import { useCurrentOrg } from "../organizations/orgs-query";
import { Workspace } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";

type ToggleWorkspacePinnedArgs = {
workspaceId: string;
Expand All @@ -27,17 +28,12 @@ export const useToggleWorkspacedPinnedMutation = () => {
// Update workspace.pinned to account for the toggle so it's reflected immediately
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspaceData) => {
return oldWorkspaceData?.map((info) => {
if (info.workspace.id !== workspaceId) {
if (info.id !== workspaceId) {
return info;
}

return {
...info,
workspace: {
...info.workspace,
pinned: !info.workspace.pinned,
},
};
const workspace = new Workspace(info);
workspace.pinned = !workspace.pinned;
return workspace;
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* See License.AGPL.txt in the project root for license information.
*/

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

type ToggleWorkspaceSharedArgs = {
workspaceId: string;
level: GitpodServer.AdmissionLevel;
level: AdmissionLevel;
};

export const useToggleWorkspaceSharedMutation = () => {
Expand All @@ -21,25 +21,32 @@ export const useToggleWorkspaceSharedMutation = () => {

return useMutation({
mutationFn: async ({ workspaceId, level }: ToggleWorkspaceSharedArgs) => {
return await getGitpodService().server.controlAdmission(workspaceId, level);
if (level === AdmissionLevel.UNSPECIFIED) {
return;
}
return await getGitpodService().server.controlAdmission(
workspaceId,
level === AdmissionLevel.EVERYONE ? "everyone" : "owner",
);
},
onSuccess: (_, { workspaceId, level }) => {
if (level === AdmissionLevel.UNSPECIFIED) {
return;
}
const queryKey = getListWorkspacesQueryKey(org.data?.id);

// Update workspace.shareable to the level we set so it's reflected immediately
queryClient.setQueryData<ListWorkspacesQueryResult>(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.map((info) => {
if (info.workspace.id !== workspaceId) {
if (info.id !== workspaceId) {
return info;
}

return {
...info,
workspace: {
...info.workspace,
shareable: level === "everyone" ? true : false,
},
};
const workspace = new Workspace(info);
if (workspace.status) {
workspace.status.admission = level;
}
return workspace;
});
});

Expand Down
Loading
Loading