From 5d372ea5746f95edff0ff6f6785a1f670eaf7302 Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Wed, 15 Jun 2022 10:53:09 +0000 Subject: [PATCH 1/4] [dashboard] introduce folded inactive ws section --- .../dashboard/src/workspaces/Workspaces.tsx | 85 +++++++++++++++---- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index ccd1b7ea45f202..7c4e0062415d7c 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -18,6 +18,8 @@ import { User } from "@gitpod/gitpod-protocol"; import { useLocation } from "react-router"; import { StartWorkspaceModalContext, StartWorkspaceModalKeyBinding } from "./start-workspace-modal-context"; import SelectIDEModal from "../settings/SelectIDEModal"; +import Arrow from "../components/Arrow"; +import ConfirmationModal from "../components/ConfirmationModal"; export interface WorkspacesProps {} @@ -25,6 +27,8 @@ export interface WorkspacesState { workspaces: WorkspaceInfo[]; isTemplateModelOpen: boolean; repos: WhitelistedRepository[]; + showInactive: boolean; + deleteModalVisible: boolean; } export default function () { @@ -35,6 +39,8 @@ export default function () { const [activeWorkspaces, setActiveWorkspaces] = useState([]); const [inactiveWorkspaces, setInactiveWorkspaces] = useState([]); const [workspaceModel, setWorkspaceModel] = useState(); + const [showInactive, setShowInactive] = useState(); + const [deleteModalVisible, setDeleteModalVisible] = useState(); const { setIsStartWorkspaceModalVisible } = useContext(StartWorkspaceModalContext); useEffect(() => { @@ -50,6 +56,18 @@ export default function () { <>
+ setDeleteModalVisible(false)} + onConfirm={() => { + inactiveWorkspaces.forEach((ws) => workspaceModel?.deleteWorkspace(ws.workspace.id)); + setDeleteModalVisible(false); + }} + > + {isOnboardingUser && } {workspaceModel?.initialized && @@ -128,27 +146,58 @@ export default function () { })} {activeWorkspaces.length > 0 &&
} {inactiveWorkspaces.length > 0 && ( -
- Unpinned workspaces that have been inactive for more than 14 days will be - automatically deleted.{" "} - +
+
+ {showInactive ? ( + <> +
+
+ Unpinned workspaces that have been inactive for more than 14 days + will be automatically deleted.{" "} + + Learn more + +
+ +
+ {inactiveWorkspaces.map((e) => { + return ( + + getGitpodService().server.stopWorkspace(wsId) + } + /> + ); + })} + + ) : null}
)} - {inactiveWorkspaces.map((e) => { - return ( - getGitpodService().server.stopWorkspace(wsId)} - /> - ); - })} ) : ( From fd2a5359e7d41bbf2fef4db049fe59696c2fb465 Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Wed, 15 Jun 2022 10:53:41 +0000 Subject: [PATCH 2/4] [dashboard] Treat today's workspaces as active --- components/dashboard/src/components/Arrow.tsx | 2 +- .../dashboard/src/workspaces/Workspaces.tsx | 63 ++++++++++--------- .../src/workspaces/workspace-model.ts | 7 ++- .../gitpod-protocol/src/util/timeutil.ts | 6 ++ 4 files changed, 48 insertions(+), 30 deletions(-) diff --git a/components/dashboard/src/components/Arrow.tsx b/components/dashboard/src/components/Arrow.tsx index ff1132afc53798..7ac5b282516042 100644 --- a/components/dashboard/src/components/Arrow.tsx +++ b/components/dashboard/src/components/Arrow.tsx @@ -10,7 +10,7 @@ function Arrow(props: { up: boolean; customBorderClasses?: string }) { className={ "mx-2 " + (props.customBorderClasses || - "border-gray-400 dark:border-gray-600 group-hover:border-gray-600 dark:group-hover:border-gray-400") + "border-gray-400 dark:border-gray-500 group-hover:border-gray-600 dark:group-hover:border-gray-400") } style={{ marginTop: 2, diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 7c4e0062415d7c..c3cfd8a1bd2931 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -58,7 +58,7 @@ export default function () { setDeleteModalVisible(false)} @@ -146,42 +146,49 @@ export default function () { })} {activeWorkspaces.length > 0 &&
} {inactiveWorkspaces.length > 0 && ( -
-
+
setShowInactive(!showInactive)} - className="flex cursor-pointer py-6" + className="flex cursor-pointer py-6 px-6 flex-row text-gray-400 bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-xl mb-2" > -
-

Inactive Workspaces

+
+
-
- +
+
+ Inactive Workspaces  + + {inactiveWorkspaces.length} + +
+
+ Unpinned workspaces that have been inactive for more than 14 days will + be automatically deleted.{" "} + evt.stopPropagation()} + > + Learn more + +
-
- {showInactive ? ( - <> -
-
- Unpinned workspaces that have been inactive for more than 14 days - will be automatically deleted.{" "} - - Learn more - -
+
+ {showInactive ? ( -
+ ) : null} +
+
+ {showInactive ? ( + <> {inactiveWorkspaces.map((e) => { return ( { @@ -145,8 +146,12 @@ export class WorkspaceModel implements Disposable, Partial { } protected isActive(info: WorkspaceInfo): boolean { + const lastSessionStart = WorkspaceInfo.lastActiveISODate(info); + const twentyfourHoursAgo = hoursBefore(new Date().toISOString(), 24); return ( - (info.workspace.pinned || (!!info.latestInstance && info.latestInstance.status?.phase !== "stopped")) && + (info.workspace.pinned || + (!!info.latestInstance && info.latestInstance.status?.phase !== "stopped") || + isDateSmallerOrEqual(twentyfourHoursAgo, lastSessionStart)) && !info.workspace.softDeleted ); } diff --git a/components/gitpod-protocol/src/util/timeutil.ts b/components/gitpod-protocol/src/util/timeutil.ts index ca54246b1c9646..9f0971b46d8832 100644 --- a/components/gitpod-protocol/src/util/timeutil.ts +++ b/components/gitpod-protocol/src/util/timeutil.ts @@ -47,6 +47,12 @@ export const orderAsc = (d1: string, d2: string): number => liftDate(d1, d2, (d1 export const liftDate1 = (d1: string, f: (d1: Date) => T): T => f(new Date(d1)); export const liftDate = (d1: string, d2: string, f: (d1: Date, d2: Date) => T): T => f(new Date(d1), new Date(d2)); +export function hoursBefore(date: string, hours: number): string { + const result = new Date(date); + result.setHours(result.getHours() - hours); + return result.toISOString(); +} + export function hoursLater(date: string, hours: number): string { const result = new Date(date); result.setHours(result.getHours() + hours); From 2ab49f3b0c8dc18401aa167b5519eff0432ebe10 Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Wed, 15 Jun 2022 11:09:22 +0000 Subject: [PATCH 3/4] [dashboard] non visible dialogs ignore keyevents --- components/dashboard/src/components/Modal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/dashboard/src/components/Modal.tsx b/components/dashboard/src/components/Modal.tsx index 70484bfcb2d0d0..0dc05e9e839dd6 100644 --- a/components/dashboard/src/components/Modal.tsx +++ b/components/dashboard/src/components/Modal.tsx @@ -37,6 +37,9 @@ export default function Modal(props: { .catch(console.error); }; const handler = (evt: KeyboardEvent) => { + if (!props.visible) { + return; + } if (evt.defaultPrevented) { return; } From 0476c1e7fced7b2b3bdeb8dbf29bab2f499c337e Mon Sep 17 00:00:00 2001 From: Sven Efftinge Date: Wed, 15 Jun 2022 11:41:54 +0000 Subject: [PATCH 4/4] [dashboard] sort workspaces by activity --- .../dashboard/src/workspaces/workspace-model.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/components/dashboard/src/workspaces/workspace-model.ts b/components/dashboard/src/workspaces/workspace-model.ts index 3d6ec2c419a6c5..bc60a498a2c915 100644 --- a/components/dashboard/src/workspaces/workspace-model.ts +++ b/components/dashboard/src/workspaces/workspace-model.ts @@ -136,8 +136,23 @@ export class WorkspaceModel implements Disposable, Partial { .indexOf(this.searchTerm!.toLowerCase()) !== -1, ); } + const now = new Date().toISOString(); + function activeDate(info: WorkspaceInfo): string { + if (!info.latestInstance) { + return info.workspace.creationTime; + } + if (info.latestInstance.status.phase === "stopped" || info.latestInstance.status.phase === "unknown") { + return WorkspaceInfo.lastActiveISODate(info); + } + return info.latestInstance.stoppedTime || info.latestInstance.stoppingTime || now; + } infos = infos.sort((a, b) => { - return WorkspaceInfo.lastActiveISODate(b).localeCompare(WorkspaceInfo.lastActiveISODate(a)); + const result = activeDate(b).localeCompare(activeDate(a)); + if (result === 0) { + // both active now? order by creationtime + return WorkspaceInfo.lastActiveISODate(b).localeCompare(WorkspaceInfo.lastActiveISODate(a)); + } + return result; }); const activeInfo = infos.filter((ws) => this.isActive(ws)); const inActiveInfo = infos.filter((ws) => !this.isActive(ws));