From 9522ac18deedc15311d3d464ee36c20e7f72e39f Mon Sep 17 00:00:00 2001 From: Gareth George Date: Fri, 22 Dec 2023 08:27:48 +0000 Subject: [PATCH] feat: add repo view --- webui/src/components/OperationList.tsx | 15 +++---- webui/src/components/OperationTree.tsx | 27 ++++++------ webui/src/state/oplog.ts | 30 +------------ webui/src/views/App.tsx | 25 +++++++++-- webui/src/views/PlanView.tsx | 7 --- webui/src/views/RepoView.tsx | 61 ++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 62 deletions(-) create mode 100644 webui/src/views/RepoView.tsx diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index c963d48b..5853d71b 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -40,7 +40,6 @@ import { import { SnapshotBrowser } from "./SnapshotBrowser"; import { formatBytes, - formatDuration, formatTime, normalizeSnapshotId, } from "../lib/formatting"; @@ -48,14 +47,15 @@ import _ from "lodash"; import { GetOperationsRequest, Restora } from "../../gen/ts/v1/service.pb"; import { useAlertApi } from "./Alerts"; import { MessageInstance } from "antd/es/message/interface"; -import Operation from "antd/es/transfer/operation"; export const OperationList = ({ req, useBackups, + showPlan, }: React.PropsWithoutRef<{ req?: GetOperationsRequest; useBackups?: BackupInfo[]; + showPlan?: boolean, }>) => { const alertApi = useAlertApi(); @@ -128,17 +128,13 @@ export const OperationList = ({ dataSource={backups} renderItem={(backup) => { const ops = backup.operations; - if (ops.length === 1) { - return ; - } - return ( {ops.map((op) => { if (shouldHideStatus(op.status!)) { return null; } - return + return })} ); @@ -155,7 +151,8 @@ export const OperationList = ({ export const OperationRow = ({ operation, alertApi, -}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance }>) => { + showPlan, +}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance, showPlan: boolean }>) => { const details = detailsForOperation(operation); const displayType = getTypeForDisplay(operation); let avatar: React.ReactNode; @@ -190,7 +187,7 @@ export const OperationRow = ({ const opName = displayTypeToString(getTypeForDisplay(operation)); let title = ( <> - {formatTime(operation.unixTimeStartMs!)} - {opName}{" "} + {showPlan ? operation.planId + " - " : undefined} {formatTime(operation.unixTimeStartMs!)} - {opName}{" "} {details.displayState} ); diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 01c9c5f8..58fc8465 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -201,65 +201,64 @@ export const OperationTree = ({ const buildTreePlan = (operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return op.planId; + return op.operations[0].planId!; }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { - key: "p" + key, - title: "" + key, - children: buildTreeYear(value), + key: key, + title: key, + children: buildTreeDay(key, value), }; }); - entries.sort(sortByKey); - if (entries.length === 1) { return entries[0].children!; } + entries.sort(sortByKey); return entries; }; -const buildTreeYear = (operations: BackupInfo[]): OpTreeNode[] => { +const buildTreeYear = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 4); }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { - key: "y" + key, + key: keyPrefix + key, title: "" + key, - children: buildTreeMonth(value), + children: buildTreeDay(keyPrefix, value), }; }); entries.sort(sortByKey); return entries; }; -const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => { +const buildTreeMonth = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 7); }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { - key: key, + key: keyPrefix + key, title: value[0].displayTime.toLocaleString("default", { month: "long", }), - children: buildTreeDay(value), + children: buildTreeDay(keyPrefix, value), }; }); entries.sort(sortByKey); return entries; }; -const buildTreeDay = (operations: BackupInfo[]): OpTreeNode[] => { +const buildTreeDay = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return localISOTime(op.displayTime).substring(0, 10); }); const entries = _.map(grouped, (value, key) => { return { - key: "d" + key, + key: keyPrefix + key, title: formatDate(value[0].displayTime), children: buildTreeLeaf(value), }; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 9ef33e04..fccd216c 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -163,37 +163,11 @@ export class BackupInfoCollector { backupLastStatus, snapshotInfo, forgotten, + planId: operations[0].planId, + repoId: operations[0].repoId, }; } - private operationToBackup(op: Operation): BackupInfo { - const startTimeMs = parseInt(op.unixTimeStartMs!); - const endTimeMs = parseInt(op.unixTimeEndMs!); - - const b: BackupInfo = { - id: op.id!, - startTimeMs, - endTimeMs, - status: op.status!, - displayTime: new Date(startTimeMs), - displayType: getTypeForDisplay(op), - repoId: op.repoId, - planId: op.planId, - snapshotId: op.snapshotId, - operations: [op], - }; - - if (op.operationBackup) { - const ob = op.operationBackup; - b.backupLastStatus = ob.lastStatus; - } - if (op.operationIndexSnapshot) { - const oi = op.operationIndexSnapshot; - b.snapshotInfo = oi.snapshot; - } - return b; - } - private addHelper(op: Operation): BackupInfo { if (op.snapshotId) { delete this.backupByOpId[op.id!]; diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 016031e5..9fd93ace 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -146,7 +146,6 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { label: "Add Repo", onClick: async () => { const { AddRepoModal } = await import("./AddRepoModal"); - showModal(); }, }, @@ -154,11 +153,29 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { return { key: "r-" + repo.id, icon: , - label: repo.id, + label: ( +
+ {repo.id}{" "} +
+ ), onClick: async () => { - const { AddRepoModal } = await import("./AddRepoModal"); + const { RepoView } = await import("./RepoView"); - showModal(); + setContent(, [ + { title: "Repos" }, + { title: repo.id || "" }, + ]); }, }; }), diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index a7bd426a..ed0c3ff1 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -1,22 +1,15 @@ import React, { useEffect, useState } from "react"; import { Plan } from "../../gen/ts/v1/config.pb"; import { Button, Flex, Tabs, Tooltip, Typography } from "antd"; -import { useShowModal } from "../components/ModalManager"; import { useRecoilValue } from "recoil"; import { configState } from "../state/config"; import { useAlertApi } from "../components/Alerts"; import { Restora } from "../../gen/ts/v1/service.pb"; -import { - EOperation, - subscribeToOperations, - unsubscribeFromOperations, -} from "../state/oplog"; import { OperationList } from "../components/OperationList"; import { OperationTree } from "../components/OperationTree"; import { MAX_OPERATION_HISTORY } from "../constants"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { - const showModal = useShowModal(); const alertsApi = useAlertApi()!; // Gracefully handle deletions by checking if the plan is still in the config. diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx new file mode 100644 index 00000000..106b23a9 --- /dev/null +++ b/webui/src/views/RepoView.tsx @@ -0,0 +1,61 @@ +import React, { useEffect, useState } from "react"; +import { Repo } from "../../gen/ts/v1/config.pb"; +import { Button, Flex, Tabs, Tooltip, Typography } from "antd"; +import { useRecoilValue } from "recoil"; +import { configState } from "../state/config"; +import { useAlertApi } from "../components/Alerts"; +import { Restora } from "../../gen/ts/v1/service.pb"; +import { OperationList } from "../components/OperationList"; +import { OperationTree } from "../components/OperationTree"; +import { MAX_OPERATION_HISTORY } from "../constants"; + +export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => { + const alertsApi = useAlertApi()!; + + // Gracefully handle deletions by checking if the plan is still in the config. + const config = useRecoilValue(configState); + let repoInConfig = config.repos?.find((p) => p.id === repo.id); + if (!repoInConfig) { + return

Repo was deleted.

; + } + repo = repoInConfig; + + return ( + <> + + + {repo.id} + + + + + + ), + }, + { + key: "2", + label: "Operation List", + children: ( + <> +

Backup Action History

+ + + ), + }, + ]} + /> + + ); +};