diff --git a/components/dashboard/src/components/ContextMenu.tsx b/components/dashboard/src/components/ContextMenu.tsx index e0eecbed25f2b9..ee30a37ff7396c 100644 --- a/components/dashboard/src/components/ContextMenu.tsx +++ b/components/dashboard/src/components/ContextMenu.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { MouseEvent, useState } from 'react'; export interface ContextMenuProps { children: React.ReactChild[] | React.ReactChild; @@ -14,7 +14,7 @@ export interface ContextMenuEntry { */ separator?: boolean; customFontStyle?: string; - onClick?: ()=>void; + onClick?: (event: MouseEvent)=>void; href?: string; } @@ -34,9 +34,10 @@ function ContextMenu(props: ContextMenuProps) { const enhancedEntries = props.menuEntries.map(e => { return { ... e, - onClick: () => { - e.onClick && e.onClick(); + onClick: (event: MouseEvent) => { + e.onClick && e.onClick(event); toggleExpanded(); + event.preventDefault(); } } }) diff --git a/components/dashboard/src/components/Modal.tsx b/components/dashboard/src/components/Modal.tsx index ef01d24f4bcb85..0023e1bde4423c 100644 --- a/components/dashboard/src/components/Modal.tsx +++ b/components/dashboard/src/components/Modal.tsx @@ -1,27 +1,57 @@ +import { Disposable, DisposableCollection } from "@gitpod/gitpod-protocol"; +import { useEffect } from "react"; + export default function Modal(props: { children: React.ReactChild[] | React.ReactChild, visible: boolean, closeable?: boolean, className?: string, - onClose: () => void + onClose: () => void, + onEnter?: () => boolean }) { + const disposable = new DisposableCollection(); + const close = () => { + disposable.dispose(); + props.onClose(); + } + useEffect(() => { + if (!props.visible) { + return; + } + const keyHandler = (k: globalThis.KeyboardEvent) => { + if (k.eventPhase === 1 /* CAPTURING */) { + if (k.key === 'Escape') { + close(); + } + if (k.key === 'Enter') { + if (props.onEnter) { + if (props.onEnter() === false) { + return; + } + } + close(); + k.stopPropagation(); + } + } + } + window.addEventListener('keydown', keyHandler, { capture: true }); + disposable.push(Disposable.create(()=> window.removeEventListener('keydown', keyHandler))); + }); if (!props.visible) { return null; } - setTimeout(() => window.addEventListener('click', props.onClose, { once: true }), 0); return ( -
+
-
+
e.stopPropagation()}> {props.closeable !== false && ( -
+
- )} {props.children}
diff --git a/components/dashboard/src/workspaces/WorkspaceEntry.tsx b/components/dashboard/src/workspaces/WorkspaceEntry.tsx index 521f6bce3862db..1af14ec22c816c 100644 --- a/components/dashboard/src/workspaces/WorkspaceEntry.tsx +++ b/components/dashboard/src/workspaces/WorkspaceEntry.tsx @@ -9,7 +9,7 @@ import { MouseEvent, useState } from 'react'; import { WorkspaceModel } from './workspace-model'; -export function WorkspaceEntry({desc, model}: {desc: WorkspaceInfo, model: WorkspaceModel}) { +export function WorkspaceEntry({ desc, model }: { desc: WorkspaceInfo, model: WorkspaceModel }) { const [isModalVisible, setModalVisible] = useState(false); const [isChangesModalVisible, setChangesModalVisible] = useState(false); const state: WorkspaceInstancePhase = desc.latestInstance?.status?.phase || 'stopped'; @@ -46,14 +46,21 @@ export function WorkspaceEntry({desc, model}: {desc: WorkspaceInfo, model: Works pathname: '/start/', hash: '#' + ws.id }); - const downloadURL = new GitpodHostUrl(window.location.href).with({ - pathname: `/workspace-download/get/${ws.id}` + const downloadURL = new GitpodHostUrl(window.location.href).with({ + pathname: `/workspace-download/get/${ws.id}` }).toString(); const menuEntries: ContextMenuEntry[] = [ { title: 'Open', href: startUrl.toString() - }, + }]; + if (state === 'running') { + menuEntries.push({ + title: 'Stop', + onClick: () => getGitpodService().server.stopWorkspace(ws.id) + }); + } + menuEntries.push( { title: 'Download', href: downloadURL @@ -80,63 +87,66 @@ export function WorkspaceEntry({desc, model}: {desc: WorkspaceInfo, model: Works setModalVisible(true); } } - ]; + ); const project = getProject(ws); - const startWsOnClick = (event: MouseEvent) => { - window.location.href = startUrl.toString(); - } const showChanges = (event: MouseEvent) => { + event.preventDefault(); setChangesModalVisible(true); } - return
-
-
-   + return -
-
{ws.id}
-
{project || 'Unknown'}
-
-
-
-
{ws.description}
-
{ws.contextURL}
+
+
+
{ws.description}
+
{ws.contextURL}
+
+
+
0 ? showChanges : undefined}> +
+
{currentBranch}
+ { + numberOfChanges > 0 ? +
{changesLabel}
+ : +
No Changes
+ } +
-
-
0 ? showChanges: startWsOnClick}> -
-
{currentBranch}
- { - numberOfChanges > 0 ? -
{changesLabel}
- : -
No Changes
- } - setChangesModalVisible(false)}> - {getChangesPopup(pendingChanges)} - +
+
{moment(WorkspaceInfo.lastActiveISODate(desc)).fromNow()}
-
-
-
{moment(WorkspaceInfo.lastActiveISODate(desc)).fromNow()}
-
-
- - Actions - -
- setModalVisible(false)}> +
+ + Actions + +
+ + setChangesModalVisible(false)}> + {getChangesPopup(pendingChanges)} + + setModalVisible(false)} onEnter={() => {model.deleteWorkspace(ws.id); return true;}}>
-

Delete {ws.id}

-
-

Do you really want to delete this workspace?

+

Delete Workspace

+
+

Are you sure you want to delete this workspace?

+
+

{ws.id}

+

{ws.description}

+
-
-
+
diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index bb77d05efb28b4..fff7ddcf6e98f9 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -89,7 +89,7 @@ export class Workspaces extends React.ComponentName
Context
Pending Changes
-
Last Active
+
Last Start
{ diff --git a/components/dashboard/src/workspaces/workspace-model.ts b/components/dashboard/src/workspaces/workspace-model.ts index fad617b2ce5ec1..aab4627f36d47a 100644 --- a/components/dashboard/src/workspaces/workspace-model.ts +++ b/components/dashboard/src/workspaces/workspace-model.ts @@ -2,7 +2,7 @@ import { Disposable, DisposableCollection, GitpodClient, WorkspaceInfo, Workspac import { getGitpodService } from "../service/service"; export class WorkspaceModel implements Disposable, Partial { - + protected workspaces = new Map(); protected currentlyFetching = new Set(); protected disposables = new DisposableCollection(); @@ -15,7 +15,7 @@ export class WorkspaceModel implements Disposable, Partial { constructor(protected setWorkspaces: (ws: WorkspaceInfo[]) => void) { this.internalRefetch(); } - + protected internalRefetch() { this.disposables.dispose(); this.disposables = new DisposableCollection(); @@ -27,17 +27,17 @@ export class WorkspaceModel implements Disposable, Partial { }); this.disposables.push(getGitpodService().registerClient(this)); } - + protected updateMap(workspaces: WorkspaceInfo[]) { for (const ws of workspaces) { this.workspaces.set(ws.workspace.id, ws); } } - + dispose(): void { this.disposables.dispose(); } - + async onInstanceUpdate(instance: WorkspaceInstance) { if (this.workspaces) { if (this.workspaces.has(instance.workspaceId)) { @@ -58,7 +58,13 @@ export class WorkspaceModel implements Disposable, Partial { } } } - + + async deleteWorkspace(id: string): Promise { + await getGitpodService().server.deleteWorkspace(id); + this.workspaces.delete(id); + this.notifyWorkpaces(); + } + protected internalActive = true; get active() { return this.internalActive;