diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go
index 6e2889a3..3ba7358d 100644
--- a/internal/orchestrator/tasks.go
+++ b/internal/orchestrator/tasks.go
@@ -56,7 +56,7 @@ func (t *ScheduledBackupTask) Next(now time.Time) *time.Time {
}
if lastBackupOp != nil {
- now = time.Unix(0, lastBackupOp.UnixTimeEndMs*int64(time.Millisecond))
+ now = time.Unix(0, lastBackupOp.UnixTimeStartMs*int64(time.Millisecond))
}
} else {
zap.S().Errorf("error getting last operation for plan %q when computing backup schedule: %v", t.plan.Id, err)
diff --git a/webui/package-lock.json b/webui/package-lock.json
index 4cc0c5d3..8531c804 100644
--- a/webui/package-lock.json
+++ b/webui/package-lock.json
@@ -1224,6 +1224,11 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
},
+ "@types/lodash": {
+ "version": "4.14.202",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
+ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
+ },
"@types/node": {
"version": "20.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.1.tgz",
@@ -1830,6 +1835,11 @@
}
}
},
+ "lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
diff --git a/webui/package.json b/webui/package.json
index 8e0380b4..99cf9377 100644
--- a/webui/package.json
+++ b/webui/package.json
@@ -11,11 +11,13 @@
"license": "ISC",
"dependencies": {
"@ant-design/icons": "^5.2.6",
+ "@types/lodash": "^4.14.202",
"@types/node": "^20.9.0",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"antd": "^5.11.1",
"buffer": "^6.0.3",
+ "lodash": "^4.17.21",
"parcel": "^2.10.2",
"process": "^0.11.10",
"react": "^18.2.0",
diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx
new file mode 100644
index 00000000..e48e9594
--- /dev/null
+++ b/webui/src/components/OperationTree.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { EOperation, getOperations } from "../state/oplog";
+import { Tree } from "antd";
+import _ from "lodash";
+import { DataNode } from "antd/es/tree";
+import { formatDate, formatTime } from "../lib/formatting";
+
+export const OperationTree = ({
+ operations,
+}: React.PropsWithoutRef<{ operations: EOperation[] }>) => {
+ operations.sort((a, b) => b.parsedTime - a.parsedTime);
+ return ;
+};
+
+// TODO: more work on this view
+const buildTree = (operations: EOperation[]): DataNode[] => {
+ const grouped = _.groupBy(operations, (op) => {
+ return new Date(op.parsedTime).toLocaleDateString("default", {
+ month: "long",
+ year: "numeric",
+ day: "numeric",
+ });
+ });
+
+ return _.keys(grouped).map((key) => {
+ return {
+ key: key,
+ title: key,
+ children: grouped[key].map((op) => {
+ return {
+ key: op.id!,
+ title: {formatTime(op.parsedTime)} - AN OPERATION,
+ };
+ }),
+ };
+ });
+};
diff --git a/webui/src/components/SnapshotBrowser.tsx b/webui/src/components/SnapshotBrowser.tsx
index 9be49c98..6ebf8197 100644
--- a/webui/src/components/SnapshotBrowser.tsx
+++ b/webui/src/components/SnapshotBrowser.tsx
@@ -20,6 +20,9 @@ const replaceKeyInTree = (
if (curNode.key === setKey) {
return setValue;
}
+ if (setKey.indexOf(curNode.key as string) === -1) {
+ return null;
+ }
if (!curNode.children) {
return null;
}
@@ -83,8 +86,6 @@ export const SnapshotBrowser = ({
return;
}
- console.log("Loading data for key: " + key);
-
const resp = await ResticUI.ListSnapshotFiles(
{
path: (key + "/") as string,
@@ -103,33 +104,20 @@ export const SnapshotBrowser = ({
}
if (!toUpdate) {
- console.log("No node to update found!");
return;
}
const toUpdateCopy = { ...toUpdate };
toUpdateCopy.children = respToNodes(resp);
- console.log(
- "Replacing key: " +
- key +
- " with: " +
- JSON.stringify(toUpdateCopy, null, 2)
- );
- console.log("In tree: " + JSON.stringify(treeData, null, 2));
-
const newTree = treeData.map((node) => {
- console.log("trying replace in tree...");
const didUpdate = replaceKeyInTree(node, key as string, toUpdateCopy);
if (didUpdate) {
- console.log("Replaced in tree successfully!");
return didUpdate;
}
return node;
});
- console.log("New tree: ", JSON.stringify(newTree, null, 2));
-
setTreeData(newTree);
};
diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts
index ca55e62a..e6f9b040 100644
--- a/webui/src/lib/formatting.ts
+++ b/webui/src/lib/formatting.ts
@@ -16,6 +16,7 @@ export const formatBytes = (bytes?: number | string) => {
};
const timezoneOffsetMs = new Date().getTimezoneOffset() * 60 * 1000;
+// formatTime formats a time as YYYY-MM-DD at HH:MM AM/PM
export const formatTime = (time: number | string) => {
if (typeof time === "string") {
time = parseInt(time);
@@ -23,7 +24,23 @@ export const formatTime = (time: number | string) => {
const d = new Date();
d.setTime(time - timezoneOffsetMs);
const isoStr = d.toISOString();
- return `${isoStr.substring(0, 10)} ${d.getUTCHours()}h${d.getUTCMinutes()}m`;
+ const hours = d.getUTCHours() % 12 == 0 ? 12 : d.getUTCHours() % 12;
+ const minutes =
+ d.getUTCMinutes() < 10 ? "0" + d.getUTCMinutes() : d.getUTCMinutes();
+ return `${isoStr.substring(0, 10)} at ${hours}:${minutes} ${
+ d.getUTCHours() > 12 ? "PM" : "AM"
+ }`;
+};
+
+// formatDate formats a time as YYYY-MM-DD
+export const formatDate = (time: number | string) => {
+ if (typeof time === "string") {
+ time = parseInt(time);
+ }
+ const d = new Date();
+ d.setTime(time - timezoneOffsetMs);
+ const isoStr = d.toISOString();
+ return isoStr.substring(0, 4);
};
export const formatDuration = (ms: number) => {
diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts
index d3a75b0a..b83c8803 100644
--- a/webui/src/state/oplog.ts
+++ b/webui/src/state/oplog.ts
@@ -112,7 +112,7 @@ export const buildOperationListListener = (
} else {
operations.push(op);
operations.sort((a, b) => {
- return parseInt(a.id!) - parseInt(b.id!);
+ return a.parsedId - b.parsedId;
});
}
} else if (type === OperationEventType.EVENT_CREATED) {
diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx
index 500fc23b..61d6062a 100644
--- a/webui/src/views/App.tsx
+++ b/webui/src/views/App.tsx
@@ -9,7 +9,7 @@ import {
import type { MenuProps } from "antd";
import { Button, Layout, List, Menu, Modal, Spin, theme } from "antd";
import { configState, fetchConfig } from "../state/config";
-import { useRecoilState } from "recoil";
+import { useRecoilState, useRecoilValue } from "recoil";
import { Config, Plan } from "../../gen/ts/v1/config.pb";
import { useAlertApi } from "../components/Alerts";
import { useShowModal } from "../components/ModalManager";
@@ -33,6 +33,7 @@ import {
OperationEvent,
OperationEventType,
} from "../../gen/ts/v1/operations.pb";
+import { MessageInstance } from "antd/es/message/interface";
const { Header, Content, Sider } = Layout;
@@ -66,6 +67,7 @@ export const App: React.FC = () => {
return (
+