-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prebuilds Page #4876
Prebuilds Page #4876
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,7 @@ const ConfigureProject = React.lazy(() => import(/* webpackPrefetch: true */ './ | |
const Projects = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Projects')); | ||
const Project = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Project')); | ||
const Prebuilds = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Prebuilds')); | ||
const Prebuild = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Prebuild')); | ||
const Settings = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Settings')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: We no longer need the Settings tab for this iteration, right? If so, let's remove this here or in a follow up issue. |
||
const InstallGitHubApp = React.lazy(() => import(/* webpackPrefetch: true */ './prebuilds/InstallGitHubApp')); | ||
const FromReferrer = React.lazy(() => import(/* webpackPrefetch: true */ './FromReferrer')); | ||
|
@@ -199,24 +200,24 @@ function App() { | |
<Route exact path={`/${team.slug}`}> | ||
<Redirect to={`/${team.slug}/projects`} /> | ||
</Route> | ||
<Route exact path={`/${team.slug}/:maybeProject/:subResource?`} render={(props) => { | ||
const { maybeProject, subResource } = props.match.params; | ||
<Route exact path={`/${team.slug}/:maybeProject/:resourceOrPrebuild?`} render={(props) => { | ||
const { maybeProject, resourceOrPrebuild } = props.match.params; | ||
if (maybeProject === "projects") { | ||
return <Projects />; | ||
} | ||
if (maybeProject === "members") { | ||
return <Members />; | ||
} | ||
AlexTugarev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (subResource === "configure") { | ||
if (resourceOrPrebuild === "configure") { | ||
return <ConfigureProject />; | ||
} | ||
if (subResource === "prebuilds") { | ||
if (resourceOrPrebuild === "prebuilds") { | ||
return <Prebuilds />; | ||
} | ||
if (subResource === "settings") { | ||
if (resourceOrPrebuild === "settings") { | ||
return <Settings />; | ||
} | ||
return <Project />; | ||
return resourceOrPrebuild ? <Prebuild /> : <Project />; | ||
}} /> | ||
</Route>)} | ||
<Route path="*" render={ | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -7,16 +7,16 @@ | |||||
import Separator from "./Separator"; | ||||||
|
||||||
export interface HeaderProps { | ||||||
title: string; | ||||||
subtitle: string; | ||||||
title: string | React.ReactElement; | ||||||
subtitle: string | React.ReactElement; | ||||||
} | ||||||
|
||||||
export default function Header(p: HeaderProps) { | ||||||
return <div className="lg:px-28 px-10 border-gray-200 dark:border-gray-800"> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
<div className="flex pb-8 pt-6"> | ||||||
<div className=""> | ||||||
<h1 className="tracking-tight">{p.title}</h1> | ||||||
<h2 className="tracking-wide">{p.subtitle}</h2> | ||||||
{typeof p.title === "string" ? (<h1 className="tracking-tight">{p.title}</h1>) : p.title} | ||||||
{typeof p.subtitle === "string" ? (<h2 className="tracking-wide">{p.subtitle}</h2>) : p.subtitle} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: Adding this comment to the closest related element. Do you think a) renaming the header title and adding a link to the project in the overview page could help here? Bringing this up as there's no other indication what this is list contains besides the active branches reference in the seach input below? Feel free to leave this as is for now.
|
||||||
</div> | ||||||
</div> | ||||||
<Separator /> | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ | |
|
||
import EventEmitter from "events"; | ||
import React, { Suspense, useEffect, useState } from "react"; | ||
import { Workspace, WorkspaceInstance, DisposableCollection, WorkspaceImageBuild, GitpodServer, HEADLESS_LOG_STREAM_STATUS_CODE_REGEX } from "@gitpod/gitpod-protocol"; | ||
import { Workspace, WorkspaceInstance, DisposableCollection, WorkspaceImageBuild, HEADLESS_LOG_STREAM_STATUS_CODE_REGEX } from "@gitpod/gitpod-protocol"; | ||
import { getGitpodService } from "../service/service"; | ||
|
||
const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs')); | ||
|
@@ -15,8 +15,7 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
const [ workspace, setWorkspace ] = useState<Workspace | undefined>(); | ||
const [ workspaceInstance, setWorkspaceInstance ] = useState<WorkspaceInstance | undefined>(); | ||
const [ error, setError ] = useState<Error | undefined>(); | ||
const logsEmitter = new EventEmitter(); | ||
const service = getGitpodService(); | ||
AlexTugarev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const [ logsEmitter ] = useState(new EventEmitter()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jankeromnes logs were sent to the wrong emitter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha, exciting! |
||
|
||
useEffect(() => { | ||
const disposables = new DisposableCollection(); | ||
|
@@ -25,12 +24,12 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
return; | ||
} | ||
try { | ||
const info = await service.server.getWorkspace(props.workspaceId); | ||
const info = await getGitpodService().server.getWorkspace(props.workspaceId); | ||
if (info.latestInstance) { | ||
setWorkspace(info.workspace); | ||
setWorkspaceInstance(info.latestInstance); | ||
} | ||
disposables.push(service.registerClient({ | ||
disposables.push(getGitpodService().registerClient({ | ||
onInstanceUpdate: setWorkspaceInstance, | ||
onWorkspaceImageBuildLogs: (info: WorkspaceImageBuild.StateInfo, content?: WorkspaceImageBuild.LogContent) => { | ||
if (!content) { | ||
|
@@ -40,7 +39,7 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
}, | ||
})); | ||
if (info.latestInstance) { | ||
disposables.push(watchHeadlessLogs(service.server, info.latestInstance.id, chunk => { | ||
disposables.push(watchHeadlessLogs(info.latestInstance.id, chunk => { | ||
logsEmitter.emit('logs', chunk); | ||
}, async () => workspaceInstance?.status.phase === 'stopped')); | ||
} | ||
|
@@ -64,7 +63,7 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
// Preparing means that we haven't actually started the workspace instance just yet, but rather | ||
// are still preparing for launch. This means we're building the Docker image for the workspace. | ||
case "preparing": | ||
service.server.watchWorkspaceImageBuildLogs(workspace!.id); | ||
getGitpodService().server.watchWorkspaceImageBuildLogs(workspace!.id); | ||
break; | ||
|
||
// Pending means the workspace does not yet consume resources in the cluster, but rather is looking for | ||
|
@@ -100,7 +99,7 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
|
||
// Stopped means the workspace ended regularly because it was shut down. | ||
case "stopped": | ||
service.server.watchWorkspaceImageBuildLogs(workspace!.id); | ||
getGitpodService().server.watchWorkspaceImageBuildLogs(workspace!.id); | ||
break; | ||
} | ||
if (workspaceInstance?.status.conditions.failed) { | ||
|
@@ -109,7 +108,6 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
}, [ workspaceInstance?.status.phase ]); | ||
|
||
return <> | ||
<div className="capitalize">{workspaceInstance?.status.phase}</div> | ||
<Suspense fallback={<div />}> | ||
<WorkspaceLogs classes="h-64 w-full" logsEmitter={logsEmitter} errorMessage={error?.message} /> | ||
</Suspense> | ||
|
@@ -121,7 +119,7 @@ export default function PrebuildLogs(props: { workspaceId?: string }) { | |
</>; | ||
} | ||
|
||
export function watchHeadlessLogs(server: GitpodServer, instanceId: string, onLog: (chunk: string) => void, checkIsDone: () => Promise<boolean>): DisposableCollection { | ||
export function watchHeadlessLogs(instanceId: string, onLog: (chunk: string) => void, checkIsDone: () => Promise<boolean>): DisposableCollection { | ||
const disposables = new DisposableCollection(); | ||
|
||
const startWatchingLogs = async () => { | ||
|
@@ -140,7 +138,7 @@ export function watchHeadlessLogs(server: GitpodServer, instanceId: string, onLo | |
let response: Response | undefined = undefined; | ||
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined = undefined; | ||
try { | ||
const logSources = await server.getHeadlessLog(instanceId); | ||
const logSources = await getGitpodService().server.getHeadlessLog(instanceId); | ||
// TODO(gpl) Only listening on first stream for now | ||
const streamIds = Object.keys(logSources.streams); | ||
if (streamIds.length < 1) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -219,11 +219,15 @@ export default function NewProject() { | |
<div className="text-base text-gray-900 dark:text-gray-50 font-medium rounded-xl whitespace-nowrap">{toSimpleName(r.name)}</div> | ||
<p>Updated {moment(r.updatedAt).fromNow()}</p> | ||
</div> | ||
<div className="flex justify-end"> | ||
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100"> | ||
<button className="primary" onClick={() => setSelectedRepo(r.name)}>Select</button> | ||
<div className="flex justify-end"> | ||
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100"> | ||
{!r.inUse ? ( | ||
<button className="primary" onClick={() => setSelectedRepo(r.name)}>Select</button> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue: Specs for repository selection are being updated and we no longer need the primary button on hover, see #4948. However, this is probably out of the scope these changes. For the secondary option below we can use a tooltip on hover to indicate a projects is already taken. 💡 |
||
) : ( | ||
<p className="my-auto">already taken</p> | ||
)} | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,78 @@ | ||||||
/** | ||||||
* Copyright (c) 2021 Gitpod GmbH. All rights reserved. | ||||||
* Licensed under the GNU Affero General Public License (AGPL). | ||||||
* See License-AGPL.txt in the project root for license information. | ||||||
*/ | ||||||
|
||||||
import moment from "moment"; | ||||||
import { PrebuildInfo } from "@gitpod/gitpod-protocol"; | ||||||
import { useContext, useEffect, useState } from "react"; | ||||||
import { useLocation, useRouteMatch } from "react-router"; | ||||||
import Header from "../components/Header"; | ||||||
import { getGitpodService } from "../service/service"; | ||||||
import { TeamsContext, getCurrentTeam } from "../teams/teams-context"; | ||||||
import { prebuildStatusIcon, prebuildStatusLabel } from "./Prebuilds"; | ||||||
import PrebuildLogs from "../components/PrebuildLogs"; | ||||||
import { shortCommitMessage } from "./render-utils"; | ||||||
|
||||||
export default function () { | ||||||
const { teams } = useContext(TeamsContext); | ||||||
const location = useLocation(); | ||||||
const match = useRouteMatch<{ team: string, project: string, prebuildId: string }>("/:team/:project/:prebuildId"); | ||||||
const projectName = match?.params?.project; | ||||||
const prebuildId = match?.params?.prebuildId; | ||||||
const team = getCurrentTeam(location, teams); | ||||||
|
||||||
const [prebuild, setPrebuild] = useState<PrebuildInfo | undefined>(); | ||||||
|
||||||
useEffect(() => { | ||||||
if (!team || !projectName || !prebuildId) { | ||||||
return; | ||||||
} | ||||||
(async () => { | ||||||
const prebuilds = await getGitpodService().server.findPrebuilds({ | ||||||
projectName, | ||||||
teamId: team.id, | ||||||
prebuildId | ||||||
}); | ||||||
setPrebuild(prebuilds[0]); | ||||||
})(); | ||||||
}, [team]); | ||||||
|
||||||
|
||||||
const renderTitle = () => { | ||||||
if (!prebuild) { | ||||||
return "unknown prebuild"; | ||||||
} | ||||||
return (<h1 className="tracking-tight">{prebuild.branch} <span className="text-gray-200">#{prebuild.branchPrebuildNumber}</span></h1>); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Minor color fix. 🖍️
Suggested change
|
||||||
}; | ||||||
|
||||||
const renderSubtitle = () => { | ||||||
if (!prebuild) { | ||||||
return ""; | ||||||
} | ||||||
const statusIcon = prebuildStatusIcon(prebuild.status); | ||||||
const status = prebuildStatusLabel(prebuild.status); | ||||||
const startedByAvatar = prebuild.startedByAvatar && <img className="rounded-full w-4 h-4 inline-block align-text-bottom mr-2" src={prebuild.startedByAvatar || ''} alt={prebuild.startedBy} />; | ||||||
return (<div className="flex"> | ||||||
<div className="text-base text-gray-900 dark:text-gray-50 font-medium uppercase"> | ||||||
<div className="inline-block align-text-bottom mr-2 w-4 h-4">{statusIcon}</div> | ||||||
{status} | ||||||
</div> | ||||||
<p className="mx-2 my-auto">·</p> | ||||||
<div className="my-auto"> | ||||||
<p>{startedByAvatar}Triggered {moment(prebuild.startedAt).fromNow()}</p> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: Is it expected to see the avatar missing from everywhere? |
||||||
</div> | ||||||
<p className="mx-2 my-auto">·</p> | ||||||
<div className="my-auto"> | ||||||
<p className="text-gray-500 dark:text-gray-50">{shortCommitMessage(prebuild.changeTitle)}</p> | ||||||
</div> | ||||||
</div>) | ||||||
}; | ||||||
|
||||||
return <> | ||||||
<Header title={renderTitle()} subtitle={renderSubtitle()} /> | ||||||
<div className="w-full"><PrebuildLogs workspaceId={prebuild?.buildWorkspaceId}/></div> | ||||||
</> | ||||||
|
||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: Fetching active branches and prebuilds requires a noticable amount of time, 5 seconds or more. Is there anything we could do in this iteration to make this smoother? If not, let's add a follow up issue for adding a loading indicator in a future iteration. ⏱️