Skip to content

Commit

Permalink
[dashboard] Better surface Prebuild errors & fix all 'Run Prebuild' b…
Browse files Browse the repository at this point in the history
…uttons
  • Loading branch information
jankeromnes authored and roboquat committed Oct 1, 2021
1 parent bbbc3c1 commit 5503c9b
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 101 deletions.
3 changes: 3 additions & 0 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/components/WorkspaceLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]);

Expand Down
32 changes: 28 additions & 4 deletions components/dashboard/src/projects/Prebuild.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -27,6 +29,7 @@ export default function () {

const [ prebuild, setPrebuild ] = useState<PrebuildWithStatus | undefined>();
const [ prebuildInstance, setPrebuildInstance ] = useState<WorkspaceInstance | undefined>();
const [ isRerunningPrebuild, setIsRerunningPrebuild ] = useState<boolean>(false);

useEffect(() => {
if (!teams || !projectName || !prebuildId) {
Expand Down Expand Up @@ -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 <>
Expand All @@ -88,9 +107,14 @@ export default function () {
<div className="h-20 px-6 bg-gray-50 dark:bg-gray-800 border-t border-gray-200 dark:border-gray-600 flex space-x-2">
{prebuildInstance && <PrebuildInstanceStatus prebuildInstance={prebuildInstance} />}
<div className="flex-grow" />
{prebuildInstance?.status.phase === "stopped"
? <a className="my-auto" href={gitpodHostUrl.withContext(`${prebuild?.info.changeUrl}`).toString()}><button>New Workspace</button></a>
: <button disabled={true}>New Workspace</button>}
{(prebuild?.status === 'aborted' || prebuild?.status === 'timeout' || !!prebuild?.error)
? <button className="flex items-center space-x-2" disabled={isRerunningPrebuild} onClick={rerunPrebuild}>
{isRerunningPrebuild && <img className="h-4 w-4 animate-spin filter brightness-150" src={Spinner} />}
<span>Rerun Prebuild ({prebuild.info.branch})</span>
</button>
: (prebuild?.status === 'available'
? <a className="my-auto" href={gitpodHostUrl.withContext(`${prebuild?.info.changeUrl}`).toString()}><button>New Workspace ({prebuild?.info.branch})</button></a>
: <button disabled={true}>New Workspace ({prebuild?.info.branch})</button>)}
</div>
</div>
</div>
Expand Down
99 changes: 46 additions & 53 deletions components/dashboard/src/projects/Prebuilds.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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);
Expand All @@ -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)]);
}
}
});
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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);
Expand All @@ -159,7 +153,8 @@ export default function () {
<div className="py-3 pl-3">
<DropDown prefix="Prebuild Status: " contextMenuWidth="w-32" entries={statusFilterEntries()} />
</div>
<button disabled={!project} onClick={() => triggerPrebuild(null)} className="ml-2">Trigger Prebuild</button>
{(!!project && prebuilds.length === 0) &&
<button onClick={() => triggerPrebuild(null)} className="ml-2">Run Prebuild</button>}
</div>
<ItemsList className="mt-2">
<Item header={true} className="grid grid-cols-3">
Expand All @@ -175,13 +170,13 @@ export default function () {
</Item>
{prebuilds.filter(filter).sort(prebuildSorter).map((p, index) => <Item key={`prebuild-${p.info.id}`} className="grid grid-cols-3">
<ItemField className="flex items-center">
<div className="cursor-pointer" onClick={() => openPrebuild(p.info)}>
<a href={`/${!!team ? 't/'+team.slug : 'projects'}/${projectName}/${p.info.id}`} className="cursor-pointer">
<div className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase mb-1">
<div className="inline-block align-text-bottom mr-2 w-4 h-4">{prebuildStatusIcon(p.status)}</div>
{prebuildStatusLabel(p.status)}
<div className="inline-block align-text-bottom mr-2 w-4 h-4">{prebuildStatusIcon(p)}</div>
{prebuildStatusLabel(p)}
</div>
<p>{p.info.startedByAvatar && <img className="rounded-full w-4 h-4 inline-block align-text-bottom mr-2" src={p.info.startedByAvatar || ''} alt={p.info.startedBy} />}Triggered {formatDate(p.info.startedAt)}</p>
</div>
</a>
</ItemField>
<ItemField className="flex items-center">
<div>
Expand All @@ -203,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 (<span className="font-medium text-red-500 uppercase">failed</span>);
}
switch (prebuild?.status) {
case undefined: // Fall through
case "queued":
return (<span className="font-medium text-orange-500 uppercase">pending</span>);
case "building":
return (<span className="font-medium text-blue-500 uppercase">running</span>);
case "aborted": // Fall through
case "timeout":
return (<span className="font-medium text-red-500 uppercase">failed</span>);
case "available":
return (<span className="font-medium text-green-500 uppercase">ready</span>);
case "building":
return (<span className="font-medium text-blue-500 uppercase">running</span>);
case "queued":
return (<span className="font-medium text-orange-500 uppercase">pending</span>);
default:
break;
}
}

export function prebuildStatusIcon(status: PrebuiltWorkspaceState | undefined) {
switch (status) {
case "aborted":
return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.70711 5.29289C6.31658 4.90237 5.68342 4.90237 5.29289 5.29289C4.90237 5.68342 4.90237 6.31658 5.29289 6.70711L6.58579 8L5.29289 9.29289C4.90237 9.68342 4.90237 10.3166 5.29289 10.7071C5.68342 11.0976 6.31658 11.0976 6.70711 10.7071L8 9.41421L9.29289 10.7071C9.68342 11.0976 10.3166 11.0976 10.7071 10.7071C11.0976 10.3166 11.0976 9.68342 10.7071 9.29289L9.41421 8L10.7071 6.70711C11.0976 6.31658 11.0976 5.68342 10.7071 5.29289C10.3166 4.90237 9.68342 4.90237 9.29289 5.29289L8 6.58579L6.70711 5.29289Z" fill="#F87171" />
</svg>)
case "available":
return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM11.7071 6.70711C12.0976 6.31658 12.0976 5.68342 11.7071 5.29289C11.3166 4.90237 10.6834 4.90237 10.2929 5.29289L7 8.58578L5.7071 7.29289C5.31658 6.90237 4.68342 6.90237 4.29289 7.29289C3.90237 7.68342 3.90237 8.31658 4.29289 8.70711L6.29289 10.7071C6.68342 11.0976 7.31658 11.0976 7.7071 10.7071L11.7071 6.70711Z" fill="#84CC16" />
</svg>);
case "building":
return (<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99609 16C13.4144 16 16.9961 12.4183 16.9961 8C16.9961 3.58172 13.4144 0 8.99609 0C4.57781 0 0.996094 3.58172 0.996094 8C0.996094 12.4183 4.57781 16 8.99609 16ZM9.99609 4C9.99609 3.44772 9.54837 3 8.99609 3C8.4438 3 7.99609 3.44772 7.99609 4V8C7.99609 8.26522 8.10144 8.51957 8.28898 8.70711L11.1174 11.5355C11.5079 11.9261 12.1411 11.9261 12.5316 11.5355C12.9221 11.145 12.9221 10.5118 12.5316 10.1213L9.99609 7.58579V4Z" fill="#60A5FA" />
</svg>);
export function prebuildStatusIcon(prebuild?: PrebuildWithStatus) {
if (prebuild?.error) {
return <img className="h-4 w-4" src={StatusFailed} />;
}
switch (prebuild?.status) {
case undefined: // Fall through
case "queued":
return (<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM5 6C5 5.44772 5.44772 5 6 5C6.55228 5 7 5.44772 7 6V10C7 10.5523 6.55228 11 6 11C5.44772 11 5 10.5523 5 10V6ZM10 5C9.44771 5 9 5.44772 9 6V10C9 10.5523 9.44771 11 10 11C10.5523 11 11 10.5523 11 10V6C11 5.44772 10.5523 5 10 5Z" fill="#FBBF24" />
</svg>);
default:
break;
return <img className="h-4 w-4" src={StatusPaused} />;
case "building":
return <img className="h-4 w-4" src={StatusRunning} />;
case "aborted": // Fall through
case "timeout":
return <img className="h-4 w-4" src={StatusFailed} />;
case "available":
return <img className="h-4 w-4" src={StatusDone} />;
}
}

Expand Down Expand Up @@ -285,7 +278,7 @@ export function PrebuildInstanceStatus(props: { prebuildInstance?: WorkspaceInst
</div>;
break;
}
if (props.prebuildInstance?.status.conditions.failed) {
if (props.prebuildInstance?.status.conditions.failed || props.prebuildInstance?.status.conditions.headlessTaskFailed) {
status = <div className="flex space-x-1 items-center text-gitpod-red">
<img className="h-4 w-4" src={StatusFailed} />
<span>FAILED</span>
Expand Down
26 changes: 10 additions & 16 deletions components/dashboard/src/projects/Project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -211,18 +201,17 @@ export default function () {
</div>}
{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 && <img className="rounded-full w-4 h-4 inline-block align-text-bottom mr-2" src={branch.changeAuthorAvatar || ''} alt={branch.changeAuthor} />;
const statusIcon = prebuildStatusIcon(prebuild?.status);
const status = prebuildStatusLabel(prebuild?.status);
const statusIcon = prebuildStatusIcon(prebuild);
const status = prebuildStatusLabel(prebuild);

return <Item key={`branch-${index}-${branchName}`} className="grid grid-cols-3 group">
return <Item key={`branch-${index}-${branch.name}`} className="grid grid-cols-3 group">
<ItemField className="flex items-center">
<div>
<a href={branch.url}><div className="text-base text-gray-600 hover:text-gray-800 dark:text-gray-50 dark:hover:text-gray-200 font-medium mb-1">
{branchName}
{branch.name}
{branch.isDefault && (<span className="ml-2 self-center rounded-xl py-0.5 px-2 text-sm bg-blue-50 text-blue-40 dark:bg-blue-500 dark:text-blue-100">DEFAULT</span>)}
</div></a>
</div>
Expand All @@ -241,7 +230,12 @@ export default function () {
<a href={gitpodHostUrl.withContext(`${branch.url}`).toString()}>
<button className={`primary mr-2 py-2 opacity-0 group-hover:opacity-100`}>New Workspace</button>
</a>
<ItemFieldContextMenu className="py-0.5" menuEntries={branchContextMenu(branch)} />
<ItemFieldContextMenu className="py-0.5" menuEntries={(!prebuild || prebuild.status === 'aborted' || prebuild.status === 'timeout' || !!prebuild.error)
? [{
title: `${prebuild ? 'Rerun' : 'Run'} Prebuild (${branch.name})`,
onClick: () => triggerPrebuild(branch),
}]
: []} />
</ItemField>
</Item>
}
Expand Down
Loading

0 comments on commit 5503c9b

Please sign in to comment.