-
+
{p.ws.workspaceId}
- {getProjectPath(WorkspaceAndInstance.toWorkspace(p.ws))}
+ {getProjectPath(workspace)}
diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx
index 1fcf3ee5e60949..db21d99e18370b 100644
--- a/components/dashboard/src/components/PrebuildLogs.tsx
+++ b/components/dashboard/src/components/PrebuildLogs.tsx
@@ -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"));
@@ -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,
diff --git a/components/dashboard/src/data/setup.tsx b/components/dashboard/src/data/setup.tsx
index 195fc8ab830141..bfdfd6d7dc0449 100644
--- a/components/dashboard/src/data/setup.tsx
+++ b/components/dashboard/src/data/setup.tsx
@@ -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"];
diff --git a/components/dashboard/src/data/workspaces/delete-inactive-workspaces-mutation.ts b/components/dashboard/src/data/workspaces/delete-inactive-workspaces-mutation.ts
index 027c46d50c75e9..20ad0c781b6726 100644
--- a/components/dashboard/src/data/workspaces/delete-inactive-workspaces-mutation.ts
+++ b/components/dashboard/src/data/workspaces/delete-inactive-workspaces-mutation.ts
@@ -40,7 +40,7 @@ export const useDeleteInactiveWorkspacesMutation = () => {
// Using the result of the mutationFn so we only remove workspaces that were delete
queryClient.setQueryData(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.filter((info) => {
- return !deletedWorkspaceIds.includes(info.workspace.id);
+ return !deletedWorkspaceIds.includes(info.id);
});
});
diff --git a/components/dashboard/src/data/workspaces/delete-workspace-mutation.ts b/components/dashboard/src/data/workspaces/delete-workspace-mutation.ts
index 4454f28ebe650f..5cb99b43e92678 100644
--- a/components/dashboard/src/data/workspaces/delete-workspace-mutation.ts
+++ b/components/dashboard/src/data/workspaces/delete-workspace-mutation.ts
@@ -27,7 +27,7 @@ export const useDeleteWorkspaceMutation = () => {
// Remove workspace from cache so it's reflected right away
queryClient.setQueryData(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.filter((info) => {
- return info.workspace.id !== workspaceId;
+ return info.id !== workspaceId;
});
});
diff --git a/components/dashboard/src/data/workspaces/list-workspaces-query.ts b/components/dashboard/src/data/workspaces/list-workspaces-query.ts
index 4b6170768dc8b5..0c3f2c5b5815db 100644
--- a/components/dashboard/src/data/workspaces/list-workspaces-query.ts
+++ b/components/dashboard/src/data/workspaces/list-workspaces-query.ts
@@ -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;
@@ -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,
+ 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;
diff --git a/components/dashboard/src/data/workspaces/listen-to-workspace-ws-messages.ts b/components/dashboard/src/data/workspaces/listen-to-workspace-ws-messages.ts
index e59986d332e9a4..9b46c12c929cbf 100644
--- a/components/dashboard/src/data/workspaces/listen-to-workspace-ws-messages.ts
+++ b/components/dashboard/src/data/workspaces/listen-to-workspace-ws-messages.ts
@@ -4,43 +4,41 @@
* 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(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(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 () => {
@@ -48,3 +46,39 @@ export const useListenToWorkspacesWSMessages = () => {
};
}, [organizationId, queryClient]);
};
+
+export const disposableWatchWorkspaceStatus = (
+ 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(),
+ };
+};
diff --git a/components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts b/components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts
index d6a1552a60582a..29449bfc692904 100644
--- a/components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts
+++ b/components/dashboard/src/data/workspaces/toggle-workspace-pinned-mutation.ts
@@ -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;
@@ -27,17 +28,12 @@ export const useToggleWorkspacedPinnedMutation = () => {
// Update workspace.pinned to account for the toggle so it's reflected immediately
queryClient.setQueryData(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;
});
});
diff --git a/components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts b/components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts
index 48cb1bc0b603b1..38b0acef8fb7cf 100644
--- a/components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts
+++ b/components/dashboard/src/data/workspaces/toggle-workspace-shared-mutation.ts
@@ -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 = () => {
@@ -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(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;
});
});
diff --git a/components/dashboard/src/data/workspaces/update-workspace-description-mutation.ts b/components/dashboard/src/data/workspaces/update-workspace-description-mutation.ts
index e0c6440cdbccea..a42033d413bf94 100644
--- a/components/dashboard/src/data/workspaces/update-workspace-description-mutation.ts
+++ b/components/dashboard/src/data/workspaces/update-workspace-description-mutation.ts
@@ -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 UpdateWorkspaceDescriptionArgs = {
workspaceId: string;
@@ -27,19 +28,15 @@ export const useUpdateWorkspaceDescriptionMutation = () => {
// pro-actively update workspace description rather than reload all workspaces
queryClient.setQueryData(queryKey, (oldWorkspacesData) => {
return oldWorkspacesData?.map((info) => {
- if (info.workspace.id !== workspaceId) {
+ if (info.id !== workspaceId) {
return info;
}
// TODO: Once the update description response includes an updated record,
// we can return that instead of having to know what to merge manually (same for other mutations)
- return {
- ...info,
- workspace: {
- ...info.workspace,
- description: newDescription,
- },
- };
+ const workspace = new Workspace(info);
+ workspace.name = newDescription;
+ return workspace;
});
});
diff --git a/components/dashboard/src/service/json-rpc-workspace-client.ts b/components/dashboard/src/service/json-rpc-workspace-client.ts
index e98c8de87ddcbe..1a06ec76af6e0c 100644
--- a/components/dashboard/src/service/json-rpc-workspace-client.ts
+++ b/components/dashboard/src/service/json-rpc-workspace-client.ts
@@ -12,11 +12,16 @@ import {
GetWorkspaceResponse,
WatchWorkspaceStatusRequest,
WatchWorkspaceStatusResponse,
+ ListWorkspacesRequest,
+ ListWorkspacesResponse,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { converter } from "./public-api";
import { getGitpodService } from "./service";
+import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
import { generateAsyncGenerator } from "@gitpod/gitpod-protocol/lib/generate-async-generator";
import { WorkspaceInstance } from "@gitpod/gitpod-protocol";
+import { parsePagination } from "@gitpod/gitpod-protocol/lib/public-api-pagination";
+import { validate as uuidValidate } from "uuid";
export class JsonRpcWorkspaceClient implements PromiseClient {
async getWorkspace(request: PartialMessage): Promise {
@@ -80,4 +85,27 @@ export class JsonRpcWorkspaceClient implements PromiseClient,
+ _options?: CallOptions,
+ ): Promise {
+ if (!request.organizationId || !uuidValidate(request.organizationId)) {
+ throw new ConnectError("organizationId is required", Code.InvalidArgument);
+ }
+ const { limit } = parsePagination(request.pagination, 50);
+ let resultTotal = 0;
+ const results = await getGitpodService().server.getWorkspaces({
+ limit,
+ pinnedOnly: request.pinned,
+ searchString: request.searchTerm,
+ organizationId: request.organizationId,
+ });
+ resultTotal = results.length;
+ const response = new ListWorkspacesResponse();
+ response.workspaces = results.map((info) => converter.toWorkspace(info));
+ response.pagination = new PaginationResponse();
+ response.pagination.total = resultTotal;
+ return response;
+ }
}
diff --git a/components/dashboard/src/start/StartWorkspace.tsx b/components/dashboard/src/start/StartWorkspace.tsx
index ee5fe3b72d42a0..483950a5c37a20 100644
--- a/components/dashboard/src/start/StartWorkspace.tsx
+++ b/components/dashboard/src/start/StartWorkspace.tsx
@@ -10,7 +10,6 @@ import {
RateLimiterError,
StartWorkspaceResult,
WorkspaceImageBuild,
- WorkspaceInstance,
} from "@gitpod/gitpod-protocol";
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
@@ -26,8 +25,9 @@ import { getGitpodService, gitpodHostUrl, getIDEFrontendService, IDEFrontendServ
import { StartPage, StartPhase, StartWorkspaceError } from "./StartPage";
import ConnectToSSHModal from "../workspaces/ConnectToSSHModal";
import Alert from "../components/Alert";
-import { converter, workspaceClient, workspacesService } from "../service/public-api";
+import { workspaceClient, workspacesService } from "../service/public-api";
import { GetWorkspaceRequest, Workspace, WorkspacePhase_Phase } from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
+import { disposableWatchWorkspaceStatus } from "../data/workspaces/listen-to-workspace-ws-messages";
const sessionId = v4();
@@ -125,11 +125,21 @@ export default class StartWorkspace extends React.Component {
+ if (resp.workspaceId !== this.state.workspace?.id || !resp.status) {
+ return;
+ }
+ this.onWorkspaceUpdate(
+ new Workspace({
+ ...this.state.workspace,
+ status: resp.status,
+ }),
+ );
+ });
+ this.toDispose.push(watchDispose);
this.toDispose.push(
getGitpodService().registerClient({
notifyDidOpenConnection: () => this.fetchWorkspaceInfo(undefined),
- onInstanceUpdate: (workspaceInstance: WorkspaceInstance) =>
- this.onInstanceUpdate(workspaceInstance),
}),
);
} catch (error) {
@@ -298,15 +308,6 @@ export default class StartWorkspace extends React.Component(undefined);
const existingWorkspaces = useMemo(() => {
- if (!workspaces.data || !CommitContext.is(workspaceContext.data)) {
+ if (!workspaces.data) {
return [];
}
return workspaces.data.filter(
(ws) =>
- ws.latestInstance?.status?.phase === "running" &&
- CommitContext.is(ws.workspace.context) &&
+ ws.status?.phase?.name === WorkspacePhase_Phase.RUNNING &&
CommitContext.is(workspaceContext.data) &&
- ws.workspace.context.repository.cloneUrl === workspaceContext.data.repository.cloneUrl &&
- ws.workspace.context.revision === workspaceContext.data.revision,
+ ws.status.gitStatus?.cloneUrl === workspaceContext.data?.repository.cloneUrl &&
+ ws.status?.gitStatus?.latestCommit === workspaceContext.data.revision,
);
}, [workspaces.data, workspaceContext.data]);
const [selectAccountError, setSelectAccountError] = useState(undefined);
@@ -432,15 +432,14 @@ export function CreateWorkspacePage() {
{createWorkspaceMutation.isStarting ? "Opening Workspace ..." : "Continue"}
-
{existingWorkspaces.length > 0 && !createWorkspaceMutation.isStarting && (