diff --git a/components/dashboard/package.json b/components/dashboard/package.json
index ba5784260e7f2f..672c961f9693f6 100644
--- a/components/dashboard/package.json
+++ b/components/dashboard/package.json
@@ -7,6 +7,7 @@
"@gitpod/gitpod-protocol": "0.1.5",
"countries-list": "^2.6.1",
"moment": "^2.29.1",
+ "monaco-editor": "^0.25.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx
index cfcf1c695662b1..711c87c0f04214 100644
--- a/components/dashboard/src/App.tsx
+++ b/components/dashboard/src/App.tsx
@@ -32,6 +32,7 @@ const NewTeam = React.lazy(() => import(/* webpackPrefetch: true */ './teams/New
const JoinTeam = React.lazy(() => import(/* webpackPrefetch: true */ './teams/JoinTeam'));
const Members = React.lazy(() => import(/* webpackPrefetch: true */ './teams/Members'));
const NewProject = React.lazy(() => import(/* webpackPrefetch: true */ './projects/NewProject'));
+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 Prebuilds = React.lazy(() => import(/* webpackPrefetch: true */ './projects/Prebuilds'));
@@ -206,6 +207,9 @@ function App() {
if (maybeProject === "members") {
return ;
}
+ if (subResource === "configure") {
+ return ;
+ }
if (subResource === "prebuilds") {
return ;
}
diff --git a/components/dashboard/src/Menu.tsx b/components/dashboard/src/Menu.tsx
index d716436731a80b..62f2205e32c551 100644
--- a/components/dashboard/src/Menu.tsx
+++ b/components/dashboard/src/Menu.tsx
@@ -48,7 +48,7 @@ export default function Menu() {
})();
const userFullName = user?.fullName || user?.name || '...';
- const showTeamsUI = user?.rolesOrPermissions?.includes('teams-and-projects') || window.location.hostname.endsWith('gitpod-dev.com') || window.location.hostname.endsWith('gitpod-io-dev.com');
+ const showTeamsUI = user?.rolesOrPermissions?.includes('teams-and-projects');
const team = getCurrentTeam(location, teams);
// Hide most of the top menu when in a full-page form.
@@ -97,6 +97,10 @@ export default function Menu() {
{
title: 'Settings',
link: `/${team.slug}/${projectName}/settings`
+ },
+ {
+ title: 'Configure',
+ link: `/${team.slug}/${projectName}/configure`
}
] : [
{
diff --git a/components/dashboard/src/components/MonacoEditor.tsx b/components/dashboard/src/components/MonacoEditor.tsx
new file mode 100644
index 00000000000000..844ca0274d371d
--- /dev/null
+++ b/components/dashboard/src/components/MonacoEditor.tsx
@@ -0,0 +1,44 @@
+/**
+ * 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 { useEffect, useRef } from "react";
+import * as monaco from "monaco-editor";
+
+export default function MonacoEditor(props: { classes: string, disabled?: boolean, language: string, value: string, onChange: (value: string) => void }) {
+ const containerRef = useRef(null);
+ const editorRef = useRef();
+
+ useEffect(() => {
+ if (containerRef.current) {
+ editorRef.current = monaco.editor.create(containerRef.current, {
+ value: props.value,
+ language: props.language,
+ minimap: {
+ enabled: false,
+ },
+ renderLineHighlight: 'none',
+ });
+ editorRef.current.onDidChangeModelContent(() => {
+ props.onChange(editorRef.current!.getValue());
+ });
+ }
+ return () => editorRef.current?.dispose();
+ }, []);
+
+ useEffect(() => {
+ if (editorRef.current && editorRef.current.getValue() !== props.value) {
+ editorRef.current.setValue(props.value);
+ }
+ }, [ props.value ]);
+
+ useEffect(() => {
+ if (editorRef.current) {
+ editorRef.current.updateOptions({ readOnly: props.disabled });
+ }
+ }, [ props.disabled ]);
+
+ return ;
+}
\ No newline at end of file
diff --git a/components/dashboard/src/components/PrebuildLogs.tsx b/components/dashboard/src/components/PrebuildLogs.tsx
new file mode 100644
index 00000000000000..f57a3306f8407f
--- /dev/null
+++ b/components/dashboard/src/components/PrebuildLogs.tsx
@@ -0,0 +1,205 @@
+/**
+ * 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 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 { getGitpodService } from "../service/service";
+
+const WorkspaceLogs = React.lazy(() => import('./WorkspaceLogs'));
+
+export default function PrebuildLogs(props: { workspaceId?: string }) {
+ const [ workspace, setWorkspace ] = useState();
+ const [ workspaceInstance, setWorkspaceInstance ] = useState();
+ const [ error, setError ] = useState();
+ const logsEmitter = new EventEmitter();
+ const service = getGitpodService();
+
+ useEffect(() => {
+ const disposables = new DisposableCollection();
+ (async () => {
+ if (!props.workspaceId) {
+ return;
+ }
+ try {
+ const info = await service.server.getWorkspace(props.workspaceId);
+ if (info.latestInstance) {
+ setWorkspace(info.workspace);
+ setWorkspaceInstance(info.latestInstance);
+ }
+ disposables.push(service.registerClient({
+ onInstanceUpdate: setWorkspaceInstance,
+ onWorkspaceImageBuildLogs: (info: WorkspaceImageBuild.StateInfo, content?: WorkspaceImageBuild.LogContent) => {
+ if (!content) {
+ return;
+ }
+ logsEmitter.emit('logs', content.text);
+ },
+ }));
+ if (info.latestInstance) {
+ disposables.push(watchHeadlessLogs(service.server, info.latestInstance.id, chunk => {
+ logsEmitter.emit('logs', chunk);
+ }, async () => workspaceInstance?.status.phase === 'stopped'));
+ }
+ } catch (err) {
+ console.error(err);
+ setError(err);
+ }
+ })();
+ return function cleanUp() {
+ disposables.dispose();
+ }
+ }, [ props.workspaceId ]);
+
+ useEffect(() => {
+ switch (workspaceInstance?.status.phase) {
+ // unknown indicates an issue within the system in that it cannot determine the actual phase of
+ // a workspace. This phase is usually accompanied by an error.
+ case "unknown":
+ break;
+
+ // 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);
+ break;
+
+ // Pending means the workspace does not yet consume resources in the cluster, but rather is looking for
+ // some space within the cluster. If for example the cluster needs to scale up to accomodate the
+ // workspace, the workspace will be in Pending state until that happened.
+ case "pending":
+ break;
+
+ // Creating means the workspace is currently being created. That includes downloading the images required
+ // to run the workspace over the network. The time spent in this phase varies widely and depends on the current
+ // network speed, image size and cache states.
+ case "creating":
+ break;
+
+ // Initializing is the phase in which the workspace is executing the appropriate workspace initializer (e.g. Git
+ // clone or backup download). After this phase one can expect the workspace to either be Running or Failed.
+ case "initializing":
+ break;
+
+ // Running means the workspace is able to actively perform work, either by serving a user through Theia,
+ // or as a headless workspace.
+ case "running":
+ break;
+
+ // Interrupted is an exceptional state where the container should be running but is temporarily unavailable.
+ // When in this state, we expect it to become running or stopping anytime soon.
+ case "interrupted":
+ break;
+
+ // Stopping means that the workspace is currently shutting down. It could go to stopped every moment.
+ case "stopping":
+ break;
+
+ // Stopped means the workspace ended regularly because it was shut down.
+ case "stopped":
+ service.server.watchWorkspaceImageBuildLogs(workspace!.id);
+ break;
+ }
+ if (workspaceInstance?.status.conditions.failed) {
+ setError(new Error(workspaceInstance.status.conditions.failed));
+ }
+ }, [ workspaceInstance?.status.phase ]);
+
+ return <>
+