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");
+ showModal(
);
+ }}
+ />
+
+ ),
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
+
+ >
+ ),
+ },
+ ]}
+ />
+ >
+ );
+};