diff --git a/components/dashboard/src/projects/Project.tsx b/components/dashboard/src/projects/Project.tsx index b656bda19985bd..a96b521feeb214 100644 --- a/components/dashboard/src/projects/Project.tsx +++ b/components/dashboard/src/projects/Project.tsx @@ -14,10 +14,11 @@ import { getGitpodService, gitpodHostUrl } from "../service/service"; import { TeamsContext, getCurrentTeam } from "../teams/teams-context"; import { prebuildStatusIcon, prebuildStatusLabel } from "./Prebuilds"; import { shortCommitMessage, toRemoteURL } from "./render-utils"; -import Spinner from "../icons/Spinner.svg"; +import { ReactComponent as Spinner } from "../icons/Spinner.svg"; import NoAccess from "../icons/NoAccess.svg"; import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error"; import { openAuthorizeWindow } from "../provider-utils"; +import Alert from "../components/Alert"; export default function () { const location = useLocation(); @@ -34,6 +35,8 @@ export default function () { const [isLoading, setIsLoading] = useState(false); const [isLoadingBranches, setIsLoadingBranches] = useState(false); const [branches, setBranches] = useState([]); + const [isConsideredInactive, setIsConsideredInactive] = useState(false); + const [isResuming, setIsResuming] = useState(false); const [prebuilds, setPrebuilds] = useState>(new Map()); const [prebuildLoaders] = useState>(new Set()); @@ -91,6 +94,7 @@ export default function () { // default branch on top of the rest const branches = details.branches.sort((a, b) => (b.isDefault as any) - (a.isDefault as any)) || []; setBranches(branches); + setIsConsideredInactive(!!details.isConsideredInactive); } } finally { setIsLoadingBranches(false); @@ -184,6 +188,22 @@ export default function () { return date ? dayjs(date).fromNow() : ""; }; + const resumePrebuilds = async () => { + if (!project) { + return; + } + try { + setIsResuming(true); + const response = await getGitpodService().server.triggerPrebuild(project.id, null); + setIsConsideredInactive(false); + history.push(`/${!!team ? "t/" + team.slug : "projects"}/${projectSlug}/${response.prebuildId}`); + } catch (error) { + console.error(error); + } finally { + setIsResuming(false); + } + }; + return ( <>
{isLoading && (
- +
)} @@ -259,9 +279,34 @@ export default function () { Prebuild + {isConsideredInactive && ( + {}} + showIcon={true} + className="flex rounded mb-2 w-full" + > + To reduce resource usage, prebuilds are automatically paused when not used for a + workspace after 7 days.{" "} + {isResuming && ( + <> + Resuming + + )} + {!isResuming && ( + resumePrebuilds()} + > + Resume prebuilds + + )} + + )} {isLoadingBranches && (
- + Fetching repository branches...
)} diff --git a/components/gitpod-protocol/src/teams-projects-protocol.ts b/components/gitpod-protocol/src/teams-projects-protocol.ts index a3b4a6db3b877f..eeb772bbad9e37 100644 --- a/components/gitpod-protocol/src/teams-projects-protocol.ts +++ b/components/gitpod-protocol/src/teams-projects-protocol.ts @@ -45,6 +45,7 @@ export namespace Project { export interface Overview { branches: BranchDetails[]; + isConsideredInactive?: boolean; } export namespace Overview { diff --git a/components/server/ee/src/prebuilds/prebuild-manager.ts b/components/server/ee/src/prebuilds/prebuild-manager.ts index 8d161ea8546b8d..e734f4b473a022 100644 --- a/components/server/ee/src/prebuilds/prebuild-manager.ts +++ b/components/server/ee/src/prebuilds/prebuild-manager.ts @@ -394,14 +394,7 @@ export class PrebuildManager { } private async shouldSkipInactiveProject(project: Project): Promise { - const usage = await this.projectService.getProjectUsage(project.id); - if (!usage?.lastWorkspaceStart) { - return false; - } - const now = Date.now(); - const lastUse = new Date(usage.lastWorkspaceStart).getTime(); - const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 1; // 1 week - return now - lastUse > inactiveProjectTime; + return await this.projectService.isProjectConsideredInactive(project.id); } private async shouldSkipInactiveRepository(ctx: TraceContext, cloneURL: 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 196646fa175a82..c42629f8f1d3f1 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -2620,6 +2620,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const context = (await this.contextParser.handle(ctx, user, contextURL)) as CommitContext; + // HACK: treat manual triggered prebuild as a reset for the inactivity state + await this.projectDB.updateProjectUsage(project.id, { + lastWorkspaceStart: new Date().toISOString(), + }); + const prebuild = await this.prebuildManager.startPrebuild(ctx, { context, user, diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index b8c946f336feae..067608cafe57c8 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -279,6 +279,17 @@ export class ProjectsService { return this.projectDB.getProjectUsage(projectId); } + async isProjectConsideredInactive(projectId: string): Promise { + const usage = await this.getProjectUsage(projectId); + if (!usage?.lastWorkspaceStart) { + return false; + } + const now = Date.now(); + const lastUse = new Date(usage.lastWorkspaceStart).getTime(); + const inactiveProjectTime = 1000 * 60 * 60 * 24 * 7 * 1; // 1 week + return now - lastUse > inactiveProjectTime; + } + async getPrebuildEvents(cloneUrl: string): Promise { const events = await this.webhookEventDB.findByCloneUrl(cloneUrl, 100); return events.map((we) => ({ diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 9d34e1092fbef5..b2797cbf4c0f78 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -2345,7 +2345,11 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { } await this.guardProjectOperation(user, projectId, "get"); try { - return await this.projectsService.getProjectOverviewCached(user, project); + const result = await this.projectsService.getProjectOverviewCached(user, project); + if (result) { + result.isConsideredInactive = await this.projectsService.isProjectConsideredInactive(project.id); + } + return result; } catch (error) { if (UnauthorizedError.is(error)) { throw new ResponseError(ErrorCodes.NOT_AUTHENTICATED, "Unauthorized", error.data);