From cb4b20d94e02df208296833821369ca77be0d575 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Thu, 23 Nov 2023 02:07:27 -0800 Subject: [PATCH 1/6] feat: initial optree implementation --- internal/orchestrator/tasks.go | 2 +- webui/package-lock.json | 10 ++ webui/package.json | 2 + webui/src/components/OperationTree.tsx | 37 ++++++ webui/src/components/SnapshotBrowser.tsx | 18 +-- webui/src/lib/formatting.ts | 19 ++- webui/src/state/oplog.ts | 2 +- webui/src/views/App.tsx | 145 +++++++++-------------- webui/src/views/PlanView.tsx | 31 ++++- 9 files changed, 152 insertions(+), 114 deletions(-) create mode 100644 webui/src/components/OperationTree.tsx 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 ( +

{ const getSidenavItems = (config: Config | null): MenuProps["items"] => { const showModal = useShowModal(); const setContent = useSetContent(); - const [snapshotsByPlan, setSnapshotsByPlan] = useState<{ - [planId: string]: EOperation[]; - }>({}); - - const addSnapshots = (planId: string, ops: EOperation[]) => { - const snapsByPlanCpy = { ...snapshotsByPlan }; - let snapsForPlanCpy = [...(snapsByPlanCpy[planId] || [])]; - for (const op of ops) { - snapsForPlanCpy.push(toEop(op)); - } - snapsForPlanCpy.sort((a, b) => { - return a.parsedTime > b.parsedTime ? -1 : 1; - }); - if (snapsForPlanCpy.length > 5) { - snapsForPlanCpy = snapsForPlanCpy.slice(0, 5); - } - snapsByPlanCpy[planId] = snapsForPlanCpy; - setSnapshotsByPlan(snapsByPlanCpy); - }; - - // Track newly created snapshots in the set. - useEffect(() => { - const listener = (event: OperationEvent) => { - if (event.type !== OperationEventType.EVENT_CREATED) return; - const op = event.operation!; - if (!op.planId) return; - if (!op.operationIndexSnapshot) return; - addSnapshots(op.planId!, [toEop(op)]); - }; - - subscribeToOperations(listener); - - return () => { - unsubscribeFromOperations(listener); - }; - }, [snapshotsByPlan]); if (!config) return []; const configPlans = config.plans || []; const configRepos = config.repos || []; - const onSelectPlan = (plan: Plan) => { - setContent(, [ - { title: "Plans" }, - { title: plan.id || "" }, - ]); - - if (!snapshotsByPlan[plan.id!]) { - (async () => { - const ops = await getOperations({ planId: plan.id!, lastN: "20" }); - // avoid races by checking again after the request - if (!snapshotsByPlan[plan.id!]) { - const snapshots = ops.filter((op) => !!op.operationIndexSnapshot); - addSnapshots(plan.id!, snapshots); - } - })(); - } - }; - const plans: MenuProps["items"] = [ { key: "add-plan", @@ -168,47 +116,16 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { }, }, ...configPlans.map((plan) => { - const children: MenuProps["items"] = ( - snapshotsByPlan[plan.id!] || [] - ).map((snapshot) => { - return { - key: "s-" + snapshot.id, - icon: , - label: ( - {"Operation " + formatTime(snapshot.parsedTime)} - ), - onClick: () => { - showModal( - showModal(null)} - footer={[ - , - ]} - > - - - - - ); - }, - }; - }); - return { key: "p-" + plan.id, icon: , label: plan.id, - children: children, - onTitleClick: onSelectPlan.bind(null, plan), // if children - onClick: onSelectPlan.bind(null, plan), // if no children + onClick: () => { + setContent(, [ + { title: "Plans" }, + { title: plan.id || "" }, + ]); + }, }; }), ]; @@ -249,3 +166,47 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => { }, ]; }; + +const OperationNotificationGenerator = () => { + const alertApi = useAlertApi()!; + const setContent = useSetContent(); + const config = useRecoilValue(configState); + + useEffect(() => { + const listener = (event: OperationEvent) => { + if (event.type != OperationEventType.EVENT_CREATED) return; + const planId = event.operation!.planId!; + const repoId = event.operation!.repoId!; + + const onClick = () => { + const plan = config.plans!.find((p) => p.id == planId); + if (!plan) return; + setContent(, [ + { title: "Plans" }, + { title: planId || "" }, + ]); + }; + + if (event.operation?.operationBackup) { + alertApi.info({ + content: `Backup started for plan ${planId}.`, + onClick: onClick, + }); + } else if (event.operation?.operationIndexSnapshot) { + const indexOp = event.operation.operationIndexSnapshot; + alertApi.info({ + content: `Indexed snapshot ${indexOp.snapshot! + .id!} for plan ${planId}.`, + onClick: onClick, + }); + } + }; + subscribeToOperations(listener); + + return () => { + unsubscribeFromOperations(listener); + }; + }, [config]); + + return <>; +}; diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index bbf82f51..ce2cf616 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { Plan } from "../../gen/ts/v1/config.pb"; -import { Button, Flex } from "antd"; +import { Button, Flex, Tabs } from "antd"; import { SettingOutlined } from "@ant-design/icons"; import { AddPlanModal } from "./AddPlanModel"; import { useShowModal } from "../components/ModalManager"; @@ -15,6 +15,7 @@ import { unsubscribeFromOperations, } from "../state/oplog"; import { OperationList } from "../components/OperationList"; +import { OperationTree } from "../components/OperationTree"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { const showModal = useShowModal(); @@ -23,7 +24,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { useEffect(() => { const listener = buildOperationListListener( - { planId: plan.id, lastN: "1000" }, + { planId: plan.id, lastN: "10000" }, (event, changedOp, operations) => { setOperations([...operations]); } @@ -78,8 +79,30 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { Prune Now -

Backup Action History ({operations.length} loaded)

- + + + + ), + }, + { + key: "2", + label: "Operation List", + children: ( + <> +

Backup Action History ({operations.length} loaded)

+ + + ), + }, + ]} + /> ); }; From 4bbd511e61832c99771ad2054f28714219219b5c Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 25 Nov 2023 17:09:00 -0800 Subject: [PATCH 2/6] created basic tree layout --- gen/go/v1/operations.pb.go | 23 +++++--- internal/oplog/oplog.go | 18 ++++-- internal/orchestrator/tasks.go | 19 +----- proto/v1/operations.proto | 12 ++-- webui/src/components/OperationList.tsx | 2 +- webui/src/components/OperationTree.tsx | 82 ++++++++++++++++++++------ webui/src/constants.ts | 2 + webui/src/lib/formatting.ts | 12 ++-- webui/src/state/config.ts | 9 +-- webui/src/state/oplog.ts | 24 ++++++-- webui/src/views/PlanView.tsx | 7 ++- 11 files changed, 141 insertions(+), 69 deletions(-) create mode 100644 webui/src/constants.ts diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 641c1cba..e3e0616a 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -177,14 +177,21 @@ type Operation struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` - RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` // repo id if associated with a repo (always true) - PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` // plan id if associated with a plan (always true) - SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` // snapshot id if associated with a snapshot. - Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` - UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"` - UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"` - DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` // human readable context message (if any) + // required, primary ID of the operation. + Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + // required, repo id if associated with a repo + RepoId string `protobuf:"bytes,2,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + // required, plan id if associated with a plan + PlanId string `protobuf:"bytes,3,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + // optional snapshot id if associated with a snapshot. + SnapshotId string `protobuf:"bytes,8,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Status OperationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=v1.OperationStatus" json:"status,omitempty"` + // required, unix time in milliseconds of the operation's creation (ID is derived from this) + UnixTimeStartMs int64 `protobuf:"varint,5,opt,name=unix_time_start_ms,json=unixTimeStartMs,proto3" json:"unix_time_start_ms,omitempty"` + // optional, unix time in milliseconds of the operation's completion + UnixTimeEndMs int64 `protobuf:"varint,6,opt,name=unix_time_end_ms,json=unixTimeEndMs,proto3" json:"unix_time_end_ms,omitempty"` + // optional, human readable context message, typically an error message. + DisplayMessage string `protobuf:"bytes,7,opt,name=display_message,json=displayMessage,proto3" json:"display_message,omitempty"` // Types that are assignable to Op: // // *Operation_OperationBackup diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index be356dd9..b4c10c3a 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -195,17 +195,25 @@ func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, err return &op, nil } +func (o *OpLog) nextOperationId(b *bolt.Bucket, unixTimeMs int64) (int64, error) { + seq, err := b.NextSequence() + if err != nil { + return 0, fmt.Errorf("next sequence: %w", err) + } + return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil +} + func (o *OpLog) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { b := tx.Bucket(OpLogBucket) if op.Id == 0 { - seq, err := b.NextSequence() - if err != nil { - return fmt.Errorf("error getting next sequence: %w", err) - } if op.UnixTimeStartMs == 0 { return fmt.Errorf("operation must have a start time") } - op.Id = op.UnixTimeStartMs<<20 | int64(seq&(1<<20-1)) + var err error + op.Id, err = o.nextOperationId(b, op.UnixTimeStartMs) + if err != nil { + return fmt.Errorf("create next operation ID: %w", err) + } } op.SnapshotId = NormalizeSnapshotId(op.SnapshotId) diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 3ba7358d..119f00e5 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -47,21 +47,6 @@ func (t *ScheduledBackupTask) Name() string { } func (t *ScheduledBackupTask) Next(now time.Time) *time.Time { - if ops, err := t.orchestrator.OpLog.GetByPlan(t.plan.Id, indexutil.CollectLastN(10)); err == nil { - var lastBackupOp *v1.Operation - for _, op := range ops { - if _, ok := op.Op.(*v1.Operation_OperationBackup); ok { - lastBackupOp = op - } - } - - if lastBackupOp != nil { - 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) - } - next := t.schedule.Next(now) return &next } @@ -116,7 +101,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan startTime := time.Now() err := WithOperation(orchestrator.OpLog, op, func() error { - zap.L().Info("Starting backup", zap.String("plan", plan.Id)) + zap.L().Info("Starting backup", zap.String("plan", plan.Id), zap.Int64("opId", op.Id)) repo, err := orchestrator.GetRepo(plan.Repo) if err != nil { return fmt.Errorf("couldn't get repo %q: %w", plan.Repo, err) @@ -230,7 +215,7 @@ func WithOperation(oplog *oplog.OpLog, op *v1.Operation, do func() error) error if op.Status == v1.OperationStatus_STATUS_INPROGRESS { op.Status = v1.OperationStatus_STATUS_SUCCESS } - if e := oplog.Update(op); err != nil { + if e := oplog.Update(op); e != nil { return multierror.Append(err, fmt.Errorf("failed to update operation in oplog: %w", e)) } return err diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 6c2cb26c..bc9ac6dd 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -11,18 +11,20 @@ message OperationList { } message Operation { + // required, primary ID of the operation. int64 id = 1; - // repo id if associated with a repo (always true) + // required, repo id if associated with a repo string repo_id = 2; - // plan id if associated with a plan (always true) + // required, plan id if associated with a plan string plan_id = 3; - // snapshot id if associated with a snapshot. + // optional snapshot id if associated with a snapshot. string snapshot_id = 8; OperationStatus status = 4; - // unix time in milliseconds of the operation's creation (ID is derived from this) + // required, unix time in milliseconds of the operation's creation (ID is derived from this) int64 unix_time_start_ms = 5; + // optional, unix time in milliseconds of the operation's completion int64 unix_time_end_ms = 6; - // human readable context message, typically an error message. + // optional, human readable context message, typically an error message. string display_message = 7; oneof op { diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 9dc6728a..675d37fa 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -28,7 +28,7 @@ import { export const OperationList = ({ operations, }: React.PropsWithoutRef<{ operations: EOperation[] }>) => { - operations.sort((a, b) => b.parsedTime - a.parsedTime); + operations = [...operations].reverse(); if (operations.length === 0) { return ( diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index e48e9594..7b7407b6 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -8,30 +8,78 @@ import { formatDate, formatTime } from "../lib/formatting"; export const OperationTree = ({ operations, }: React.PropsWithoutRef<{ operations: EOperation[] }>) => { - operations.sort((a, b) => b.parsedTime - a.parsedTime); - return ; + operations = [...operations].reverse(); // reverse such that newest operations are at index 0. + + const treeData = buildTreeYear(operations); + const keys = buildDefaultExpandedKeys(treeData); + + return ; }; -// TODO: more work on this view -const buildTree = (operations: EOperation[]): DataNode[] => { +const buildDefaultExpandedKeys = (tree?: DataNode[]) => { + const keys: string[] = []; + while (tree && tree.length > 0) { + const node = tree[0]; + keys.push(node.key as string); + tree = node.children!; + } + return keys; +}; + +const buildTreeYear = (operations: EOperation[]): DataNode[] => { const grouped = _.groupBy(operations, (op) => { - return new Date(op.parsedTime).toLocaleDateString("default", { - month: "long", - year: "numeric", - day: "numeric", - }); + return op.parsedDate.getFullYear(); }); - return _.keys(grouped).map((key) => { + const entries: DataNode[] = _.map(grouped, (value, key) => { return { - key: key, - title: key, - children: grouped[key].map((op) => { - return { - key: op.id!, - title: {formatTime(op.parsedTime)} - AN OPERATION, - }; + key: "y" + key, + title: "" + key, + children: buildTreeMonth(value), + }; + }); + return entries; +}; + +const buildTreeMonth = (operations: EOperation[]): DataNode[] => { + const grouped = _.groupBy(operations, (op) => { + return op.parsedDate.getMonth(); + }); + const entries: DataNode[] = _.map(grouped, (value, key) => { + return { + key: "m" + key, + title: value[0].parsedDate.toLocaleString("default", { + month: "long", + year: "numeric", }), + children: buildTreeDay(value), + }; + }); + return entries; +}; + +const buildTreeDay = (operations: EOperation[]): DataNode[] => { + const grouped = _.groupBy(operations, (op) => { + console.log("Operation date: " + formatDate(op.parsedTime)); + return formatDate(op.parsedTime); + }); + + const entries = _.map(grouped, (value, key) => { + return { + key: "d" + key, + title: formatDate(value[0].parsedTime), + children: buildTreeLeaf(value), + }; + }); + return entries; +}; + +const buildTreeLeaf = (operations: EOperation[]): DataNode[] => { + return _.map(operations, (op) => { + return { + key: op.id!, + title: formatTime(op.parsedDate) + " - ", + isLeaf: true, }; }); }; diff --git a/webui/src/constants.ts b/webui/src/constants.ts new file mode 100644 index 00000000..c11d281d --- /dev/null +++ b/webui/src/constants.ts @@ -0,0 +1,2 @@ +export const API_PREFIX = "/api"; +export const MAX_OPERATION_HISTORY = 10000; diff --git a/webui/src/lib/formatting.ts b/webui/src/lib/formatting.ts index e6f9b040..7510b505 100644 --- a/webui/src/lib/formatting.ts +++ b/webui/src/lib/formatting.ts @@ -17,9 +17,11 @@ 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) => { +export const formatTime = (time: number | string | Date) => { if (typeof time === "string") { time = parseInt(time); + } else if (time instanceof Date) { + time = time.getTime(); } const d = new Date(); d.setTime(time - timezoneOffsetMs); @@ -33,14 +35,16 @@ export const formatTime = (time: number | string) => { }; // formatDate formats a time as YYYY-MM-DD -export const formatDate = (time: number | string) => { +export const formatDate = (time: number | string | Date) => { if (typeof time === "string") { time = parseInt(time); + } else if (time instanceof Date) { + time = time.getTime(); } - const d = new Date(); + let d = new Date(); d.setTime(time - timezoneOffsetMs); const isoStr = d.toISOString(); - return isoStr.substring(0, 4); + return isoStr.substring(0, 10); }; export const formatDuration = (ms: number) => { diff --git a/webui/src/state/config.ts b/webui/src/state/config.ts index 9b8f0d21..8ff09728 100644 --- a/webui/src/state/config.ts +++ b/webui/src/state/config.ts @@ -1,6 +1,7 @@ import { atom, useSetRecoilState } from "recoil"; import { Config, Repo } from "../../gen/ts/v1/config.pb"; import { ResticUI } from "../../gen/ts/v1/service.pb"; +import { API_PREFIX } from "../constants"; export const configState = atom({ key: "config", @@ -8,17 +9,17 @@ export const configState = atom({ }); export const fetchConfig = async (): Promise => { - return await ResticUI.GetConfig({}, { pathPrefix: "/api" }); + return await ResticUI.GetConfig({}, { pathPrefix: API_PREFIX }); }; export const addRepo = async (repo: Repo): Promise => { return await ResticUI.AddRepo(repo, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, }); }; export const updateConfig = async (config: Config): Promise => { return await ResticUI.SetConfig(config, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, }); -}; \ No newline at end of file +}; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index b83c8803..0005b315 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -8,10 +8,13 @@ import { import { GetOperationsRequest, ResticUI } from "../../gen/ts/v1/service.pb"; import { EventEmitter } from "events"; import { useAlertApi } from "../components/Alerts"; +import { API_PREFIX } from "../constants"; +import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; export type EOperation = Operation & { parsedId: number; parsedTime: number; + parsedDate: Date; }; const subscribers: ((event: OperationEvent) => void)[] = []; @@ -28,7 +31,7 @@ const subscribers: ((event: OperationEvent) => void)[] = []; subscribers.forEach((subscriber) => subscriber(event)); }, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, } ); } catch (e: any) { @@ -52,7 +55,7 @@ export const getOperations = async ({ lastN, }, { - pathPrefix: "/api", + pathPrefix: API_PREFIX, } ); return (opList.operations || []).map(toEop); @@ -124,12 +127,23 @@ export const buildOperationListListener = ( }; export const toEop = (op: Operation): EOperation => { - const time = - op.operationIndexSnapshot?.snapshot?.unixTimeMs || op.unixTimeStartMs; + const time = parseInt(op.unixTimeStartMs!); + const date = new Date(); + date.setTime(time); return { ...op, parsedId: parseInt(op.id!), - parsedTime: parseInt(time!), + parsedTime: time, + parsedDate: date, }; }; + +// TODO: aggregate backup info from oplog. +interface BackupInfo { + opids: string[]; + repoId: string; + planId: string; + backupLastStatus?: BackupProgressEntry; + snapshotInfo?: ResticSnapshot; +} diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index ce2cf616..f4d1c7e5 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -16,6 +16,7 @@ import { } 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(); @@ -24,7 +25,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { useEffect(() => { const listener = buildOperationListListener( - { planId: plan.id, lastN: "10000" }, + { planId: plan.id, lastN: "" + MAX_OPERATION_HISTORY }, (event, changedOp, operations) => { setOperations([...operations]); } @@ -87,7 +88,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { label: "Condensed View", children: ( <> - + ), }, @@ -97,7 +98,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { children: ( <>

Backup Action History ({operations.length} loaded)

- + ), }, From e1a10e57c87f51af934e425927ddb848edc872bf Mon Sep 17 00:00:00 2001 From: Gareth George Date: Fri, 24 Nov 2023 00:37:05 -0800 Subject: [PATCH 3/6] differentiate operations in tree view --- webui/src/components/OperationList.tsx | 4 +- webui/src/components/OperationTree.tsx | 113 +++++++++++++++++++------ webui/src/state/oplog.ts | 110 ++++++++++++++++++++++-- 3 files changed, 193 insertions(+), 34 deletions(-) diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 061bf260..3fc863e7 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -68,13 +68,13 @@ export const OperationList = ({ dataSource={groupedItems} renderItem={(group, index) => { if (group.length === 1) { - return ; + return ; } return ( {group.map((op) => ( - + ))} ); diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 7b7407b6..03ceb097 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -4,50 +4,78 @@ import { Tree } from "antd"; import _ from "lodash"; import { DataNode } from "antd/es/tree"; import { formatDate, formatTime } from "../lib/formatting"; +import { + ExclamationOutlined, + QuestionOutlined, + SaveOutlined, +} from "@ant-design/icons"; +import { OperationStatus } from "../../gen/ts/v1/operations.pb"; + +type OpTreeNode = DataNode & { + operation?: EOperation; +}; export const OperationTree = ({ operations, }: React.PropsWithoutRef<{ operations: EOperation[] }>) => { operations = [...operations].reverse(); // reverse such that newest operations are at index 0. - const treeData = buildTreeYear(operations); - const keys = buildDefaultExpandedKeys(treeData); - - return ; -}; - -const buildDefaultExpandedKeys = (tree?: DataNode[]) => { - const keys: string[] = []; - while (tree && tree.length > 0) { - const node = tree[0]; - keys.push(node.key as string); - tree = node.children!; + if (operations.length === 0) { + return ( +
+ No operations yet. +
+ ); } - return keys; + + const treeData = buildTreeYear(operations); + + return ( + + treeData={treeData} + showIcon + defaultExpandedKeys={[operations[0].id!]} + titleRender={(node: OpTreeNode): React.ReactNode => { + if (node.title) { + return <>{node.title}; + } + if (node.operation) { + const op = node.operation; + if (op.operationBackup) { + return <>{formatTime(op.parsedDate)} - Backup Operation; + } else if (op.operationIndexSnapshot) { + return <>{formatTime(op.parsedDate)} - Index Snapshot; + } + } + return no associated title, no associated operation; + }} + > + ); }; -const buildTreeYear = (operations: EOperation[]): DataNode[] => { +const buildTreeYear = (operations: EOperation[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { return op.parsedDate.getFullYear(); }); - const entries: DataNode[] = _.map(grouped, (value, key) => { + const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { key: "y" + key, title: "" + key, children: buildTreeMonth(value), }; }); + entries.sort(sortByKey); return entries; }; -const buildTreeMonth = (operations: EOperation[]): DataNode[] => { +const buildTreeMonth = (operations: EOperation[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return op.parsedDate.getMonth(); + return `y${op.parsedDate.getFullYear()}m${op.parsedDate.getMonth()}`; }); - const entries: DataNode[] = _.map(grouped, (value, key) => { + const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { - key: "m" + key, + key: key, title: value[0].parsedDate.toLocaleString("default", { month: "long", year: "numeric", @@ -55,13 +83,13 @@ const buildTreeMonth = (operations: EOperation[]): DataNode[] => { children: buildTreeDay(value), }; }); + entries.sort(sortByKey); return entries; }; -const buildTreeDay = (operations: EOperation[]): DataNode[] => { +const buildTreeDay = (operations: EOperation[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - console.log("Operation date: " + formatDate(op.parsedTime)); - return formatDate(op.parsedTime); + return `y${op.parsedDate.getFullYear()}m${op.parsedDate.getMonth()}d${op.parsedDate.getDate()}`; }); const entries = _.map(grouped, (value, key) => { @@ -71,15 +99,48 @@ const buildTreeDay = (operations: EOperation[]): DataNode[] => { children: buildTreeLeaf(value), }; }); + entries.sort(sortByKey); return entries; }; -const buildTreeLeaf = (operations: EOperation[]): DataNode[] => { - return _.map(operations, (op) => { +const buildTreeLeaf = (operations: EOperation[]): OpTreeNode[] => { + const entries = _.map(operations, (op) => { + let iconColor = "grey"; + let icon: React.ReactNode | null = ; + + switch (op.status) { + case OperationStatus.STATUS_SUCCESS: + iconColor = "green"; + break; + case OperationStatus.STATUS_ERROR: + iconColor = "red"; + break; + case OperationStatus.STATUS_INPROGRESS: + iconColor = "blue"; + break; + } + + if (op.status === OperationStatus.STATUS_ERROR) { + icon = ; + } else if (op.operationBackup) { + icon = ; + } + return { key: op.id!, - title: formatTime(op.parsedDate) + " - ", - isLeaf: true, + operation: op, + icon: icon, }; }); + entries.sort(sortByKey); + return entries; +}; + +const sortByKey = (a: OpTreeNode, b: OpTreeNode) => { + if (a.key < b.key) { + return 1; + } else if (a.key > b.key) { + return -1; + } + return 0; }; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 0005b315..46b2b699 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -12,7 +12,6 @@ import { API_PREFIX } from "../constants"; import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; export type EOperation = Operation & { - parsedId: number; parsedTime: number; parsedDate: Date; }; @@ -90,10 +89,10 @@ export const buildOperationListListener = ( (async () => { let opsFromServer = await getOperations(req); operations = opsFromServer.filter( - (o) => !operations.find((op) => op.parsedId === o.parsedId) + (o) => !operations.find((op) => op.id === o.id) ); operations.sort((a, b) => { - return a.parsedId - b.parsedId; + return a.parsedTime! - b.parsedTime!; }); callback(null, null, operations); @@ -115,7 +114,7 @@ export const buildOperationListListener = ( } else { operations.push(op); operations.sort((a, b) => { - return a.parsedId - b.parsedId; + return a.parsedTime! - b.parsedTime!; }); } } else if (type === OperationEventType.EVENT_CREATED) { @@ -133,7 +132,6 @@ export const toEop = (op: Operation): EOperation => { return { ...op, - parsedId: parseInt(op.id!), parsedTime: time, parsedDate: date, }; @@ -141,9 +139,109 @@ export const toEop = (op: Operation): EOperation => { // TODO: aggregate backup info from oplog. interface BackupInfo { - opids: string[]; + startTimeMs: number; + endTimeMs: number; repoId: string; planId: string; + snapshotId: string; backupLastStatus?: BackupProgressEntry; snapshotInfo?: ResticSnapshot; } + +// BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects. +// A backup info object aggregates the backup status (if available), snapshot info (if available), and possibly the forget status (if available). +export class BackupInfoCollector { + private listeners: ((event: OperationEventType, info: BackupInfo) => void)[] = + []; + private backupByOpId: { [key: string]: BackupInfo } = {}; + private backupBySnapshotId: { [key: string]: BackupInfo } = {}; + + private mergeBackups(existing: BackupInfo, newInfo: BackupInfo) { + existing.startTimeMs = Math.min(existing.startTimeMs, newInfo.startTimeMs); + existing.endTimeMs = Math.max(existing.endTimeMs, newInfo.endTimeMs); + if (newInfo.backupLastStatus) { + existing.backupLastStatus = newInfo.backupLastStatus; + } + if (newInfo.snapshotInfo) { + existing.snapshotInfo = newInfo.snapshotInfo; + } + return existing; + } + + private operationToBackup(op: Operation): BackupInfo { + const startTimeMs = parseInt(op.unixTimeStartMs!); + const endTimeMs = parseInt(op.unixTimeEndMs!); + const repoId = op.repoId!; + const planId = op.planId!; + const snapshotId = op.snapshotId!; + + const b: BackupInfo = { + startTimeMs, + endTimeMs, + repoId, + planId, + snapshotId, + }; + + 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) { + if (op.snapshotId) { + if (this.backupByOpId[op.id!]) { + delete this.backupByOpId[op.id!]; + } + + let newInfo = this.operationToBackup(op); + const existing = this.backupBySnapshotId[op.snapshotId!]; + if (existing) { + this.mergeBackups(existing, newInfo); + return existing; + } else { + this.backupBySnapshotId[op.snapshotId] = newInfo; + return newInfo; + } + } else { + const newInfo = this.operationToBackup(op); + this.backupByOpId[op.id!] = newInfo; + return newInfo; + } + } + + public addOperation(event: OperationEventType, op: Operation) { + const backupInfo = this.addHelper(op); + this.listeners.forEach((l) => l(event, backupInfo)); + } + + public getAll(): BackupInfo[] { + const arr = []; + arr.push(...Object.values(this.backupByOpId)); + arr.push(...Object.values(this.backupBySnapshotId)); + return arr; + } + + public subscribe( + listener: (event: OperationEventType, info: BackupInfo) => void + ) { + this.listeners.push(listener); + } + + public unsubscribe( + listener: (event: OperationEventType, info: BackupInfo) => void + ) { + const index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners[index] = this.listeners[this.listeners.length - 1]; + this.listeners.pop(); + } + } +} From 52cfd28d1d97b41111e8d4e1b467d529ad0e00f7 Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 25 Nov 2023 15:09:00 -0800 Subject: [PATCH 4/6] optree UI refinements and backend prep for operation list view by snapshot id --- gen/go/v1/operations.pb.go | 28 ++-- gen/go/v1/service.pb.go | 182 +++++++++++++------------ internal/api/server.go | 4 +- internal/orchestrator/tasks.go | 2 +- proto/v1/operations.proto | 1 + proto/v1/service.proto | 1 + webui/gen/ts/v1/operations.pb.ts | 1 + webui/gen/ts/v1/service.pb.ts | 1 + webui/package-lock.json | 106 ++++++++++++++ webui/package.json | 1 + webui/src/components/OperationTree.tsx | 175 +++++++++++++++++++----- webui/src/index.css | 3 - webui/src/index.html | 2 +- webui/src/index.sass | 8 ++ webui/src/state/oplog.ts | 87 +++++++----- webui/src/views/AddRepoModel.tsx | 3 +- webui/src/views/App.tsx | 11 +- webui/src/views/PlanView.tsx | 2 +- 18 files changed, 438 insertions(+), 180 deletions(-) delete mode 100644 webui/src/index.css create mode 100644 webui/src/index.sass diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index e3e0616a..e04ab7eb 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -78,6 +78,7 @@ const ( OperationStatus_STATUS_INPROGRESS OperationStatus = 2 OperationStatus_STATUS_SUCCESS OperationStatus = 3 OperationStatus_STATUS_ERROR OperationStatus = 4 + OperationStatus_STATUS_CANCELLED OperationStatus = 5 ) // Enum value maps for OperationStatus. @@ -88,6 +89,7 @@ var ( 2: "STATUS_INPROGRESS", 3: "STATUS_SUCCESS", 4: "STATUS_ERROR", + 5: "STATUS_CANCELLED", } OperationStatus_value = map[string]int32{ "STATUS_UNKNOWN": 0, @@ -95,6 +97,7 @@ var ( "STATUS_INPROGRESS": 2, "STATUS_SUCCESS": 3, "STATUS_ERROR": 4, + "STATUS_CANCELLED": 5, } ) @@ -533,18 +536,19 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, - 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x76, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, - 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, - 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, - 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, - 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x10, - 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, - 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, - 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, - 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0x8c, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, + 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, + 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, + 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, + 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, + 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x04, 0x12, 0x14, 0x0a, 0x10, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x41, 0x4e, 0x43, + 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, + 0x67, 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index a288305d..00311bdf 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -83,9 +83,10 @@ type GetOperationsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` - PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` - LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations } func (x *GetOperationsRequest) Reset() { @@ -134,6 +135,13 @@ func (x *GetOperationsRequest) GetPlanId() string { return "" } +func (x *GetOperationsRequest) GetSnapshotId() string { + if x != nil { + return x.SnapshotId + } + return "" +} + func (x *GetOperationsRequest) GetLastN() int64 { if x != nil { return x.LastN @@ -395,90 +403,92 @@ var file_v1_service_proto_rawDesc = []byte{ 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x5f, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, - 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, - 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, - 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, - 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, 0x73, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x75, - 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, 0x64, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x74, 0x69, - 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, 0x69, 0x6d, - 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, 0x32, 0x81, - 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x0a, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, 0x01, 0x2a, - 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, 0x0a, 0x07, - 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, - 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x1a, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, 0x65, 0x74, - 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, - 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, 0x0a, 0x0d, - 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x2e, - 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, - 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, - 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, - 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, + 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, + 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, + 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, + 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, + 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, + 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, + 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, + 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, + 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, + 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, + 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, + 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, + 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, + 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, + 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, + 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, + 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, + 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, + 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, + 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, + 0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, - 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x2f, 0x66, - 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x12, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x62, - 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, 0x75, 0x74, - 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x11, 0x2e, - 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, - 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x76, 0x31, - 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, 0x70, 0x61, - 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x72, 0x65, - 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, + 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, + 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, + 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, + 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, + 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, + 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, + 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, + 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, + 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, + 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/api/server.go b/internal/api/server.go index 0acfb58e..bb17d897 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -96,7 +96,7 @@ func (s *Server) AddRepo(ctx context.Context, repo *v1.Repo) (*v1.Config, error) return c, nil } -// ListSnapshots implements GET /v1/snapshots/{repo.id}/{plan.id?} +// ListSnapshots implements POST /v1/snapshots func (s *Server) ListSnapshots(ctx context.Context, query *v1.ListSnapshotsRequest) (*v1.ResticSnapshotList, error) { repo, err := s.orchestrator.GetRepo(query.RepoId) if err != nil { @@ -200,6 +200,8 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest ops, err = s.oplog.GetByPlan(req.PlanId, collector) } else if req.RepoId != "" { ops, err = s.oplog.GetByRepo(req.RepoId, collector) + } else if req.SnapshotId != "" { + ops, err = s.oplog.GetBySnapshotId(req.SnapshotId, collector) } else { ops, err = s.oplog.GetAll() } diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 119f00e5..eb574506 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -109,7 +109,7 @@ func backupHelper(ctx context.Context, orchestrator *Orchestrator, plan *v1.Plan lastSent := time.Now() // debounce progress updates, these can endup being very frequent. summary, err := repo.Backup(ctx, plan, func(entry *restic.BackupProgressEntry) { - if time.Since(lastSent) < 200*time.Millisecond { + if time.Since(lastSent) < 250*time.Millisecond { return } lastSent = time.Now() diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index bc9ac6dd..f6256b3d 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -52,6 +52,7 @@ enum OperationStatus { STATUS_INPROGRESS = 2; STATUS_SUCCESS = 3; STATUS_ERROR = 4; + STATUS_CANCELLED = 5; } message OperationBackup { diff --git a/proto/v1/service.proto b/proto/v1/service.proto index 180f3a1e..b2357db1 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -84,6 +84,7 @@ message ListSnapshotsRequest { message GetOperationsRequest { string repo_id = 1; string plan_id = 2; + string snapshot_id = 4; int64 last_n = 3; // limit to the last n operations } diff --git a/webui/gen/ts/v1/operations.pb.ts b/webui/gen/ts/v1/operations.pb.ts index 474b5cbf..c984d9ec 100644 --- a/webui/gen/ts/v1/operations.pb.ts +++ b/webui/gen/ts/v1/operations.pb.ts @@ -27,6 +27,7 @@ export enum OperationStatus { STATUS_INPROGRESS = "STATUS_INPROGRESS", STATUS_SUCCESS = "STATUS_SUCCESS", STATUS_ERROR = "STATUS_ERROR", + STATUS_CANCELLED = "STATUS_CANCELLED", } export type OperationList = { diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index 8dc10d96..83b887cb 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -18,6 +18,7 @@ export type ListSnapshotsRequest = { export type GetOperationsRequest = { repoId?: string planId?: string + snapshotId?: string lastN?: string } diff --git a/webui/package-lock.json b/webui/package-lock.json index 8531c804..ac797873 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -887,6 +887,17 @@ "react-refresh": "^0.9.0" } }, + "@parcel/transformer-sass": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-sass/-/transformer-sass-2.10.3.tgz", + "integrity": "sha512-Q8pTsMO+YuczBjmW8NdFcUjYpCdvY6EzYhPYw4eTyvldalGkaUVs1mJoKmWDHIDsIpdiaARxTpXP946B9cgE3w==", + "dev": true, + "requires": { + "@parcel/plugin": "2.10.3", + "@parcel/source-map": "^2.1.1", + "sass": "^1.38.0" + } + }, "@parcel/transformer-svg": { "version": "2.10.3", "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.10.3.tgz", @@ -1333,6 +1344,16 @@ "throttle-debounce": "^5.0.0" } }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1361,6 +1382,12 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, "boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1413,6 +1440,22 @@ "supports-color": "^7.1.0" } }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -1611,11 +1654,27 @@ "to-regex-range": "^5.0.1" } }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, "get-port": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==" }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", @@ -1660,6 +1719,12 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, + "immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -1680,6 +1745,15 @@ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1926,6 +2000,12 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==" }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, "nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -2470,6 +2550,15 @@ "util-deprecate": "^1.0.1" } }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, "recoil": { "version": "0.7.7", "resolved": "https://registry.npmjs.org/recoil/-/recoil-0.7.7.tgz", @@ -2498,6 +2587,17 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "sass": { + "version": "1.69.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", + "integrity": "sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -2527,6 +2627,12 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, "srcset": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", diff --git a/webui/package.json b/webui/package.json index 99cf9377..2440c524 100644 --- a/webui/package.json +++ b/webui/package.json @@ -27,6 +27,7 @@ "typescript": "^5.2.2" }, "devDependencies": { + "@parcel/transformer-sass": "^2.10.3", "events": "^3.3.0", "stream-browserify": "^3.0.0" } diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 03ceb097..9bdac8b5 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -1,26 +1,82 @@ -import React from "react"; -import { EOperation, getOperations } from "../state/oplog"; +import React, { useEffect, useState } from "react"; +import { + BackupInfo, + BackupInfoCollector, + EOperation, + getOperations, + subscribeToOperations, + toEop, + unsubscribeFromOperations, +} from "../state/oplog"; import { Tree } from "antd"; import _ from "lodash"; import { DataNode } from "antd/es/tree"; -import { formatDate, formatTime } from "../lib/formatting"; +import { + formatBytes, + formatDate, + formatDuration, + formatTime, + normalizeSnapshotId, +} from "../lib/formatting"; import { ExclamationOutlined, QuestionOutlined, SaveOutlined, } from "@ant-design/icons"; -import { OperationStatus } from "../../gen/ts/v1/operations.pb"; +import { + OperationEvent, + OperationEventType, + OperationStatus, +} from "../../gen/ts/v1/operations.pb"; +import { useAlertApi } from "./Alerts"; +import { MAX_OPERATION_HISTORY } from "../constants"; +import { OperationList } from "./OperationList"; type OpTreeNode = DataNode & { - operation?: EOperation; + backup?: BackupInfo; }; export const OperationTree = ({ - operations, -}: React.PropsWithoutRef<{ operations: EOperation[] }>) => { - operations = [...operations].reverse(); // reverse such that newest operations are at index 0. + planId, + repoId, +}: React.PropsWithoutRef<{ planId?: string; repoId?: string }>) => { + const alertApi = useAlertApi(); + const [backups, setBackups] = useState([]); - if (operations.length === 0) { + // track backups for this operation tree view. + useEffect(() => { + const backupCollector = new BackupInfoCollector(); + const lis = (opEvent: OperationEvent) => { + backupCollector.addOperation(opEvent.type!, opEvent.operation!); + }; + subscribeToOperations(lis); + + backupCollector.subscribe(() => { + const backups = backupCollector.getAll(); + backups.sort((a, b) => { + return b.startTimeMs - a.startTimeMs; + }); + setBackups(backups); + }); + + getOperations({ + planId, + repoId, + snapshotId, + lastN: "" + MAX_OPERATION_HISTORY, + }) + .then((ops) => { + backupCollector.bulkAddOperations(ops); + }) + .catch((e) => { + alertApi!.error("Failed to fetch operations: " + e.message); + }); + return () => { + unsubscribeFromOperations(lis); + }; + }, [planId, repoId]); + + if (backups.length === 0) { return (
No operations yet. @@ -28,34 +84,81 @@ export const OperationTree = ({ ); } - const treeData = buildTreeYear(operations); + const treeData = buildTreeYear(backups); return ( treeData={treeData} showIcon - defaultExpandedKeys={[operations[0].id!]} + defaultExpandedKeys={[backups[0].id!]} titleRender={(node: OpTreeNode): React.ReactNode => { if (node.title) { return <>{node.title}; } - if (node.operation) { - const op = node.operation; - if (op.operationBackup) { - return <>{formatTime(op.parsedDate)} - Backup Operation; - } else if (op.operationIndexSnapshot) { - return <>{formatTime(op.parsedDate)} - Index Snapshot; + if (node.backup) { + const b = node.backup; + const details: string[] = []; + + if (b.backupLastStatus) { + if (b.backupLastStatus.summary) { + const s = b.backupLastStatus.summary; + details.push( + `${formatBytes(s.totalBytesProcessed)} in ${formatDuration( + s.totalDuration! + )}` + ); + } else if (b.backupLastStatus.status) { + const s = b.backupLastStatus.status; + const bytesDone = parseInt(s.bytesDone!); + const bytesTotal = parseInt(s.totalBytes!); + const percent = Math.floor((bytesDone / bytesTotal) * 100); + details.push( + `${percent}% processed ${formatBytes( + bytesDone + )} / ${formatBytes(bytesTotal)}` + ); + } } + if (b.snapshotInfo) { + details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`); + } + + let detailsElem: React.ReactNode | null = null; + if (details.length > 0) { + detailsElem = ( + + [{details.join(", ")}] + + ); + } + + return ( + <> + Backup {formatTime(b.displayTime)} {detailsElem} + + ); } - return no associated title, no associated operation; + return ( + ERROR: this element should not appear, this is a bug. + ); }} > ); }; -const buildTreeYear = (operations: EOperation[]): OpTreeNode[] => { +const BackupInfoPanel = ({ + backup, +}: React.PropsWithoutRef<{ backup: BackupInfo }>) => { + return ( + <> + + + ); +}; + +const buildTreeYear = (operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return op.parsedDate.getFullYear(); + return op.displayTime.getFullYear(); }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { @@ -69,14 +172,14 @@ const buildTreeYear = (operations: EOperation[]): OpTreeNode[] => { return entries; }; -const buildTreeMonth = (operations: EOperation[]): OpTreeNode[] => { +const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return `y${op.parsedDate.getFullYear()}m${op.parsedDate.getMonth()}`; + return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}`; }); const entries: OpTreeNode[] = _.map(grouped, (value, key) => { return { key: key, - title: value[0].parsedDate.toLocaleString("default", { + title: value[0].displayTime.toLocaleString("default", { month: "long", year: "numeric", }), @@ -87,15 +190,15 @@ const buildTreeMonth = (operations: EOperation[]): OpTreeNode[] => { return entries; }; -const buildTreeDay = (operations: EOperation[]): OpTreeNode[] => { +const buildTreeDay = (operations: BackupInfo[]): OpTreeNode[] => { const grouped = _.groupBy(operations, (op) => { - return `y${op.parsedDate.getFullYear()}m${op.parsedDate.getMonth()}d${op.parsedDate.getDate()}`; + return `y${op.displayTime.getFullYear()}m${op.displayTime.getMonth()}d${op.displayTime.getDate()}`; }); const entries = _.map(grouped, (value, key) => { return { key: "d" + key, - title: formatDate(value[0].parsedTime), + title: formatDate(value[0].displayTime), children: buildTreeLeaf(value), }; }); @@ -103,12 +206,15 @@ const buildTreeDay = (operations: EOperation[]): OpTreeNode[] => { return entries; }; -const buildTreeLeaf = (operations: EOperation[]): OpTreeNode[] => { - const entries = _.map(operations, (op) => { +const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => { + const entries = _.map(operations, (b): OpTreeNode => { let iconColor = "grey"; let icon: React.ReactNode | null = ; - switch (op.status) { + switch (b.status) { + case OperationStatus.STATUS_PENDING: + iconColor = "grey"; + break; case OperationStatus.STATUS_SUCCESS: iconColor = "green"; break; @@ -118,17 +224,20 @@ const buildTreeLeaf = (operations: EOperation[]): OpTreeNode[] => { case OperationStatus.STATUS_INPROGRESS: iconColor = "blue"; break; + case OperationStatus.STATUS_CANCELLED: + iconColor = "orange"; + break; } - if (op.status === OperationStatus.STATUS_ERROR) { + if (b.status === OperationStatus.STATUS_ERROR) { icon = ; - } else if (op.operationBackup) { + } else { icon = ; } return { - key: op.id!, - operation: op, + key: b.id!, + backup: b, icon: icon, }; }); diff --git a/webui/src/index.css b/webui/src/index.css deleted file mode 100644 index 0e956968..00000000 --- a/webui/src/index.css +++ /dev/null @@ -1,3 +0,0 @@ -body { - margin: 0px; -} \ No newline at end of file diff --git a/webui/src/index.html b/webui/src/index.html index 59067bbe..dc64a937 100644 --- a/webui/src/index.html +++ b/webui/src/index.html @@ -2,7 +2,7 @@ ResticUI - +
diff --git a/webui/src/index.sass b/webui/src/index.sass new file mode 100644 index 00000000..9ad2e53a --- /dev/null +++ b/webui/src/index.sass @@ -0,0 +1,8 @@ +body + margin: 0px + +.resticui + &.backup-details + color: grey + font-family: monospace + font-size: 0.7em diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 46b2b699..7adfcf30 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -1,4 +1,3 @@ -import { atom } from "recoil"; import { Operation, OperationEvent, @@ -6,10 +5,9 @@ import { OperationStatus, } from "../../gen/ts/v1/operations.pb"; import { GetOperationsRequest, ResticUI } from "../../gen/ts/v1/service.pb"; -import { EventEmitter } from "events"; -import { useAlertApi } from "../components/Alerts"; import { API_PREFIX } from "../constants"; import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; +import _ from "lodash"; export type EOperation = Operation & { parsedTime: number; @@ -42,21 +40,12 @@ const subscribers: ((event: OperationEvent) => void)[] = []; } })(); -export const getOperations = async ({ - planId, - repoId, - lastN, -}: GetOperationsRequest): Promise => { - const opList = await ResticUI.GetOperations( - { - planId, - repoId, - lastN, - }, - { - pathPrefix: API_PREFIX, - } - ); +export const getOperations = async ( + req: GetOperationsRequest +): Promise => { + const opList = await ResticUI.GetOperations(req, { + pathPrefix: API_PREFIX, + }); return (opList.operations || []).map(toEop); }; @@ -137,13 +126,16 @@ export const toEop = (op: Operation): EOperation => { }; }; -// TODO: aggregate backup info from oplog. -interface BackupInfo { +export interface BackupInfo { + id: string; // id of the first operation that generated this backup. + displayTime: Date; startTimeMs: number; endTimeMs: number; - repoId: string; - planId: string; - snapshotId: string; + status: OperationStatus; + operations: Operation[]; + repoId?: string; + planId?: string; + snapshotId?: string; backupLastStatus?: BackupProgressEntry; snapshotInfo?: ResticSnapshot; } @@ -151,14 +143,24 @@ interface BackupInfo { // BackupInfoCollector maps multiple operations to single aggregate 'BackupInfo' objects. // A backup info object aggregates the backup status (if available), snapshot info (if available), and possibly the forget status (if available). export class BackupInfoCollector { - private listeners: ((event: OperationEventType, info: BackupInfo) => void)[] = - []; + private listeners: (( + event: OperationEventType, + info: BackupInfo[] + ) => void)[] = []; private backupByOpId: { [key: string]: BackupInfo } = {}; private backupBySnapshotId: { [key: string]: BackupInfo } = {}; private mergeBackups(existing: BackupInfo, newInfo: BackupInfo) { existing.startTimeMs = Math.min(existing.startTimeMs, newInfo.startTimeMs); existing.endTimeMs = Math.max(existing.endTimeMs, newInfo.endTimeMs); + existing.displayTime = new Date(existing.startTimeMs); + if (newInfo.startTimeMs >= existing.startTimeMs) { + existing.status = newInfo.status; // use the latest status + } + existing.operations = _.uniqBy( + [...existing.operations, ...newInfo.operations], + (o) => o.id! + ); if (newInfo.backupLastStatus) { existing.backupLastStatus = newInfo.backupLastStatus; } @@ -171,16 +173,17 @@ export class BackupInfoCollector { private operationToBackup(op: Operation): BackupInfo { const startTimeMs = parseInt(op.unixTimeStartMs!); const endTimeMs = parseInt(op.unixTimeEndMs!); - const repoId = op.repoId!; - const planId = op.planId!; - const snapshotId = op.snapshotId!; const b: BackupInfo = { + id: op.id!, startTimeMs, endTimeMs, - repoId, - planId, - snapshotId, + status: op.status!, + displayTime: new Date(startTimeMs), + repoId: op.repoId, + planId: op.planId, + snapshotId: op.snapshotId, + operations: [op], }; if (op.operationBackup) { @@ -191,15 +194,12 @@ export class BackupInfoCollector { const oi = op.operationIndexSnapshot; b.snapshotInfo = oi.snapshot; } - return b; } private addHelper(op: Operation) { if (op.snapshotId) { - if (this.backupByOpId[op.id!]) { - delete this.backupByOpId[op.id!]; - } + delete this.backupByOpId[op.id!]; let newInfo = this.operationToBackup(op); const existing = this.backupBySnapshotId[op.snapshotId!]; @@ -217,9 +217,22 @@ export class BackupInfoCollector { } } - public addOperation(event: OperationEventType, op: Operation) { + public addOperation(event: OperationEventType, op: Operation): BackupInfo { const backupInfo = this.addHelper(op); - this.listeners.forEach((l) => l(event, backupInfo)); + this.listeners.forEach((l) => l(event, [backupInfo])); + return backupInfo; + } + + public bulkAddOperations(ops: Operation[]): BackupInfo[] { + const backupInfos = ops.map((op) => this.addHelper(op)); + this.listeners.forEach((l) => + l(OperationEventType.EVENT_UNKNOWN, backupInfos) + ); + return backupInfos; + } + + public addOperationNoNotify(op: Operation) { + this.addHelper(op); } public getAll(): BackupInfo[] { diff --git a/webui/src/views/AddRepoModel.tsx b/webui/src/views/AddRepoModel.tsx index 25e7e955..e62d7b83 100644 --- a/webui/src/views/AddRepoModel.tsx +++ b/webui/src/views/AddRepoModel.tsx @@ -106,7 +106,8 @@ export const AddRepoModel = ({ showModal(null); alertsApi.success("Updated repo " + repo.uri); - // Update the snapshots for the repo + // Update the snapshots for the repo to confirm the config works. + // TODO: this operation is only used here, find a different RPC for this purpose. await ResticUI.ListSnapshots( { repoId: repo.id, diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx index 61d6062a..d7e2bb2e 100644 --- a/webui/src/views/App.tsx +++ b/webui/src/views/App.tsx @@ -25,7 +25,7 @@ import { toEop, unsubscribeFromOperations, } from "../state/oplog"; -import { formatTime } from "../lib/formatting"; +import { formatTime, normalizeSnapshotId } from "../lib/formatting"; import { SnapshotBrowser } from "../components/SnapshotBrowser"; import { OperationRow } from "../components/OperationList"; import { @@ -58,7 +58,8 @@ export const App: React.FC = () => { .catch((err) => { alertApi.error(err.message, 0); alertApi.error( - "Failed to fetch initial config, typically this means the UI could not connect to the backend" + "Failed to fetch initial config, typically this means the UI could not connect to the backend", + 0 ); }); }, []); @@ -173,6 +174,7 @@ const OperationNotificationGenerator = () => { const config = useRecoilValue(configState); useEffect(() => { + // TODO: factor notification generator into a separate file. const listener = (event: OperationEvent) => { if (event.type != OperationEventType.EVENT_CREATED) return; const planId = event.operation!.planId!; @@ -195,8 +197,9 @@ const OperationNotificationGenerator = () => { } else if (event.operation?.operationIndexSnapshot) { const indexOp = event.operation.operationIndexSnapshot; alertApi.info({ - content: `Indexed snapshot ${indexOp.snapshot! - .id!} for plan ${planId}.`, + content: `Indexed snapshot ${normalizeSnapshotId( + indexOp.snapshot!.id! + )} for plan ${planId}.`, onClick: onClick, }); } diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index f4d1c7e5..9a3c4d58 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -85,7 +85,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { items={[ { key: "1", - label: "Condensed View", + label: "Tree View", children: ( <> From b5d77be32d5266cb5706f37999a05fc6200c51bd Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 25 Nov 2023 17:27:28 -0800 Subject: [PATCH 5/6] implement tree view --- gen/go/v1/service.pb.go | 175 ++++++++++++------------ internal/api/server.go | 9 ++ internal/orchestrator/repo.go | 1 + proto/v1/service.proto | 1 + webui/gen/ts/v1/service.pb.ts | 1 + webui/src/components/OperationList.tsx | 58 ++++---- webui/src/components/OperationTree.tsx | 181 ++++++++++++++----------- webui/src/state/oplog.ts | 10 +- webui/src/views/PlanView.tsx | 25 +--- 9 files changed, 254 insertions(+), 207 deletions(-) diff --git a/gen/go/v1/service.pb.go b/gen/go/v1/service.pb.go index 00311bdf..4a612e1e 100644 --- a/gen/go/v1/service.pb.go +++ b/gen/go/v1/service.pb.go @@ -83,10 +83,11 @@ type GetOperationsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` - PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` - SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` - LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations + RepoId string `protobuf:"bytes,1,opt,name=repo_id,json=repoId,proto3" json:"repo_id,omitempty"` + PlanId string `protobuf:"bytes,2,opt,name=plan_id,json=planId,proto3" json:"plan_id,omitempty"` + SnapshotId string `protobuf:"bytes,4,opt,name=snapshot_id,json=snapshotId,proto3" json:"snapshot_id,omitempty"` + Ids []int64 `protobuf:"varint,5,rep,packed,name=ids,proto3" json:"ids,omitempty"` + LastN int64 `protobuf:"varint,3,opt,name=last_n,json=lastN,proto3" json:"last_n,omitempty"` // limit to the last n operations } func (x *GetOperationsRequest) Reset() { @@ -142,6 +143,13 @@ func (x *GetOperationsRequest) GetSnapshotId() string { return "" } +func (x *GetOperationsRequest) GetIds() []int64 { + if x != nil { + return x.Ids + } + return nil +} + func (x *GetOperationsRequest) GetLastN() int64 { if x != nil { return x.LastN @@ -403,92 +411,93 @@ var file_v1_service_proto_rawDesc = []byte{ 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, - 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x80, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, + 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6c, 0x61, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, - 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, - 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, 0x1f, 0x0a, - 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, - 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, - 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, 0x07, 0x4c, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, - 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, - 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, 0x6d, 0x65, - 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, 0x43, 0x0a, - 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x12, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x3a, - 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3b, - 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, - 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, 0x31, 0x2f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, 0x12, 0x47, - 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x1d, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, - 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, 0x12, 0x57, - 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, 0x2e, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x65, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x53, - 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x53, 0x6e, - 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, 0xe4, 0x93, - 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, - 0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, - 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x69, 0x64, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x03, 0x52, 0x03, + 0x69, 0x64, 0x73, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x61, 0x73, 0x74, 0x4e, 0x22, 0x68, 0x0a, 0x18, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x70, 0x6f, 0x49, 0x64, 0x12, + 0x1f, 0x0a, 0x0b, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x49, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x22, 0x56, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, + 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x25, 0x0a, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x07, 0x65, 0x6e, 0x74, 0x72, 0x69, 0x65, 0x73, 0x22, 0xd3, 0x01, 0x0a, + 0x07, 0x4c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x70, 0x61, 0x74, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x69, 0x64, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x03, 0x67, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6d, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x63, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x74, 0x69, + 0x6d, 0x65, 0x32, 0x81, 0x06, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, 0x55, 0x49, 0x12, + 0x43, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c, 0x12, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3a, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x0a, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, + 0x0f, 0x3a, 0x01, 0x2a, 0x22, 0x0a, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x3b, 0x0a, 0x07, 0x41, 0x64, 0x64, 0x52, 0x65, 0x70, 0x6f, 0x12, 0x08, 0x2e, 0x76, 0x31, + 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x1a, 0x0a, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f, 0x76, + 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x72, 0x65, 0x70, 0x6f, 0x12, 0x61, 0x0a, + 0x12, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x12, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x22, + 0x1d, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x76, 0x65, + 0x6e, 0x74, 0x73, 0x2f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x30, 0x01, + 0x12, 0x57, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x76, 0x31, + 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x19, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x6f, + 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5b, 0x0a, 0x0d, 0x4c, 0x69, 0x73, + 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x18, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x69, 0x63, + 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x22, 0x18, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x12, 0x3a, 0x01, 0x2a, 0x22, 0x0d, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, 0x12, 0x70, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x1c, 0x2e, 0x76, 0x31, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, - 0x74, 0x53, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, - 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x73, - 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, 0x75, 0x70, - 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, 0x82, 0xd3, - 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6d, 0x64, - 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, 0x68, 0x41, - 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, - 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c, 0x69, - 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, - 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2f, - 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, - 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, + 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, + 0x74, 0x73, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x4f, 0x0a, 0x06, 0x42, 0x61, 0x63, 0x6b, + 0x75, 0x70, 0x12, 0x12, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, + 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x19, + 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x22, 0x0e, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x6d, 0x64, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x75, 0x70, 0x12, 0x5b, 0x0a, 0x10, 0x50, 0x61, 0x74, + 0x68, 0x41, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x12, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x1a, 0x11, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x4c, 0x69, 0x73, 0x74, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, + 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, 0x67, + 0x65, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x69, 0x63, 0x75, 0x69, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/api/server.go b/internal/api/server.go index bb17d897..87eac8fe 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -202,6 +202,15 @@ func (s *Server) GetOperations(ctx context.Context, req *v1.GetOperationsRequest ops, err = s.oplog.GetByRepo(req.RepoId, collector) } else if req.SnapshotId != "" { ops, err = s.oplog.GetBySnapshotId(req.SnapshotId, collector) + } else if len(req.Ids) > 0 { + ops = make([]*v1.Operation, 0, len(req.Ids)) + for i, id := range req.Ids { + op, err := s.oplog.Get(id) + if err != nil { + return nil, fmt.Errorf("failed to get operation %d: %w", i, err) + } + ops = append(ops, op) + } } else { ops, err = s.oplog.GetAll() } diff --git a/internal/orchestrator/repo.go b/internal/orchestrator/repo.go index 968d756b..cb62f7a0 100644 --- a/internal/orchestrator/repo.go +++ b/internal/orchestrator/repo.go @@ -20,6 +20,7 @@ type RepoOrchestrator struct { repo *restic.Repo } +// newRepoOrchestrator accepts a config and a repo that is configured with the properties of that config object. func newRepoOrchestrator(repoConfig *v1.Repo, repo *restic.Repo) *RepoOrchestrator { return &RepoOrchestrator{ repoConfig: repoConfig, diff --git a/proto/v1/service.proto b/proto/v1/service.proto index b2357db1..8fdf39be 100644 --- a/proto/v1/service.proto +++ b/proto/v1/service.proto @@ -85,6 +85,7 @@ message GetOperationsRequest { string repo_id = 1; string plan_id = 2; string snapshot_id = 4; + repeated int64 ids = 5; int64 last_n = 3; // limit to the last n operations } diff --git a/webui/gen/ts/v1/service.pb.ts b/webui/gen/ts/v1/service.pb.ts index 83b887cb..6fb4ba4b 100644 --- a/webui/gen/ts/v1/service.pb.ts +++ b/webui/gen/ts/v1/service.pb.ts @@ -19,6 +19,7 @@ export type GetOperationsRequest = { repoId?: string planId?: string snapshotId?: string + ids?: string[] lastN?: string } diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 3fc863e7..078cd2fd 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { Operation, OperationStatus } from "../../gen/ts/v1/operations.pb"; import { Card, @@ -8,6 +8,7 @@ import { List, Progress, Row, + Spin, Typography, } from "antd"; import { @@ -16,7 +17,12 @@ import { SaveOutlined, } from "@ant-design/icons"; import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic.pb"; -import { EOperation } from "../state/oplog"; +import { + EOperation, + buildOperationListListener, + subscribeToOperations, + unsubscribeFromOperations, +} from "../state/oplog"; import { SnapshotBrowser } from "./SnapshotBrowser"; import { formatBytes, @@ -24,11 +30,24 @@ import { formatTime, normalizeSnapshotId, } from "../lib/formatting"; +import _ from "lodash"; +import { GetOperationsRequest } from "../../gen/ts/v1/service.pb"; export const OperationList = ({ - operations, -}: React.PropsWithoutRef<{ operations: EOperation[] }>) => { - operations = [...operations].reverse(); + req, +}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => { + const [operations, setOperations] = useState([]); + + useEffect(() => { + const lis = buildOperationListListener(req, (event, operation, opList) => { + setOperations(opList); + }); + subscribeToOperations(lis); + + return () => { + unsubscribeFromOperations(lis); + }; + }, [JSON.stringify(req)]); if (operations.length === 0) { return ( @@ -39,27 +58,20 @@ export const OperationList = ({ ); } - const groupBy = (ops: EOperation[], keyFunc: (op: EOperation) => string) => { - const groups: { [key: string]: EOperation[] } = {}; - - ops.forEach((op) => { - const key = keyFunc(op); - if (!(key in groups)) { - groups[key] = []; - } - groups[key].push(op); - }); - - return Object.values(groups); - }; - // groups items by snapshotID if one can be identified, otherwise by operation ID. - const groupedItems = groupBy(operations, (op: EOperation) => { - return getSnapshotId(op) || op.id!; - }); + const groupedItems = _.values( + _.groupBy(operations, (op: EOperation) => { + return getSnapshotId(op) || op.id!; + }) + ); groupedItems.sort((a, b) => { return b[0].parsedTime - a[0].parsedTime; }); + groupedItems.forEach((group) => { + group.sort((a, b) => { + return b.parsedTime - a.parsedTime; + }); + }); return ( 50 ? { position: "both", align: "center", defaultPageSize: 50 } - : {} + : undefined } /> ); diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx index 9bdac8b5..40ef3e39 100644 --- a/webui/src/components/OperationTree.tsx +++ b/webui/src/components/OperationTree.tsx @@ -8,7 +8,7 @@ import { toEop, unsubscribeFromOperations, } from "../state/oplog"; -import { Tree } from "antd"; +import { Col, Empty, Row, Tree } from "antd"; import _ from "lodash"; import { DataNode } from "antd/es/tree"; import { @@ -23,30 +23,34 @@ import { QuestionOutlined, SaveOutlined, } from "@ant-design/icons"; -import { - OperationEvent, - OperationEventType, - OperationStatus, -} from "../../gen/ts/v1/operations.pb"; +import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations.pb"; import { useAlertApi } from "./Alerts"; -import { MAX_OPERATION_HISTORY } from "../constants"; import { OperationList } from "./OperationList"; +import { GetOperationsRequest } from "../../gen/ts/v1/service.pb"; type OpTreeNode = DataNode & { backup?: BackupInfo; }; export const OperationTree = ({ - planId, - repoId, -}: React.PropsWithoutRef<{ planId?: string; repoId?: string }>) => { + req, +}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => { + console.log("Loading operation tree with req: ", req); const alertApi = useAlertApi(); const [backups, setBackups] = useState([]); + const [selectedBackupId, setSelectedBackupId] = useState(null); // track backups for this operation tree view. useEffect(() => { + setSelectedBackupId(null); const backupCollector = new BackupInfoCollector(); const lis = (opEvent: OperationEvent) => { + if (!!req.planId && opEvent.operation!.planId !== req.planId) { + return; + } + if (!!req.repoId && opEvent.operation!.repoId !== req.repoId) { + return; + } backupCollector.addOperation(opEvent.type!, opEvent.operation!); }; subscribeToOperations(lis); @@ -59,12 +63,7 @@ export const OperationTree = ({ setBackups(backups); }); - getOperations({ - planId, - repoId, - snapshotId, - lastN: "" + MAX_OPERATION_HISTORY, - }) + getOperations(req) .then((ops) => { backupCollector.bulkAddOperations(ops); }) @@ -74,7 +73,7 @@ export const OperationTree = ({ return () => { unsubscribeFromOperations(lis); }; - }, [planId, repoId]); + }, [req]); if (backups.length === 0) { return ( @@ -86,73 +85,94 @@ export const OperationTree = ({ const treeData = buildTreeYear(backups); - return ( - - treeData={treeData} - showIcon - defaultExpandedKeys={[backups[0].id!]} - titleRender={(node: OpTreeNode): React.ReactNode => { - if (node.title) { - return <>{node.title}; - } - if (node.backup) { - const b = node.backup; - const details: string[] = []; + let oplist: React.ReactNode | null = null; + if (selectedBackupId) { + const backup = backups.find((b) => b.id === selectedBackupId); + if (!backup) { + oplist = ; + } else { + const req: GetOperationsRequest = { + ids: backup.operations.map((op) => op.id!), + }; + oplist = ( + <> +

Backup at {formatTime(backup.displayTime)}

+ + + ); + } + } - if (b.backupLastStatus) { - if (b.backupLastStatus.summary) { - const s = b.backupLastStatus.summary; - details.push( - `${formatBytes(s.totalBytesProcessed)} in ${formatDuration( - s.totalDuration! - )}` - ); - } else if (b.backupLastStatus.status) { - const s = b.backupLastStatus.status; - const bytesDone = parseInt(s.bytesDone!); - const bytesTotal = parseInt(s.totalBytes!); - const percent = Math.floor((bytesDone / bytesTotal) * 100); - details.push( - `${percent}% processed ${formatBytes( - bytesDone - )} / ${formatBytes(bytesTotal)}` + return ( + + +

Browse Backups

+ + treeData={treeData} + showIcon + defaultExpandedKeys={[backups[0].id!]} + onSelect={(keys, info) => { + setSelectedBackupId( + info.selectedNodes.length > 0 + ? info.selectedNodes[0].backup!.id! + : null + ); + }} + titleRender={(node: OpTreeNode): React.ReactNode => { + if (node.title) { + return <>{node.title}; + } + if (node.backup) { + const b = node.backup; + const details: string[] = []; + + if (b.backupLastStatus) { + if (b.backupLastStatus.summary) { + const s = b.backupLastStatus.summary; + details.push( + `${formatBytes(s.totalBytesProcessed)} in ${formatDuration( + s.totalDuration! + )}` + ); + } else if (b.backupLastStatus.status) { + const s = b.backupLastStatus.status; + const bytesDone = parseInt(s.bytesDone!); + const bytesTotal = parseInt(s.totalBytes!); + const percent = Math.floor((bytesDone / bytesTotal) * 100); + details.push( + `${percent}% processed ${formatBytes( + bytesDone + )} / ${formatBytes(bytesTotal)}` + ); + } + } + if (b.snapshotInfo) { + details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`); + } + + let detailsElem: React.ReactNode | null = null; + if (details.length > 0) { + detailsElem = ( + + [{details.join(", ")}] + + ); + } + + return ( + <> + Backup {formatTime(b.displayTime)} {detailsElem} + ); } - } - if (b.snapshotInfo) { - details.push(`ID: ${normalizeSnapshotId(b.snapshotInfo.id!)}`); - } - - let detailsElem: React.ReactNode | null = null; - if (details.length > 0) { - detailsElem = ( - - [{details.join(", ")}] - + return ( + ERROR: this element should not appear, this is a bug. ); - } - - return ( - <> - Backup {formatTime(b.displayTime)} {detailsElem} - - ); - } - return ( - ERROR: this element should not appear, this is a bug. - ); - }} - > - ); -}; - -const BackupInfoPanel = ({ - backup, -}: React.PropsWithoutRef<{ backup: BackupInfo }>) => { - return ( - <> - - + }} + > + + {oplist} +
); }; @@ -181,7 +201,6 @@ const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => { key: key, title: value[0].displayTime.toLocaleString("default", { month: "long", - year: "numeric", }), children: buildTreeDay(value), }; diff --git a/webui/src/state/oplog.ts b/webui/src/state/oplog.ts index 7adfcf30..f46afe2b 100644 --- a/webui/src/state/oplog.ts +++ b/webui/src/state/oplog.ts @@ -96,6 +96,12 @@ export const buildOperationListListener = ( if (!!req.repoId && op.repoId !== req.repoId) { return; } + if (req.snapshotId && op.snapshotId !== req.snapshotId) { + return; + } + if (req.ids && !req.ids.includes(op.id!)) { + return; + } if (type === OperationEventType.EVENT_UPDATED) { const index = operations.findIndex((o) => o.id === op.id); if (index > -1) { @@ -243,13 +249,13 @@ export class BackupInfoCollector { } public subscribe( - listener: (event: OperationEventType, info: BackupInfo) => void + listener: (event: OperationEventType, info: BackupInfo[]) => void ) { this.listeners.push(listener); } public unsubscribe( - listener: (event: OperationEventType, info: BackupInfo) => void + listener: (event: OperationEventType, info: BackupInfo[]) => void ) { const index = this.listeners.indexOf(listener); if (index > -1) { diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index 9a3c4d58..899f84e7 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -21,21 +21,6 @@ import { MAX_OPERATION_HISTORY } from "../constants"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { const showModal = useShowModal(); const alertsApi = useAlertApi()!; - const [operations, setOperations] = useState([]); - - useEffect(() => { - const listener = buildOperationListListener( - { planId: plan.id, lastN: "" + MAX_OPERATION_HISTORY }, - (event, changedOp, operations) => { - setOperations([...operations]); - } - ); - subscribeToOperations(listener); - - return () => { - unsubscribeFromOperations(listener); - }; - }, [plan.id]); // Gracefully handle deletions by checking if the plan is still in the config. const config = useRecoilValue(configState); @@ -88,7 +73,9 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { label: "Tree View", children: ( <> - + ), }, @@ -97,8 +84,10 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { label: "Operation List", children: ( <> -

Backup Action History ({operations.length} loaded)

- +

Backup Action History

+ ), }, From c00b96eaeea39dd224f374f39ef595fd4be5ca0f Mon Sep 17 00:00:00 2001 From: Gareth George Date: Sat, 25 Nov 2023 17:33:57 -0800 Subject: [PATCH 6/6] fix: repo orchestrator tests --- internal/orchestrator/repo.go | 5 +++++ internal/orchestrator/repo_test.go | 36 +++++++++++++++--------------- pkg/restic/restic.go | 1 - 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/internal/orchestrator/repo.go b/internal/orchestrator/repo.go index cb62f7a0..77d7e2bf 100644 --- a/internal/orchestrator/repo.go +++ b/internal/orchestrator/repo.go @@ -48,6 +48,11 @@ func (r *RepoOrchestrator) SnapshotsForPlan(ctx context.Context, plan *v1.Plan) func (r *RepoOrchestrator) Backup(ctx context.Context, plan *v1.Plan, progressCallback func(event *restic.BackupProgressEntry)) (*restic.BackupProgressEntry, error) { zap.L().Debug("repo orchestrator starting backup", zap.String("repo", r.repoConfig.Id)) + + if err := r.repo.Init(ctx); err != nil { + return nil, fmt.Errorf("failed to init repo: %w", err) + } + snapshots, err := r.SnapshotsForPlan(ctx, plan) if err != nil { return nil, fmt.Errorf("failed to get snapshots for plan: %w", err) diff --git a/internal/orchestrator/repo_test.go b/internal/orchestrator/repo_test.go index b348191e..6994beea 100644 --- a/internal/orchestrator/repo_test.go +++ b/internal/orchestrator/repo_test.go @@ -18,15 +18,15 @@ func TestBackup(t *testing.T) { // create a new repo with cache disabled for testing r := &v1.Repo{ - Id: "test", - Uri: repo, + Id: "test", + Uri: repo, Password: "test", - Flags: []string{"--no-cache"}, + Flags: []string{"--no-cache"}, } - + plan := &v1.Plan{ - Id: "test", - Repo: "test", + Id: "test", + Repo: "test", Paths: []string{testData}, } @@ -48,34 +48,34 @@ func TestBackup(t *testing.T) { func TestSnapshotParenting(t *testing.T) { t.Parallel() - + repo := t.TempDir() testData := test.CreateTestData(t) // create a new repo with cache disabled for testing r := &v1.Repo{ - Id: "test", - Uri: repo, + Id: "test", + Uri: repo, Password: "test", - Flags: []string{"--no-cache"}, + Flags: []string{"--no-cache"}, } plans := []*v1.Plan{ { - Id: "test", - Repo: "test", + Id: "test", + Repo: "test", Paths: []string{testData}, }, { - Id: "test2", - Repo: "test", + Id: "test2", + Repo: "test", Paths: []string{testData}, }, } orchestrator := newRepoOrchestrator(r, restic.NewRepo(r, restic.WithFlags("--no-cache"))) - for i := 0; i < 4; i ++{ + for i := 0; i < 4; i++ { for _, plan := range plans { summary, err := orchestrator.Backup(context.Background(), plan, nil) if err != nil { @@ -92,7 +92,7 @@ func TestSnapshotParenting(t *testing.T) { } } } - + for _, plan := range plans { snapshots, err := orchestrator.SnapshotsForPlan(context.Background(), plan) if err != nil { @@ -105,7 +105,7 @@ func TestSnapshotParenting(t *testing.T) { } for i := 1; i < len(snapshots); i++ { - prev := snapshots[i - 1] + prev := snapshots[i-1] curr := snapshots[i] if prev.ToProto().UnixTimeMs >= curr.ToProto().UnixTimeMs { @@ -129,4 +129,4 @@ func TestSnapshotParenting(t *testing.T) { if len(snapshots) != 8 { t.Errorf("expected 8 snapshots, got %d", len(snapshots)) } -} \ No newline at end of file +} diff --git a/pkg/restic/restic.go b/pkg/restic/restic.go index 9978fa9d..5b73e7cb 100644 --- a/pkg/restic/restic.go +++ b/pkg/restic/restic.go @@ -77,7 +77,6 @@ func (r *Repo) init(ctx context.Context) error { func (r *Repo) Init(ctx context.Context) error { r.mu.Lock() defer r.mu.Unlock() - r.initialized = false return r.init(ctx) }