diff --git a/components/dashboard/src/components/ContextMenu.tsx b/components/dashboard/src/components/ContextMenu.tsx index a8f36dabbd6f04..034fc3573fbc89 100644 --- a/components/dashboard/src/components/ContextMenu.tsx +++ b/components/dashboard/src/components/ContextMenu.tsx @@ -82,27 +82,28 @@ function ContextMenu(props: ContextMenuProps) { {expanded ?
- {props.menuEntries.map((e, index) => { - const clickable = e.href || e.onClick || e.link; - const entry =
- {e.customContent || <>
{e.title}
{e.active ?
: null}} -
- const key = `entry-${menuId}-${index}-${e.title}`; - if (e.link) { - return - {entry} - ; - } else if (e.href) { - return - {entry} - ; - } else { - return
- {entry} + {props.menuEntries.length === 0 + ?

No actions available

+ : props.menuEntries.map((e, index) => { + const clickable = e.href || e.onClick || e.link; + const entry =
+ {e.customContent || <>
{e.title}
{e.active ?
: null}}
- } - - })} + const key = `entry-${menuId}-${index}-${e.title}`; + if (e.link) { + return + {entry} + ; + } else if (e.href) { + return + {entry} + ; + } else { + return
+ {entry} +
+ } + })}
: null diff --git a/components/dashboard/src/components/ItemsList.tsx b/components/dashboard/src/components/ItemsList.tsx index 7ef23b34bbb59c..e57a59e6bacf54 100644 --- a/components/dashboard/src/components/ItemsList.tsx +++ b/components/dashboard/src/components/ItemsList.tsx @@ -46,10 +46,10 @@ export function ItemFieldIcon(props: { } export function ItemFieldContextMenu(props: { - menuEntries?: ContextMenuEntry[]; + menuEntries: ContextMenuEntry[]; className?: string; }) { return
- {!props.menuEntries ? null : } +
; } diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx index 14e4a061c8af7e..fe22a310199668 100644 --- a/components/dashboard/src/components/PrebuildLogs.tsx +++ b/components/dashboard/src/components/PrebuildLogs.tsx @@ -110,6 +110,9 @@ export default function PrebuildLogs(props: PrebuildLogsProps) { getGitpodService().server.watchWorkspaceImageBuildLogs(workspace!.id); break; } + if (workspaceInstance?.status.conditions.headlessTaskFailed) { + setError(new Error(workspaceInstance.status.conditions.headlessTaskFailed)); + } if (workspaceInstance?.status.conditions.failed) { setError(new Error(workspaceInstance.status.conditions.failed)); } diff --git a/components/dashboard/src/components/WorkspaceLogs.tsx b/components/dashboard/src/components/WorkspaceLogs.tsx index 98e4e153f353cc..ae6be25bb1957c 100644 --- a/components/dashboard/src/components/WorkspaceLogs.tsx +++ b/components/dashboard/src/components/WorkspaceLogs.tsx @@ -75,7 +75,7 @@ export default function WorkspaceLogs(props: WorkspaceLogsProps) { useEffect(() => { if (terminalRef.current && props.errorMessage) { - terminalRef.current.write(`\r\n\u001b[38;5;196m${props.errorMessage}\u001b[0m`); + terminalRef.current.write(`\r\n\u001b[38;5;196m${props.errorMessage}\u001b[0m\r\n`); } }, [ terminalRef.current, props.errorMessage ]); diff --git a/components/dashboard/src/projects/Prebuild.tsx b/components/dashboard/src/projects/Prebuild.tsx index c02318c061e4c4..0fc935dce08799 100644 --- a/components/dashboard/src/projects/Prebuild.tsx +++ b/components/dashboard/src/projects/Prebuild.tsx @@ -7,15 +7,17 @@ import moment from "moment"; import { PrebuildWithStatus, WorkspaceInstance } from "@gitpod/gitpod-protocol"; import { useContext, useEffect, useState } from "react"; -import { useLocation, useRouteMatch } from "react-router"; +import { useHistory, useLocation, useRouteMatch } from "react-router"; import Header from "../components/Header"; import PrebuildLogs from "../components/PrebuildLogs"; +import Spinner from "../icons/Spinner.svg"; import { getGitpodService, gitpodHostUrl } from "../service/service"; import { TeamsContext, getCurrentTeam } from "../teams/teams-context"; import { PrebuildInstanceStatus } from "./Prebuilds"; import { shortCommitMessage } from "./render-utils"; export default function () { + const history = useHistory(); const location = useLocation(); const { teams } = useContext(TeamsContext); @@ -27,6 +29,7 @@ export default function () { const [ prebuild, setPrebuild ] = useState(); const [ prebuildInstance, setPrebuildInstance ] = useState(); + const [ isRerunningPrebuild, setIsRerunningPrebuild ] = useState(false); useEffect(() => { if (!teams || !projectName || !prebuildId) { @@ -76,6 +79,22 @@ export default function () { setPrebuildInstance(instance); } + const rerunPrebuild = async () => { + if (!prebuild) { + return; + } + setIsRerunningPrebuild(true); + try { + await getGitpodService().server.triggerPrebuild(prebuild.info.projectId, prebuild.info.branch); + // TODO: Open a Prebuilds page that's specific to `prebuild.info.branch`? + history.push(`/${!!team ? 't/'+team.slug : 'projects'}/${projectName}/prebuilds`); + } catch (error) { + console.error('Could not rerun prebuild', error); + } finally { + setIsRerunningPrebuild(false); + } + } + useEffect(() => { document.title = 'Prebuild — Gitpod' }, []); return <> @@ -88,9 +107,14 @@ export default function () {
{prebuildInstance && }
- {prebuildInstance?.status.phase === "stopped" - ? - : } + {(prebuild?.status === 'aborted' || prebuild?.status === 'timeout' || !!prebuild?.error) + ? + : (prebuild?.status === 'available' + ? + : )}
diff --git a/components/dashboard/src/projects/Prebuilds.tsx b/components/dashboard/src/projects/Prebuilds.tsx index 0ede7bf2bac5e2..553ab6d61c2689 100644 --- a/components/dashboard/src/projects/Prebuilds.tsx +++ b/components/dashboard/src/projects/Prebuilds.tsx @@ -5,9 +5,9 @@ */ import moment from "moment"; -import { PrebuildInfo, PrebuildWithStatus, PrebuiltWorkspaceState, Project, WorkspaceInstance } from "@gitpod/gitpod-protocol"; +import { PrebuildWithStatus, PrebuiltWorkspaceState, Project, WorkspaceInstance } from "@gitpod/gitpod-protocol"; import { useContext, useEffect, useState } from "react"; -import { useHistory, useLocation, useRouteMatch } from "react-router"; +import { useLocation, useRouteMatch } from "react-router"; import Header from "../components/Header"; import DropDown, { DropDownEntry } from "../components/DropDown"; import { ItemsList, Item, ItemField, ItemFieldContextMenu } from "../components/ItemsList"; @@ -22,7 +22,6 @@ import { ContextMenuEntry } from "../components/ContextMenu"; import { shortCommitMessage } from "./render-utils"; export default function () { - const history = useHistory(); const location = useLocation(); const { teams } = useContext(TeamsContext); @@ -45,7 +44,7 @@ export default function () { const registration = getGitpodService().registerClient({ onPrebuildUpdate: (update: PrebuildWithStatus) => { if (update.info.projectId === project.id) { - setPrebuilds(prev => [update, ...prev.filter(p => p.info.id !== update.info.id)]) + setPrebuilds(prev => [update, ...prev.filter(p => p.info.id !== update.info.id)]); } } }); @@ -77,18 +76,17 @@ export default function () { }, [teams]); const prebuildContextMenu = (p: PrebuildWithStatus) => { - const running = p.status === "building"; + const isFailed = p.status === "aborted" || p.status === "timeout" || !!p.error; + const isRunning = p.status === "building"; const entries: ContextMenuEntry[] = []; - entries.push({ - title: "View Prebuild", - onClick: () => openPrebuild(p.info) - }); - entries.push({ - title: "Trigger Prebuild", - onClick: () => triggerPrebuild(p.info.branch), - separator: running - }); - if (running) { + if (isFailed) { + entries.push({ + title: `Rerun Prebuild (${p.info.branch})`, + onClick: () => triggerPrebuild(p.info.branch), + separator: isRunning + }); + } + if (isRunning) { entries.push({ title: "Cancel Prebuild", customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300', @@ -131,10 +129,6 @@ export default function () { return -1; } - const openPrebuild = (pb: PrebuildInfo) => { - history.push(`/${!!team ? 't/'+team.slug : 'projects'}/${projectName}/${pb.id}`); - } - const triggerPrebuild = (branchName: string | null) => { if (project) { getGitpodService().server.triggerPrebuild(project.id, branchName); @@ -159,7 +153,8 @@ export default function () {
- + {(!!project && prebuilds.length === 0) && + } @@ -171,18 +166,17 @@ export default function () { Branch - {prebuilds.filter(filter).sort(prebuildSorter).map((p, index) => -
openPrebuild(p.info)}> +
-
{prebuildStatusIcon(p.status)}
- {prebuildStatusLabel(p.status)} +
{prebuildStatusIcon(p)}
+ {prebuildStatusLabel(p)}

{p.info.startedByAvatar && {p.info.startedBy}}Triggered {formatDate(p.info.startedAt)}

-
+
@@ -204,41 +198,39 @@ export default function () { ; } -export function prebuildStatusLabel(status: PrebuiltWorkspaceState | undefined) { - switch (status) { - case "aborted": +export function prebuildStatusLabel(prebuild?: PrebuildWithStatus) { + if (prebuild?.error) { + return (failed); + } + switch (prebuild?.status) { + case undefined: // Fall through + case "queued": + return (pending); + case "building": + return (running); + case "aborted": // Fall through + case "timeout": return (failed); case "available": return (ready); - case "building": - return (running); - case "queued": - return (pending); - default: - break; } } -export function prebuildStatusIcon(status: PrebuiltWorkspaceState | undefined) { - switch (status) { - case "aborted": - return ( - - ) - case "available": - return ( - - ); - case "building": - return ( - - ); +export function prebuildStatusIcon(prebuild?: PrebuildWithStatus) { + if (prebuild?.error) { + return ; + } + switch (prebuild?.status) { + case undefined: // Fall through case "queued": - return ( - - ); - default: - break; + return ; + case "building": + return ; + case "aborted": // Fall through + case "timeout": + return ; + case "available": + return ; } } @@ -262,7 +254,8 @@ export function PrebuildInstanceStatus(props: { prebuildInstance?: WorkspaceInst case 'creating': // Fall through case 'initializing': // Fall through case 'running': // Fall through - case 'interrupted': + case 'interrupted': // Fall through + case 'stopping': status =
RUNNING @@ -272,7 +265,6 @@ export function PrebuildInstanceStatus(props: { prebuildInstance?: WorkspaceInst Prebuild in progress ...
; break; - case 'stopping': // Fall through case 'stopped': status =
@@ -286,7 +278,7 @@ export function PrebuildInstanceStatus(props: { prebuildInstance?: WorkspaceInst
; break; } - if (props.prebuildInstance?.status.conditions.failed) { + if (props.prebuildInstance?.status.conditions.failed || props.prebuildInstance?.status.conditions.headlessTaskFailed) { status =
FAILED diff --git a/components/dashboard/src/projects/Project.tsx b/components/dashboard/src/projects/Project.tsx index 18796e06c7e80d..9b95e5a9b8541e 100644 --- a/components/dashboard/src/projects/Project.tsx +++ b/components/dashboard/src/projects/Project.tsx @@ -13,7 +13,6 @@ import { ItemsList, Item, ItemField, ItemFieldContextMenu } from "../components/ import { getGitpodService, gitpodHostUrl } from "../service/service"; import { TeamsContext, getCurrentTeam } from "../teams/teams-context"; import { prebuildStatusIcon, prebuildStatusLabel } from "./Prebuilds"; -import { ContextMenuEntry } from "../components/ContextMenu"; import { shortCommitMessage, toRemoteURL } from "./render-utils"; import Spinner from "../icons/Spinner.svg"; import NoAccess from "../icons/NoAccess.svg"; @@ -120,15 +119,6 @@ export default function () { }); }; - const branchContextMenu = (branch: Project.BranchDetails) => { - const entries: ContextMenuEntry[] = []; - entries.push({ - title: "Rerun Prebuild", - onClick: () => triggerPrebuild(branch), - }); - return entries; - } - const lastPrebuild = (branch: Project.BranchDetails) => { const lastPrebuild = lastPrebuilds.get(branch.name); if (!lastPrebuild) { @@ -203,7 +193,6 @@ export default function () { Prebuild - {isLoadingBranches &&
@@ -212,18 +201,17 @@ export default function () {
} {branches.filter(filter).slice(0, 10).map((branch, index) => { - const branchName = branch.name; const prebuild = lastPrebuild(branch); // this might lazily trigger fetching of prebuild details const avatar = branch.changeAuthorAvatar && {branch.changeAuthor}; - const statusIcon = prebuildStatusIcon(prebuild?.status); - const status = prebuildStatusLabel(prebuild?.status); + const statusIcon = prebuildStatusIcon(prebuild); + const status = prebuildStatusLabel(prebuild); - return + return @@ -242,7 +230,12 @@ export default function () { - + triggerPrebuild(branch), + }] + : []} /> } diff --git a/components/dashboard/src/projects/Projects.tsx b/components/dashboard/src/projects/Projects.tsx index 0c36782c37f4f4..cd0112a79454dd 100644 --- a/components/dashboard/src/projects/Projects.tsx +++ b/components/dashboard/src/projects/Projects.tsx @@ -14,13 +14,10 @@ import { useContext, useEffect, useState } from "react"; import { getGitpodService } from "../service/service"; import { getCurrentTeam, TeamsContext } from "../teams/teams-context"; import { ThemeContext } from "../theme-context"; -import { PrebuildWithStatus, PrebuiltWorkspaceState, Project } from "@gitpod/gitpod-protocol"; +import { PrebuildWithStatus, Project } from "@gitpod/gitpod-protocol"; import { toRemoteURL } from "./render-utils"; import ContextMenu from "../components/ContextMenu"; -import StatusDone from "../icons/StatusDone.svg"; -import StatusPaused from "../icons/StatusPaused.svg"; -import StatusRunning from "../icons/StatusRunning.svg"; -import StatusFailed from "../icons/StatusFailed.svg"; +import { prebuildStatusIcon } from "./Prebuilds"; export default function () { const location = useLocation(); @@ -78,21 +75,6 @@ export default function () { const teamOrUserSlug = !!team ? 't/'+team.slug : 'projects'; - const getPrebuildStatusIcon = (status: PrebuiltWorkspaceState) => { - switch (status) { - case undefined: // Fall through - case "queued": - return StatusPaused; - case "building": - return StatusRunning; - case "aborted": // Fall through - case "timeout": - return StatusFailed; - case "available": - return StatusDone; - } - } - return <>
{projects.length === 0 && ( @@ -160,11 +142,11 @@ export default function () {
{lastPrebuilds.get(p.id) ? (
- - -
{lastPrebuilds.get(p.id)?.info?.branch}
- · -
{moment(lastPrebuilds.get(p.id)?.info?.startedAt, "YYYYMMDD").fromNow()}
+ + {prebuildStatusIcon(lastPrebuilds.get(p.id))} +
{lastPrebuilds.get(p.id)?.info?.branch}
+ · +
{moment(lastPrebuilds.get(p.id)?.info?.startedAt, "YYYYMMDD").fromNow()}
View All →
diff --git a/components/dashboard/src/settings/EnvironmentVariables.tsx b/components/dashboard/src/settings/EnvironmentVariables.tsx index 96775c887885a3..b0e1f4c3078bb7 100644 --- a/components/dashboard/src/settings/EnvironmentVariables.tsx +++ b/components/dashboard/src/settings/EnvironmentVariables.tsx @@ -216,7 +216,6 @@ export default function EnvVars() { Name Scope - {envVars.map(variable => { return diff --git a/components/dashboard/src/teams/Members.tsx b/components/dashboard/src/teams/Members.tsx index c57902714eed2b..28c47e02c39c5a 100644 --- a/components/dashboard/src/teams/Members.tsx +++ b/components/dashboard/src/teams/Members.tsx @@ -151,7 +151,6 @@ export default function() { Role - {filteredMembers.length === 0 @@ -190,7 +189,7 @@ export default function() { customFontStyle: 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300', onClick: () => removeTeamMember(m.userId) }] - : undefined)} /> + : [])} /> )} diff --git a/components/ee/db-sync/src/tests/basic-replication.spec.db.ts b/components/ee/db-sync/src/tests/basic-replication.spec.db.ts index 5bdbe5591dc0b1..4d05717ea9d987 100644 --- a/components/ee/db-sync/src/tests/basic-replication.spec.db.ts +++ b/components/ee/db-sync/src/tests/basic-replication.spec.db.ts @@ -64,7 +64,8 @@ describe('Basic unidirectional replication', () => { ]); }) - it('Should replicate with a start date', async () => { + it('Should replicate with a start date', async function () { + this.timeout(10000); await query(source, `INSERT INTO names VALUES ("9fa18735-c43b-4651-81c5-ddfbdee1035c", "Foo", "1970-01-01 00:00:01.001", 0)`) await query(source, `INSERT INTO names VALUES ("9fa18735-c43b-4651-81c5-ddfbdee1035d", "Bar", "1980-01-01 00:00:01.001", 0)`) diff --git a/components/gitpod-db/src/typeorm/workspace-db-impl.ts b/components/gitpod-db/src/typeorm/workspace-db-impl.ts index 0a4047bcde513a..c870dc10fe692e 100644 --- a/components/gitpod-db/src/typeorm/workspace-db-impl.ts +++ b/components/gitpod-db/src/typeorm/workspace-db-impl.ts @@ -567,6 +567,7 @@ export abstract class AbstractTypeORMWorkspaceDBImpl implements WorkspaceDB { return await repo.save(pws as DBPrebuiltWorkspace); } + // Find the (last triggered) prebuild for a given commit public async findPrebuiltWorkspaceByCommit(cloneURL: string, commit: string): Promise { if (!commit || !cloneURL) { return undefined; diff --git a/components/gitpod-protocol/src/teams-projects-protocol.ts b/components/gitpod-protocol/src/teams-projects-protocol.ts index b96f800512047f..345d4b5495393d 100644 --- a/components/gitpod-protocol/src/teams-projects-protocol.ts +++ b/components/gitpod-protocol/src/teams-projects-protocol.ts @@ -57,6 +57,7 @@ export namespace Project { export interface PrebuildWithStatus { info: PrebuildInfo; status: PrebuiltWorkspaceState; + error?: string; } export interface PrebuildInfo { @@ -93,7 +94,6 @@ export namespace PrebuildInfo { export interface StartPrebuildResult { wsid: string; done: boolean; - didFinish?: boolean; } export interface Team { diff --git a/components/server/ee/src/prebuilds/prebuild-manager.ts b/components/server/ee/src/prebuilds/prebuild-manager.ts index 60e5a38bf2f873..ea4bc56d75a3f3 100644 --- a/components/server/ee/src/prebuilds/prebuild-manager.ts +++ b/components/server/ee/src/prebuilds/prebuild-manager.ts @@ -67,7 +67,10 @@ export class PrebuildManager { try { const existingPB = await this.workspaceDB.trace({ span }).findPrebuiltWorkspaceByCommit(cloneURL, commit); if (!!existingPB) { - return { wsid: existingPB.buildWorkspaceId, done: true, didFinish: existingPB.state === 'available' }; + // If the prebuild is failed, we want to retrigger it. + if (existingPB.state !== 'aborted' && existingPB.state !== 'timeout' && !existingPB.error) { + return { wsid: existingPB.buildWorkspaceId, done: true }; + } } const contextParser = this.getContextParserFor(contextURL); diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index e9e310bf76cc8a..8c742bf4a40ae9 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -694,6 +694,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl p.id)]); - result.push(...infos.map(info => ({ info, status: prebuilds.find(p => p.id === info.id)?.state! }))); + result.push(...infos.map(info => { + const p = prebuilds.find(p => p.id === info.id)!; + const r: PrebuildWithStatus = { info, status: p.state }; + if (p.error) { + r.error = p.error; + } + return r; + })); } return result; }