Skip to content

Implement Project-level environment variables #7295

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

Merged
merged 5 commits into from
Jan 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions components/dashboard/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { trackButtonOrAnchor, trackPathChange, trackLocation } from './Analytics
import { User } from '@gitpod/gitpod-protocol';
import * as GitpodCookie from '@gitpod/gitpod-protocol/lib/util/gitpod-cookie';
import { Experiment } from './experiments';
import ProjectSettings from './projects/ProjectSettings';

const Setup = React.lazy(() => import(/* webpackPrefetch: true */ './Setup'));
const Workspaces = React.lazy(() => import(/* webpackPrefetch: true */ './workspaces/Workspaces'));
Expand All @@ -42,6 +41,8 @@ const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ './projec
const ConfigureProject = React.lazy(() => import(/* webpackPrefetch: true */ './projects/ConfigureProject'));
const Projects = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Projects'));
const Project = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Project'));
const ProjectSettings = React.lazy(() => import(/* webpackPrefetch: true */ './projects/ProjectSettings'));
const ProjectVariables = React.lazy(() => import(/* webpackPrefetch: true */ './projects/ProjectVariables'));
const Prebuilds = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Prebuilds'));
const Prebuild = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Prebuild'));
const InstallGitHubApp = React.lazy(() => import(/* webpackPrefetch: true */ './projects/InstallGitHubApp'));
Expand Down Expand Up @@ -307,6 +308,9 @@ function App() {
if (resourceOrPrebuild === "configure") {
return <ConfigureProject />;
}
if (resourceOrPrebuild === "variables") {
return <ProjectVariables />;
}
if (resourceOrPrebuild === "prebuilds") {
return <Prebuilds />;
}
Expand Down Expand Up @@ -337,11 +341,14 @@ function App() {
if (maybeProject === "settings") {
return <TeamSettings />;
}
if (resourceOrPrebuild === "settings") {
return <ProjectSettings />;
}
if (resourceOrPrebuild === "configure") {
return <ConfigureProject />;
}
if (resourceOrPrebuild === "settings") {
return <ProjectSettings />;
if (resourceOrPrebuild === "variables") {
return <ProjectVariables />;
}
if (resourceOrPrebuild === "prebuilds") {
return <Prebuilds />;
Expand Down
27 changes: 23 additions & 4 deletions components/dashboard/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ import PillMenuItem from "./components/PillMenuItem";
import TabMenuItem from "./components/TabMenuItem";
import { getTeamSettingsMenu } from "./teams/TeamSettings";
import { getProjectSettingsMenu } from "./projects/ProjectSettings";
import { ProjectContext } from "./projects/project-context";

interface Entry {
title: string,
link: string,
alternatives?: string[]
}


export default function Menu() {
const { user } = useContext(UserContext);
const { teams } = useContext(TeamsContext);
const location = useLocation();
const team = getCurrentTeam(location, teams);
const { project, setProject } = useContext(ProjectContext);

const match = useRouteMatch<{ segment1?: string, segment2?: string, segment3?: string }>("/(t/)?:segment1/:segment2?/:segment3?");
const projectSlug = (() => {
Expand All @@ -45,7 +47,7 @@ export default function Menu() {
})();
const prebuildId = (() => {
const resource = projectSlug && match?.params?.segment3;
if (resource !== "workspaces" && resource !== "prebuilds" && resource !== "settings" && resource !== "configure") {
if (resource !== "workspaces" && resource !== "prebuilds" && resource !== "settings" && resource !== "configure" && resource !== "variables") {
return resource;
}
})();
Expand All @@ -57,7 +59,6 @@ export default function Menu() {
}

const userFullName = user?.fullName || user?.name || '...';
const team = getCurrentTeam(location, teams);

{
// updating last team selection
Expand Down Expand Up @@ -88,6 +89,24 @@ export default function Menu() {
})();
}, [ teams ]);

useEffect(() => {
if (!teams || !projectSlug) {
return;
}
(async () => {
const projects = (!!team
? await getGitpodService().server.getTeamProjects(team.id)
: await getGitpodService().server.getUserProjects());

// Find project matching with slug, otherwise with name
const project = projectSlug && projects.find(p => p.slug ? p.slug === projectSlug : p.name === projectSlug);
if (!project) {
return;
}
setProject(project);
})();
}, [projectSlug, setProject, team, teams]);

const teamOrUserSlug = !!team ? '/t/' + team.slug : '/projects';
const leftMenu: Entry[] = (() => {
// Project menu
Expand Down Expand Up @@ -218,7 +237,7 @@ export default function Menu() {
{ projectSlug && (
<div className="flex h-full rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 px-2 py-1">
<Link to={`${teamOrUserSlug}/${projectSlug}${prebuildId ? "/prebuilds" : ""}`}>
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{projectSlug}</span>
<span className="text-base text-gray-600 dark:text-gray-400 font-semibold">{project?.name}</span>
</Link>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion components/dashboard/src/components/ItemsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function Item(props: {
}) {
const headerClassName = "text-sm text-gray-400 border-t border-b border-gray-200 dark:border-gray-800";
const notHeaderClassName = "rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gitpod-kumquat-light";
return <div className={`flex flex-grow flex-row w-full px-3 py-3 justify-between transition ease-in-out ${props.header ? headerClassName : notHeaderClassName} ${props.className || ""}`}>
return <div className={`flex flex-grow flex-row w-full p-3 justify-between transition ease-in-out ${props.header ? headerClassName : notHeaderClassName} ${props.className || ""}`}>
{props.children}
</div>;
}
Expand Down
3 changes: 2 additions & 1 deletion components/dashboard/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default function Modal(props: {
return () => {
window.removeEventListener('keydown', handler);
};
}, []); // Empty array ensures that effect is only run on mount and unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.onClose, props.onEnter]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This was required because apparently state variables (like name: string and value: string in AddVariableModal) get "captured" in the onClose / onEnter lambdas, thus causing these functions to always use initial state values (i.e. empty string) instead of updated state values (i.e. whatever name/value a user typed).

The solution was to make this effect depend on onClose and onEnter, such that, if these functions change (they do change every time a captured state variable changes), the handler gets re-registered properly.


if (!props.visible) {
return null;
Expand Down
13 changes: 8 additions & 5 deletions components/dashboard/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ReactDOM from 'react-dom';
import App from './App';
import { UserContextProvider } from './user-context';
import { TeamsContextProvider } from './teams/teams-context';
import { ProjectContextProvider } from './projects/project-context';
import { ThemeContextProvider } from './theme-context';
import { BrowserRouter } from 'react-router-dom';

Expand All @@ -18,11 +19,13 @@ ReactDOM.render(
<React.StrictMode>
<UserContextProvider>
<TeamsContextProvider>
<ThemeContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeContextProvider>
<ProjectContextProvider>
<ThemeContextProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeContextProvider>
</ProjectContextProvider>
</TeamsContextProvider>
</UserContextProvider>
</React.StrictMode>,
Expand Down
39 changes: 9 additions & 30 deletions components/dashboard/src/projects/ConfigureProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@
*/

import React, { Suspense, useContext, useEffect, useState } from "react";
import { useLocation, useRouteMatch } from "react-router";
import { Project, StartPrebuildResult, WorkspaceInstance } from "@gitpod/gitpod-protocol";
import PrebuildLogs from "../components/PrebuildLogs";
import TabMenuItem from "../components/TabMenuItem";
import { getGitpodService } from "../service/service";
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
import Spinner from "../icons/Spinner.svg";
import NoAccess from "../icons/NoAccess.svg";
import PrebuildLogsEmpty from "../images/prebuild-logs-empty.svg";
Expand All @@ -19,8 +17,8 @@ import { ThemeContext } from "../theme-context";
import { PrebuildInstanceStatus } from "./Prebuilds";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { openAuthorizeWindow } from "../provider-utils";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import { getProjectSettingsMenu } from "./ProjectSettings";
import { ProjectSettingsPage } from "./ProjectSettings";
import { ProjectContext } from "./project-context";

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

Expand All @@ -42,12 +40,7 @@ const TASKS = {
// }

export default function () {
const { teams } = useContext(TeamsContext);
const location = useLocation();
const team = getCurrentTeam(location, teams);
const routeMatch = useRouteMatch<{ teamSlug: string, projectSlug: string }>("/(t/)?:teamSlug/:projectSlug/configure");
const projectSlug = routeMatch?.params.projectSlug;
const [project, setProject] = useState<Project | undefined>();
const { project } = useContext(ProjectContext);
const [gitpodYml, setGitpodYml] = useState<string>('');
const [dockerfile, setDockerfile] = useState<string>('');
const [editorMessage, setEditorMessage] = useState<React.ReactNode | null>(null);
Expand All @@ -68,26 +61,12 @@ export default function () {
setIsDetecting(true);
setIsEditorDisabled(true);
setEditorMessage(null);
if (!teams) {
setIsDetecting(false);
setEditorMessage(<EditorMessage type="warning" heading="Couldn't load teams information." message="Please try to reload this page." />);
return;
}
(async () => {
const projects = (!!team
? await getGitpodService().server.getTeamProjects(team.id)
: await getGitpodService().server.getUserProjects());

const project = projectSlug && projects.find(
p => p.slug ? p.slug === projectSlug :
p.name === projectSlug);

if (!project) {
setIsDetecting(false);
setEditorMessage(<EditorMessage type="warning" heading="Couldn't load project information." message="Please try to reload this page." />);
return;
}
setProject(project);
try {
await detectProjectConfiguration(project);
} catch (error) {
Expand All @@ -106,7 +85,7 @@ export default function () {
}
}
})();
}, [teams, team]);
}, [project]);

const detectProjectConfiguration = async (project: Project) => {
const guessedConfigStringPromise = getGitpodService().server.guessProjectConfiguration(project.id);
Expand Down Expand Up @@ -227,7 +206,7 @@ export default function () {
redirectToNewWorkspace();
}

return <PageWithSubMenu subMenu={getProjectSettingsMenu(project, team)} title="Configuration" subtitle="View and edit project configuration.">
return <ProjectSettingsPage project={project}>
<div className="flex space-x-4">
<div className="flex-1 h-96 rounded-xl overflow-hidden relative flex flex-col">
<div className="flex bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-600 px-6 pt-3">
Expand All @@ -245,7 +224,7 @@ export default function () {
{showAuthBanner ? (
<div className="mt-8 text-gray-500 flex-col">
<div className="p-16 text-center">
<img src={NoAccess} title="No Access" className="m-auto mb-4" />
<img alt="" src={NoAccess} title="No Access" className="m-auto mb-4" />
<div className="text-center text-gray-600 dark:text-gray-50 pb-3 font-bold">
No Access
</div>
Expand All @@ -256,7 +235,7 @@ export default function () {
</div>
</div>
) : (<>
<img className="h-5 w-5 animate-spin" src={Spinner} />
<img alt="" className="h-5 w-5 animate-spin" src={Spinner} />
<span className="font-semibold text-gray-400">Detecting project configuration ...</span>
</>
)}
Expand All @@ -266,7 +245,7 @@ export default function () {
<div className="flex-grow flex">{startPrebuildResult
? <PrebuildLogs workspaceId={startPrebuildResult.wsid} onInstanceUpdate={onInstanceUpdate} />
: (!prebuildWasTriggered && <div className="flex-grow flex flex-col items-center justify-center">
<img className="w-14" role="presentation" src={isDark ? PrebuildLogsEmptyDark : PrebuildLogsEmpty} />
<img alt="" className="w-14" role="presentation" src={isDark ? PrebuildLogsEmptyDark : PrebuildLogsEmpty} />
<h3 className="text-center text-lg text-gray-500 dark:text-gray-50 mt-4">No Recent Prebuild</h3>
<p className="text-center text-base text-gray-500 dark:text-gray-400 mt-2 w-64">Edit the project configuration on the left to get started. <a className="gp-link" href="https://www.gitpod.io/docs/config-gitpod-file/">Learn more</a></p>
</div>)
Expand All @@ -283,7 +262,7 @@ export default function () {
</div>
</div>
</div>
</PageWithSubMenu>;
</ProjectSettingsPage>;
}

function EditorMessage(props: { heading: string, message: string, type: 'success' | 'warning' }) {
Expand Down
42 changes: 17 additions & 25 deletions components/dashboard/src/projects/ProjectSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*/

import { useContext, useEffect, useState } from "react";
import { useLocation, useRouteMatch } from "react-router";
import { useLocation } from "react-router";
import { Project, Team } from "@gitpod/gitpod-protocol";
import CheckBox from "../components/CheckBox";
import { getGitpodService } from "../service/service";
import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
import { PageWithSubMenu } from "../components/PageWithSubMenu";
import PillLabel from "../components/PillLabel";
import { ProjectContext } from "./project-context";

export function getProjectSettingsMenu(project?: Project, team?: Team) {
const teamOrUserSlug = !!team ? 't/' + team.slug : 'projects';
Expand All @@ -24,37 +25,28 @@ export function getProjectSettingsMenu(project?: Project, team?: Team) {
title: 'Configuration',
link: [`/${teamOrUserSlug}/${project?.slug || project?.name}/configure`],
},
{
title: 'Variables',
link: [`/${teamOrUserSlug}/${project?.slug || project?.name}/variables`],
},
];
}

export default function () {
export function ProjectSettingsPage(props: { project?: Project, children?: React.ReactNode }) {
const location = useLocation();
const { teams } = useContext(TeamsContext);
const team = getCurrentTeam(location, teams);
const match = useRouteMatch<{ team: string, resource: string }>("/(t/)?:team/:resource");
const projectSlug = match?.params?.resource;
const [ project, setProject ] = useState<Project | undefined>();

const [ isLoading, setIsLoading ] = useState<boolean>(true);
const [ isIncrementalPrebuildsEnabled, setIsIncrementalPrebuildsEnabled ] = useState<boolean>(false);
return <PageWithSubMenu subMenu={getProjectSettingsMenu(props.project, team)} title="Settings" subtitle="Manage project settings and configuration">
{props.children}
</PageWithSubMenu>
}

useEffect(() => {
if (!teams || !projectSlug) {
return;
}
(async () => {
const projects = (!!team
? await getGitpodService().server.getTeamProjects(team.id)
: await getGitpodService().server.getUserProjects());
export default function () {
const { project } = useContext(ProjectContext);

// Find project matching with slug, otherwise with name
const project = projectSlug && projects.find(p => p.slug ? p.slug === projectSlug : p.name === projectSlug);
if (!project) {
return;
}
setProject(project);
})();
}, [ projectSlug, team, teams ]);
const [ isLoading, setIsLoading ] = useState<boolean>(true);
const [ isIncrementalPrebuildsEnabled, setIsIncrementalPrebuildsEnabled ] = useState<boolean>(false);

useEffect(() => {
if (!project) {
Expand Down Expand Up @@ -82,13 +74,13 @@ export default function () {
}
}

return <PageWithSubMenu subMenu={getProjectSettingsMenu(project, team)} title="Settings" subtitle="Manage project settings and configuration">
return <ProjectSettingsPage project={project}>
<h3>Incremental Prebuilds</h3>
<CheckBox
title={<span>Enable Incremental Prebuilds <PillLabel type="warn" className="font-semibold mt-2 py-0.5 px-2 self-center">Beta</PillLabel></span>}
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>}
checked={isIncrementalPrebuildsEnabled}
disabled={isLoading}
onChange={toggleIncrementalPrebuilds} />
</PageWithSubMenu>;
</ProjectSettingsPage>;
}
Loading