Skip to content

Commit b404e72

Browse files
committed
Allow enabling/disabling Incremental Prebuilds in a Project's settings
1 parent 713a33e commit b404e72

File tree

19 files changed

+220
-60
lines changed

19 files changed

+220
-60
lines changed

components/dashboard/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { trackButtonOrAnchor, trackPathChange, trackLocation } from './Analytics
2121
import { User } from '@gitpod/gitpod-protocol';
2222
import * as GitpodCookie from '@gitpod/gitpod-protocol/lib/util/gitpod-cookie';
2323
import { Experiment } from './experiments';
24+
import ProjectSettings from './projects/ProjectSettings';
2425

2526
const Setup = React.lazy(() => import(/* webpackPrefetch: true */ './Setup'));
2627
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ './workspaces/Workspaces'));
@@ -298,6 +299,9 @@ function App() {
298299
<Route exact path="/projects" component={Projects} />
299300
<Route exact path="/projects/:projectName/:resourceOrPrebuild?" render={(props) => {
300301
const { resourceOrPrebuild } = props.match.params;
302+
if (resourceOrPrebuild === "settings") {
303+
return <ProjectSettings />;
304+
}
301305
if (resourceOrPrebuild === "configure") {
302306
return <ConfigureProject />;
303307
}
@@ -337,6 +341,9 @@ function App() {
337341
if (resourceOrPrebuild === "configure") {
338342
return <ConfigureProject />;
339343
}
344+
if (resourceOrPrebuild === "settings") {
345+
return <ProjectSettings />;
346+
}
340347
if (resourceOrPrebuild === "workspaces") {
341348
return <Workspaces />;
342349
}

components/dashboard/src/Menu.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7-
import { User, TeamMemberInfo } from "@gitpod/gitpod-protocol";
7+
import { User, TeamMemberInfo, Project } from "@gitpod/gitpod-protocol";
88
import { useContext, useEffect, useState } from "react";
99
import { Link } from "react-router-dom";
1010
import { useLocation, useRouteMatch } from "react-router";
@@ -21,6 +21,8 @@ import ContextMenu from "./components/ContextMenu";
2121
import Separator from "./components/Separator";
2222
import PillMenuItem from "./components/PillMenuItem";
2323
import TabMenuItem from "./components/TabMenuItem";
24+
import { getTeamSettingsMenu } from "./teams/TeamSettings";
25+
import { getProjectSettingsMenu } from "./projects/ProjectSettings";
2426

2527
interface Entry {
2628
title: string,
@@ -35,14 +37,14 @@ export default function Menu() {
3537
const location = useLocation();
3638

3739
const match = useRouteMatch<{ segment1?: string, segment2?: string, segment3?: string }>("/(t/)?:segment1/:segment2?/:segment3?");
38-
const projectName = (() => {
40+
const projectSlug = (() => {
3941
const resource = match?.params?.segment2;
4042
if (resource && !["projects", "members", "users", "workspaces", "settings"].includes(resource)) {
4143
return resource;
4244
}
4345
})();
4446
const prebuildId = (() => {
45-
const resource = projectName && match?.params?.segment3;
47+
const resource = projectSlug && match?.params?.segment3;
4648
if (resource !== "workspaces" && resource !== "prebuilds" && resource !== "settings" && resource !== "configure") {
4749
return resource;
4850
}
@@ -89,24 +91,25 @@ export default function Menu() {
8991
const teamOrUserSlug = !!team ? '/t/' + team.slug : '/projects';
9092
const leftMenu: Entry[] = (() => {
9193
// Project menu
92-
if (projectName) {
94+
if (projectSlug) {
9395
return [
9496
{
9597
title: 'Branches',
96-
link: `${teamOrUserSlug}/${projectName}`
98+
link: `${teamOrUserSlug}/${projectSlug}`,
9799
},
98100
{
99101
title: 'Workspaces',
100-
link: `${teamOrUserSlug}/${projectName}/workspaces`
102+
link: `${teamOrUserSlug}/${projectSlug}/workspaces`,
101103
},
102104
{
103105
title: 'Prebuilds',
104-
link: `${teamOrUserSlug}/${projectName}/prebuilds`
106+
link: `${teamOrUserSlug}/${projectSlug}/prebuilds`,
105107
},
106108
{
107-
title: 'Configuration',
108-
link: `${teamOrUserSlug}/${projectName}/configure`
109-
}
109+
title: 'Settings',
110+
link: `${teamOrUserSlug}/${projectSlug}/settings`,
111+
alternatives: getProjectSettingsMenu({ slug: projectSlug } as Project, team).flatMap(e => e.link),
112+
},
110113
];
111114
}
112115
// Team menu
@@ -132,6 +135,7 @@ export default function Menu() {
132135
teamSettingsList.push({
133136
title: 'Settings',
134137
link: `/t/${team.slug}/settings`,
138+
alternatives: getTeamSettingsMenu(team).flatMap(e => e.link),
135139
})
136140
}
137141

@@ -174,7 +178,7 @@ export default function Menu() {
174178
const renderTeamMenu = () => {
175179
return (
176180
<div className="flex p-1 pl-3 ">
177-
{ projectName && <div className="flex h-full rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1">
181+
{ projectSlug && <div className="flex h-full rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1">
178182
<Link to={team ? `/t/${team.slug}/projects` : `/projects`}>
179183
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{team?.name || userFullName}</span>
180184
</Link>
@@ -214,15 +218,15 @@ export default function Menu() {
214218
}
215219
]}>
216220
<div className="flex h-full px-2 py-1 space-x-3.5">
217-
{ !projectName && <span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{team?.name || userFullName}</span>}
221+
{ !projectSlug && <span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{team?.name || userFullName}</span>}
218222
<img className="filter-grayscale" style={{marginTop: 5, marginBottom: 5}} src={CaretUpDown} />
219223
</div>
220224
</ContextMenu>
221225
</div>
222-
{ projectName && (
226+
{ projectSlug && (
223227
<div className="flex h-full rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1">
224-
<Link to={`${teamOrUserSlug}/${projectName}${prebuildId ? "/prebuilds" : ""}`}>
225-
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{projectName}</span>
228+
<Link to={`${teamOrUserSlug}/${projectSlug}${prebuildId ? "/prebuilds" : ""}`}>
229+
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{projectSlug}</span>
226230
</Link>
227231
</div>
228232
)}

components/dashboard/src/components/CheckBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
function CheckBox(props: {
99
name?: string,
1010
title: string | React.ReactNode,
11-
desc: string,
11+
desc: string | React.ReactNode,
1212
checked: boolean,
1313
disabled?: boolean,
1414
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void

components/dashboard/src/components/PrebuildLogs.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
2424

2525
useEffect(() => {
2626
const disposables = new DisposableCollection();
27+
setWorkspaceInstance(undefined);
2728
(async () => {
2829
if (!props.workspaceId) {
2930
return;
@@ -120,7 +121,7 @@ export default function PrebuildLogs(props: PrebuildLogsProps) {
120121
if (workspaceInstance?.status.conditions.failed) {
121122
setError(new Error(workspaceInstance.status.conditions.failed));
122123
}
123-
}, [ workspaceInstance?.status.phase ]);
124+
}, [ props.workspaceId, workspaceInstance?.status.phase ]);
124125

125126
return <Suspense fallback={<div />}>
126127
<WorkspaceLogs classes="h-full w-full" logsEmitter={logsEmitter} errorMessage={error?.message} />

components/dashboard/src/projects/ConfigureProject.tsx

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import PrebuildLogs from "../components/PrebuildLogs";
1111
import TabMenuItem from "../components/TabMenuItem";
1212
import { getGitpodService } from "../service/service";
1313
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
14-
import Header from "../components/Header";
1514
import Spinner from "../icons/Spinner.svg";
1615
import NoAccess from "../icons/NoAccess.svg";
1716
import PrebuildLogsEmpty from "../images/prebuild-logs-empty.svg";
@@ -20,28 +19,12 @@ import { ThemeContext } from "../theme-context";
2019
import { PrebuildInstanceStatus } from "./Prebuilds";
2120
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
2221
import { openAuthorizeWindow } from "../provider-utils";
22+
import { PageWithSubMenu } from "../components/PageWithSubMenu";
23+
import { getProjectSettingsMenu } from "./ProjectSettings";
2324

2425
const MonacoEditor = React.lazy(() => import('../components/MonacoEditor'));
2526

2627
const TASKS = {
27-
NPM: `tasks:
28-
- init: npm install
29-
command: npm run start`,
30-
Yarn: `tasks:
31-
- init: yarn install
32-
command: yarn run start`,
33-
Go: `tasks:
34-
- init: go get && go build ./... && go test ./...
35-
command: go run`,
36-
Rails: `tasks:
37-
- init: bin/setup
38-
command: bin/rails server`,
39-
Rust: `tasks:
40-
- init: cargo build
41-
command: cargo watch -x run`,
42-
Python: `tasks:
43-
- init: pip install -r requirements.txt
44-
command: python main.py`,
4528
Other: `tasks:
4629
- init: |
4730
echo 'TODO: build project'
@@ -244,9 +227,8 @@ export default function () {
244227
redirectToNewWorkspace();
245228
}
246229

247-
return <>
248-
<Header title="Configuration" subtitle="View and edit project configuration." />
249-
<div className="app-container mt-8 flex space-x-4">
230+
return <PageWithSubMenu subMenu={getProjectSettingsMenu(project, team)} title="Configuration" subtitle="View and edit project configuration.">
231+
<div className="flex space-x-4">
250232
<div className="flex-1 h-96 rounded-xl overflow-hidden relative flex flex-col">
251233
<div className="flex bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-600 px-6 pt-3">
252234
<TabMenuItem name=".gitpod.yml" selected={selectedEditor === '.gitpod.yml'} onClick={() => setSelectedEditor('.gitpod.yml')} />
@@ -301,7 +283,7 @@ export default function () {
301283
</div>
302284
</div>
303285
</div>
304-
</>;
286+
</PageWithSubMenu>;
305287
}
306288

307289
function EditorMessage(props: { heading: string, message: string, type: 'success' | 'warning' }) {

components/dashboard/src/projects/Prebuild.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
1515
import { TeamsContext, getCurrentTeam } from "../teams/teams-context";
1616
import { PrebuildInstanceStatus } from "./Prebuilds";
1717
import { shortCommitMessage } from "./render-utils";
18+
import { Link } from "react-router-dom";
1819

1920
export default function () {
2021
const history = useHistory();
@@ -41,21 +42,21 @@ export default function () {
4142
? await getGitpodService().server.getTeamProjects(team.id)
4243
: await getGitpodService().server.getUserProjects());
4344

44-
const project = projectSlug && projects.find(
45-
p => p.slug ? p.slug === projectSlug :
46-
p.name === projectSlug);
47-
45+
const project = projectSlug && projects.find(p => !!p.slug
46+
? p.slug === projectSlug
47+
: p.name === projectSlug);
4848
if (!project) {
4949
console.error(new Error(`Project not found! (teamId: ${team?.id}, projectName: ${projectSlug})`));
5050
return;
5151
}
52+
5253
const prebuilds = await getGitpodService().server.findPrebuilds({
5354
projectId: project.id,
5455
prebuildId
5556
});
5657
setPrebuild(prebuilds[0]);
5758
})();
58-
}, [ teams ]);
59+
}, [prebuildId, projectSlug, team, teams]);
5960

6061
const renderTitle = () => {
6162
if (!prebuild) {
@@ -77,6 +78,12 @@ export default function () {
7778
<div className="my-auto">
7879
<p className="text-gray-500 dark:text-gray-50">{shortCommitMessage(prebuild.info.changeTitle)}</p>
7980
</div>
81+
{!!prebuild.info.basedOnPrebuildId && <>
82+
<p className="mx-2 my-auto">·</p>
83+
<div className="my-auto">
84+
<p className="text-gray-500 dark:text-gray-50">Incremental Prebuild (<Link className="gp-link" title={prebuild.info.basedOnPrebuildId} to={`./${prebuild.info.basedOnPrebuildId}`}>base</Link>)</p>
85+
</div>
86+
</>}
8087
</div>)
8188
};
8289

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { useContext, useEffect, useState } from "react";
8+
import { useLocation, useRouteMatch } from "react-router";
9+
import { Project, Team } from "@gitpod/gitpod-protocol";
10+
import CheckBox from "../components/CheckBox";
11+
import { getGitpodService } from "../service/service";
12+
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
13+
import { PageWithSubMenu } from "../components/PageWithSubMenu";
14+
import PillLabel from "../components/PillLabel";
15+
16+
export function getProjectSettingsMenu(project?: Project, team?: Team) {
17+
const teamOrUserSlug = !!team ? 't/' + team.slug : 'projects';
18+
return [
19+
{
20+
title: 'Configuration',
21+
link: [`/${teamOrUserSlug}/${project?.slug || project?.name}/configure`],
22+
},
23+
{
24+
title: 'Project',
25+
link: [`/${teamOrUserSlug}/${project?.slug || project?.name}/settings`],
26+
},
27+
];
28+
}
29+
30+
export default function () {
31+
const location = useLocation();
32+
const { teams } = useContext(TeamsContext);
33+
const team = getCurrentTeam(location, teams);
34+
const match = useRouteMatch<{ team: string, resource: string }>("/(t/)?:team/:resource");
35+
const projectSlug = match?.params?.resource;
36+
const [ project, setProject ] = useState<Project | undefined>();
37+
38+
const [ isLoading, setIsLoading ] = useState<boolean>(true);
39+
const [ isIncrementalPrebuildsEnabled, setIsIncrementalPrebuildsEnabled ] = useState<boolean>(false);
40+
41+
useEffect(() => {
42+
if (!teams || !projectSlug) {
43+
return;
44+
}
45+
(async () => {
46+
const projects = (!!team
47+
? await getGitpodService().server.getTeamProjects(team.id)
48+
: await getGitpodService().server.getUserProjects());
49+
50+
// Find project matching with slug, otherwise with name
51+
const project = projectSlug && projects.find(p => p.slug ? p.slug === projectSlug : p.name === projectSlug);
52+
if (!project) {
53+
return;
54+
}
55+
setProject(project);
56+
})();
57+
}, [ projectSlug, team, teams ]);
58+
59+
useEffect(() => {
60+
if (!project) {
61+
return;
62+
}
63+
setIsLoading(false);
64+
setIsIncrementalPrebuildsEnabled(!!project.settings?.useIncrementalPrebuilds);
65+
}, [ project ]);
66+
67+
const toggleIncrementalPrebuilds = async () => {
68+
if (!project) {
69+
return;
70+
}
71+
setIsLoading(true);
72+
try {
73+
await getGitpodService().server.updateProjectSettings(project.id, { useIncrementalPrebuilds: !isIncrementalPrebuildsEnabled });
74+
setIsIncrementalPrebuildsEnabled(!isIncrementalPrebuildsEnabled);
75+
} finally {
76+
setIsLoading(false);
77+
}
78+
}
79+
80+
return <PageWithSubMenu subMenu={getProjectSettingsMenu(project, team)} title="Settings" subtitle={`Manage settings for your project ${project?.name}`}>
81+
<h3>Incremental Prebuilds</h3>
82+
<CheckBox
83+
title={<span>Enable Incremental Prebuilds <PillLabel type="warn" className="font-semibold mt-2 py-0.5 px-2 self-center">Beta</PillLabel></span>}
84+
desc={<span>When possible, use an earlier successful prebuild as a base to create new prebuilds. This can make your prebuilds significantly faster, especially if they normally take longer than 10 minutes. <a className="gp-link" href="https://www.gitpod.io/changelog/faster-incremental-prebuilds">Learn more</a></span>}
85+
checked={isIncrementalPrebuildsEnabled}
86+
disabled={isLoading}
87+
onChange={toggleIncrementalPrebuilds} />
88+
</PageWithSubMenu>;
89+
}

components/dashboard/src/teams/TeamSettings.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* See License-AGPL.txt in the project root for license information.
55
*/
66

7+
import { Team } from "@gitpod/gitpod-protocol";
78
import { useContext, useEffect, useState } from "react";
89
import { Redirect, useLocation } from "react-router";
910
import CodeText from "../components/CodeText";
@@ -13,6 +14,15 @@ import { getGitpodService, gitpodHostUrl } from "../service/service";
1314
import { UserContext } from "../user-context";
1415
import { getCurrentTeam, TeamsContext } from "./teams-context";
1516

17+
export function getTeamSettingsMenu(team?: Team) {
18+
return [
19+
{
20+
title: 'Team',
21+
link: [`/t/${team?.slug}/settings`],
22+
},
23+
];
24+
}
25+
1626
export default function TeamSettings() {
1727
const [modal, setModal] = useState(false);
1828
const [teamSlug, setTeamSlug] = useState('');
@@ -44,15 +54,8 @@ export default function TeamSettings() {
4454
document.location.href = gitpodHostUrl.asDashboard().toString();
4555
};
4656

47-
const settingsMenu = [
48-
{
49-
title: 'General',
50-
link: [`/t/${team?.slug}/settings`]
51-
}
52-
]
53-
5457
return <>
55-
<PageWithSubMenu subMenu={settingsMenu} title='General' subtitle='Manage general team settings.'>
58+
<PageWithSubMenu subMenu={getTeamSettingsMenu(team)} title="Settings" subtitle="Manage general team settings.">
5659
<h3>Delete Team</h3>
5760
<p className="text-base text-gray-500 pb-4">Deleting this team will also remove all associated data with this team, including projects and workspaces. Deleted teams cannot be restored!</p>
5861
<button className="danger secondary" onClick={() => setModal(true)}>Delete Team</button>

0 commit comments

Comments
 (0)