Skip to content

[dashboard] Update Project Configurator #5236

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 3 commits into from
Aug 25, 2021
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
61 changes: 55 additions & 6 deletions components/dashboard/src/components/MonacoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,46 @@ import { useContext, useEffect, useRef } from "react";
import * as monaco from "monaco-editor";
import { ThemeContext } from "../theme-context";

export default function MonacoEditor(props: { classes: string, disabled?: boolean, language: string, value: string, onChange: (value: string) => void }) {
monaco.editor.defineTheme('gitpod', {
base: 'vs',
inherit: true,
rules: [],
colors: {},
});
monaco.editor.defineTheme('gitpod-disabled', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': '#F5F5F4', // Tailwind's warmGray 100 https://tailwindcss.com/docs/customizing-colors
},
});
monaco.editor.defineTheme('gitpod-dark', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#292524', // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors
},
});
monaco.editor.defineTheme('gitpod-dark-disabled', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#44403C', // Tailwind's warmGray 700 https://tailwindcss.com/docs/customizing-colors
},
});

export interface MonacoEditorProps {
classes: string;
disabled?: boolean;
language: string;
value: string;
onChange: (value: string) => void;
}

export default function MonacoEditor(props: MonacoEditorProps) {
const containerRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const { isDark } = useContext(ThemeContext);
Expand All @@ -22,10 +61,21 @@ export default function MonacoEditor(props: { classes: string, disabled?: boolea
enabled: false,
},
renderLineHighlight: 'none',
lineNumbers: 'off',
glyphMargin: false,
folding: false,
});
editorRef.current.onDidChangeModelContent(() => {
props.onChange(editorRef.current!.getValue());
});
// 8px top margin: https://github.com/Microsoft/monaco-editor/issues/1333
editorRef.current.changeViewZones(accessor => {
accessor.addZone({
afterLineNumber: 0,
heightInPx: 8,
domNode: document.createElement('div'),
});
});
}
return () => editorRef.current?.dispose();
}, []);
Expand All @@ -37,14 +87,13 @@ export default function MonacoEditor(props: { classes: string, disabled?: boolea
}, [ props.value ]);

useEffect(() => {
monaco.editor.setTheme(isDark ? 'vs-dark' : 'vs');
}, [ isDark ]);

useEffect(() => {
monaco.editor.setTheme(props.disabled
? (isDark ? 'gitpod-dark-disabled' : 'gitpod-disabled')
: (isDark ? 'gitpod-dark' : 'gitpod'));
if (editorRef.current) {
editorRef.current.updateOptions({ readOnly: props.disabled });
}
}, [ props.disabled ]);
}, [ props.disabled, isDark ]);

return <div className={props.classes} ref={containerRef} />;
}
23 changes: 12 additions & 11 deletions components/dashboard/src/components/PrebuildLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { getGitpodService } from "../service/service";

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

export default function PrebuildLogs(props: { workspaceId?: string }) {
export interface PrebuildLogsProps {
workspaceId?: string;
onInstanceUpdate?: (instance: WorkspaceInstance) => void;
}

export default function PrebuildLogs(props: PrebuildLogsProps) {
const [ workspace, setWorkspace ] = useState<Workspace | undefined>();
const [ workspaceInstance, setWorkspaceInstance ] = useState<WorkspaceInstance | undefined>();
const [ error, setError ] = useState<Error | undefined>();
Expand Down Expand Up @@ -54,6 +59,9 @@ export default function PrebuildLogs(props: { workspaceId?: string }) {
}, [ props.workspaceId ]);

useEffect(() => {
if (props.onInstanceUpdate && workspaceInstance) {
props.onInstanceUpdate(workspaceInstance);
}
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.
Expand Down Expand Up @@ -107,16 +115,9 @@ export default function PrebuildLogs(props: { workspaceId?: string }) {
}
}, [ workspaceInstance?.status.phase ]);

return <>
<Suspense fallback={<div />}>
<WorkspaceLogs classes="h-64 w-full" logsEmitter={logsEmitter} errorMessage={error?.message} />
</Suspense>
<div className="mt-2 flex justify-center space-x-2">
{workspaceInstance?.status.phase === 'stopped'
? <a href={workspace?.contextURL ? '/#' + workspace.contextURL.replace(/^prebuild/, '') : undefined}><button>Open Workspace</button></a>
: <button className="secondary disabled" disabled={true}>Open Workspace</button> }
</div>
</>;
return <Suspense fallback={<div />}>
<WorkspaceLogs classes="h-full w-full" logsEmitter={logsEmitter} errorMessage={error?.message} />
</Suspense>;
}

export function watchHeadlessLogs(instanceId: string, onLog: (chunk: string) => void, checkIsDone: () => Promise<boolean>): DisposableCollection {
Expand Down
100 changes: 51 additions & 49 deletions components/dashboard/src/components/WorkspaceLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,87 @@
*/

import EventEmitter from 'events';
import React from 'react';
import { useContext, useEffect, useRef } from 'react';
import { Terminal, ITerminalOptions, ITheme } from 'xterm';
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css';
import { DisposableCollection } from '@gitpod/gitpod-protocol';
import { ThemeContext } from '../theme-context';

const darkTheme: ITheme = {
background: '#292524', // Tailwind's warmGray 800 https://tailwindcss.com/docs/customizing-colors
};
const lightTheme: ITheme = {
background: '#F5F5F4', // Tailwind's warmGray 100 https://tailwindcss.com/docs/customizing-colors
foreground: '#78716C', // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors
cursor: '#78716C', // Tailwind's warmGray 500 https://tailwindcss.com/docs/customizing-colors
}

export interface WorkspaceLogsProps {
logsEmitter: EventEmitter;
errorMessage?: string;
classes?: string;
}

export interface WorkspaceLogsState {
}

export default class WorkspaceLogs extends React.Component<WorkspaceLogsProps, WorkspaceLogsState> {
protected xTermParentRef: React.RefObject<HTMLDivElement>;
protected terminal: Terminal | undefined;
protected fitAddon: FitAddon | undefined;

constructor(props: WorkspaceLogsProps) {
super(props);
this.xTermParentRef = React.createRef();
}
export default function WorkspaceLogs(props: WorkspaceLogsProps) {
const xTermParentRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal>();
const fitAddon = new FitAddon();
const { isDark } = useContext(ThemeContext);

private readonly toDispose = new DisposableCollection();
componentDidMount() {
const element = this.xTermParentRef.current;
if (element === null) {
useEffect(() => {
if (!xTermParentRef.current) {
return;
}
const theme: ITheme = {};
const options: ITerminalOptions = {
cursorBlink: false,
disableStdin: true,
fontSize: 14,
theme,
theme: darkTheme,
scrollback: 9999999,
};
this.terminal = new Terminal(options);
this.fitAddon = new FitAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.open(element);
this.props.logsEmitter.on('logs', logs => {
if (this.fitAddon && this.terminal && logs) {
this.terminal.write(logs);
const terminal = new Terminal(options);
terminalRef.current = terminal;
terminal.loadAddon(fitAddon);
terminal.open(xTermParentRef.current);
props.logsEmitter.on('logs', logs => {
if (terminal && logs) {
terminal.write(logs);
}
});
this.toDispose.push(this.terminal);
this.fitAddon.fit();
fitAddon.fit();
return function cleanUp() {
terminal.dispose();
}
});

useEffect(() => {
// Fit terminal on window resize (debounced)
let timeout: NodeJS.Timeout | undefined;
const onWindowResize = () => {
clearTimeout(timeout!);
timeout = setTimeout(() => this.fitAddon!.fit(), 20);
timeout = setTimeout(() => fitAddon.fit(), 20);
};
window.addEventListener('resize', onWindowResize);
this.toDispose.push({
dispose: () => {
clearTimeout(timeout!);
window.removeEventListener('resize', onWindowResize);
}
});
}
return function cleanUp() {
clearTimeout(timeout!);
window.removeEventListener('resize', onWindowResize);
}
});

componentDidUpdate() {
if (this.terminal && this.props.errorMessage) {
this.terminal.write(`\n\u001b[38;5;196m${this.props.errorMessage}\u001b[0m`);
useEffect(() => {
if (terminalRef.current && props.errorMessage) {
terminalRef.current.write(`\n\u001b[38;5;196m${props.errorMessage}\u001b[0m`);
}
}
}, [ terminalRef.current, props.errorMessage ]);

componentWillUnmount() {
this.toDispose.dispose();
}
useEffect(() => {
if (!terminalRef.current) {
return;
}
terminalRef.current.setOption('theme', isDark ? darkTheme : lightTheme);
}, [ terminalRef.current, isDark ]);

render() {
return <div className={`mt-6 ${this.props.classes || 'h-72 w-11/12 lg:w-3/5'} rounded-xl bg-black relative`}>
<div className="absolute top-0 left-0 bottom-0 right-0 m-6" ref={this.xTermParentRef}></div>
</div>;
}
return <div className={`${props.classes || 'mt-6 h-72 w-11/12 lg:w-3/5 rounded-xl overflow-hidden'} bg-gray-100 dark:bg-gray-800 relative`}>
<div className="absolute top-0 left-0 bottom-0 right-0 m-6" ref={xTermParentRef}></div>
</div>;
}
1 change: 1 addition & 0 deletions components/dashboard/src/icons/Spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/SpinnerDark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/StatusDone.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/StatusFailed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/StatusPaused.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/icons/StatusRunning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions components/dashboard/src/images/prebuild-logs-empty.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading