diff --git a/components/dashboard/src/admin/WorkspaceDetail.tsx b/components/dashboard/src/admin/WorkspaceDetail.tsx
index a6e003b3cc24fb..ff1dea0ccb4985 100644
--- a/components/dashboard/src/admin/WorkspaceDetail.tsx
+++ b/components/dashboard/src/admin/WorkspaceDetail.tsx
@@ -62,7 +62,12 @@ export default function WorkspaceDetail(props: { workspace: WorkspaceAndInstance
{user?.name || props.workspace.ownerId}
{workspace.shareable ? 'Enabled' : 'Disabled'}
-
+
{
+ getGitpodService().server.adminRestoreSoftDeletedWorkspace(workspace.workspaceId);
+ }
+ }] || undefined}>{workspace.softDeleted ? `'${workspace.softDeleted}' ${moment(workspace.softDeletedTime).fromNow()}` : 'No'}
diff --git a/components/gitpod-protocol/src/admin-protocol.ts b/components/gitpod-protocol/src/admin-protocol.ts
index 1137a045382989..61a6c4e9312b64 100644
--- a/components/gitpod-protocol/src/admin-protocol.ts
+++ b/components/gitpod-protocol/src/admin-protocol.ts
@@ -21,6 +21,7 @@ export interface AdminServer {
adminGetWorkspaces(req: AdminGetWorkspacesRequest): Promise>;
adminGetWorkspace(id: string): Promise;
adminForceStopWorkspace(id: string): Promise;
+ adminRestoreSoftDeletedWorkspace(id: string): Promise;
adminSetLicense(key: string): Promise;
diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts
index 956c0b94924cb0..854c24d8fbb2fd 100644
--- a/components/server/ee/src/workspace/gitpod-server-impl.ts
+++ b/components/server/ee/src/workspace/gitpod-server-impl.ts
@@ -506,6 +506,28 @@ export class GitpodServerEEImpl
await this.internalStopWorkspace({ span }, id, undefined, StopWorkspacePolicy.IMMEDIATELY);
}
+ async adminRestoreSoftDeletedWorkspace(id: string): Promise {
+ this.requireEELicense(Feature.FeatureAdminDashboard);
+
+ await this.guardAdminAccess("adminRestoreSoftDeletedWorkspace", {id}, Permission.ADMIN_WORKSPACES);
+
+ const span = opentracing.globalTracer().startSpan("adminRestoreSoftDeletedWorkspace");
+ await this.workspaceDb.trace({ span }).transaction(async db => {
+ const ws = await this.internalGetWorkspace(id, db);
+ if (!ws.softDeleted) {
+ return;
+ }
+ if (!!ws.contentDeletedTime) {
+ throw new ResponseError(ErrorCodes.NOT_FOUND, "The workspace content was already garbage-collected.");
+ }
+ // @ts-ignore
+ ws.softDeleted = null;
+ ws.softDeletedTime = '';
+ ws.pinned = true;
+ await db.store(ws);
+ });
+ }
+
protected async guardAdminAccess(method: string, params: any, requiredPermission: PermissionName) {
const user = this.checkAndBlockUser(method);
if (!this.authorizationService.hasPermission(user, requiredPermission)) {
diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts
index 23499ef557e5f0..d185403a73989d 100644
--- a/components/server/src/auth/rate-limiter.ts
+++ b/components/server/src/auth/rate-limiter.ts
@@ -106,6 +106,7 @@ function readConfig(): RateLimiterConfig {
"adminGetWorkspaces": { group: "default", points: 1 },
"adminGetWorkspace": { group: "default", points: 1 },
"adminForceStopWorkspace": { group: "default", points: 1 },
+ "adminRestoreSoftDeletedWorkspace": { group: "default", points: 1 },
"adminSetLicense": { group: "default", points: 1 },
"validateLicense": { group: "default", points: 1 },
diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts
index c5793b12f502cd..495f16999830ce 100644
--- a/components/server/src/workspace/gitpod-server-impl.ts
+++ b/components/server/src/workspace/gitpod-server-impl.ts
@@ -1507,6 +1507,10 @@ export class GitpodServerImpl {
+ throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
+ }
+
async adminSetLicense(key: string): Promise {
throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
}