mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-24 17:05:45 +00:00
feat: add repo view
This commit is contained in:
@@ -40,7 +40,6 @@ import {
|
||||
import { SnapshotBrowser } from "./SnapshotBrowser";
|
||||
import {
|
||||
formatBytes,
|
||||
formatDuration,
|
||||
formatTime,
|
||||
normalizeSnapshotId,
|
||||
} from "../lib/formatting";
|
||||
@@ -48,14 +47,15 @@ import _ from "lodash";
|
||||
import { GetOperationsRequest, Restora } from "../../gen/ts/v1/service.pb";
|
||||
import { useAlertApi } from "./Alerts";
|
||||
import { MessageInstance } from "antd/es/message/interface";
|
||||
import Operation from "antd/es/transfer/operation";
|
||||
|
||||
export const OperationList = ({
|
||||
req,
|
||||
useBackups,
|
||||
showPlan,
|
||||
}: React.PropsWithoutRef<{
|
||||
req?: GetOperationsRequest;
|
||||
useBackups?: BackupInfo[];
|
||||
showPlan?: boolean,
|
||||
}>) => {
|
||||
const alertApi = useAlertApi();
|
||||
|
||||
@@ -128,17 +128,13 @@ export const OperationList = ({
|
||||
dataSource={backups}
|
||||
renderItem={(backup) => {
|
||||
const ops = backup.operations;
|
||||
if (ops.length === 1) {
|
||||
return <OperationRow alertApi={alertApi!} key={ops[0].id!} operation={toEop(ops[0])} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card size="small" style={{ margin: "5px" }}>
|
||||
{ops.map((op) => {
|
||||
if (shouldHideStatus(op.status!)) {
|
||||
return null;
|
||||
}
|
||||
return <OperationRow alertApi={alertApi!} key={op.id!} operation={toEop(op)} />
|
||||
return <OperationRow alertApi={alertApi!} key={op.id!} operation={toEop(op)} showPlan={showPlan || false} />
|
||||
})}
|
||||
</Card>
|
||||
);
|
||||
@@ -155,7 +151,8 @@ export const OperationList = ({
|
||||
export const OperationRow = ({
|
||||
operation,
|
||||
alertApi,
|
||||
}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance }>) => {
|
||||
showPlan,
|
||||
}: React.PropsWithoutRef<{ operation: EOperation, alertApi?: MessageInstance, showPlan: boolean }>) => {
|
||||
const details = detailsForOperation(operation);
|
||||
const displayType = getTypeForDisplay(operation);
|
||||
let avatar: React.ReactNode;
|
||||
@@ -190,7 +187,7 @@ export const OperationRow = ({
|
||||
const opName = displayTypeToString(getTypeForDisplay(operation));
|
||||
let title = (
|
||||
<>
|
||||
{formatTime(operation.unixTimeStartMs!)} - {opName}{" "}
|
||||
{showPlan ? operation.planId + " - " : undefined} {formatTime(operation.unixTimeStartMs!)} - {opName}{" "}
|
||||
<span className="restora operation-details">{details.displayState}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -201,65 +201,64 @@ export const OperationTree = ({
|
||||
|
||||
const buildTreePlan = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return op.planId;
|
||||
return op.operations[0].planId!;
|
||||
});
|
||||
|
||||
const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: "p" + key,
|
||||
title: "" + key,
|
||||
children: buildTreeYear(value),
|
||||
key: key,
|
||||
title: key,
|
||||
children: buildTreeDay(key, value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
|
||||
if (entries.length === 1) {
|
||||
return entries[0].children!;
|
||||
}
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeYear = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const buildTreeYear = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return localISOTime(op.displayTime).substring(0, 4);
|
||||
});
|
||||
|
||||
const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: "y" + key,
|
||||
key: keyPrefix + key,
|
||||
title: "" + key,
|
||||
children: buildTreeMonth(value),
|
||||
children: buildTreeDay(keyPrefix, value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeMonth = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const buildTreeMonth = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return localISOTime(op.displayTime).substring(0, 7);
|
||||
});
|
||||
const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: key,
|
||||
key: keyPrefix + key,
|
||||
title: value[0].displayTime.toLocaleString("default", {
|
||||
month: "long",
|
||||
}),
|
||||
children: buildTreeDay(value),
|
||||
children: buildTreeDay(keyPrefix, value),
|
||||
};
|
||||
});
|
||||
entries.sort(sortByKey);
|
||||
return entries;
|
||||
};
|
||||
|
||||
const buildTreeDay = (operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const buildTreeDay = (keyPrefix: string, operations: BackupInfo[]): OpTreeNode[] => {
|
||||
const grouped = _.groupBy(operations, (op) => {
|
||||
return localISOTime(op.displayTime).substring(0, 10);
|
||||
});
|
||||
|
||||
const entries = _.map(grouped, (value, key) => {
|
||||
return {
|
||||
key: "d" + key,
|
||||
key: keyPrefix + key,
|
||||
title: formatDate(value[0].displayTime),
|
||||
children: buildTreeLeaf(value),
|
||||
};
|
||||
|
||||
@@ -163,37 +163,11 @@ export class BackupInfoCollector {
|
||||
backupLastStatus,
|
||||
snapshotInfo,
|
||||
forgotten,
|
||||
planId: operations[0].planId,
|
||||
repoId: operations[0].repoId,
|
||||
};
|
||||
}
|
||||
|
||||
private operationToBackup(op: Operation): BackupInfo {
|
||||
const startTimeMs = parseInt(op.unixTimeStartMs!);
|
||||
const endTimeMs = parseInt(op.unixTimeEndMs!);
|
||||
|
||||
const b: BackupInfo = {
|
||||
id: op.id!,
|
||||
startTimeMs,
|
||||
endTimeMs,
|
||||
status: op.status!,
|
||||
displayTime: new Date(startTimeMs),
|
||||
displayType: getTypeForDisplay(op),
|
||||
repoId: op.repoId,
|
||||
planId: op.planId,
|
||||
snapshotId: op.snapshotId,
|
||||
operations: [op],
|
||||
};
|
||||
|
||||
if (op.operationBackup) {
|
||||
const ob = op.operationBackup;
|
||||
b.backupLastStatus = ob.lastStatus;
|
||||
}
|
||||
if (op.operationIndexSnapshot) {
|
||||
const oi = op.operationIndexSnapshot;
|
||||
b.snapshotInfo = oi.snapshot;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private addHelper(op: Operation): BackupInfo {
|
||||
if (op.snapshotId) {
|
||||
delete this.backupByOpId[op.id!];
|
||||
|
||||
+21
-4
@@ -146,7 +146,6 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
|
||||
label: "Add Repo",
|
||||
onClick: async () => {
|
||||
const { AddRepoModal } = await import("./AddRepoModal");
|
||||
|
||||
showModal(<AddRepoModal template={null} />);
|
||||
},
|
||||
},
|
||||
@@ -154,11 +153,29 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
|
||||
return {
|
||||
key: "r-" + repo.id,
|
||||
icon: <CheckCircleOutlined style={{ color: "green" }} />,
|
||||
label: repo.id,
|
||||
label: (
|
||||
<div className="restora visible-on-hover">
|
||||
{repo.id}{" "}
|
||||
<Button
|
||||
className="hidden-child"
|
||||
type="text"
|
||||
size="small"
|
||||
shape="circle"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={async () => {
|
||||
const { AddRepoModal } = await import("./AddRepoModal");
|
||||
showModal(<AddRepoModal template={repo} />);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
onClick: async () => {
|
||||
const { AddRepoModal } = await import("./AddRepoModal");
|
||||
const { RepoView } = await import("./RepoView");
|
||||
|
||||
showModal(<AddRepoModal template={repo} />);
|
||||
setContent(<RepoView repo={repo} />, [
|
||||
{ title: "Repos" },
|
||||
{ title: repo.id || "" },
|
||||
]);
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Plan } from "../../gen/ts/v1/config.pb";
|
||||
import { Button, Flex, Tabs, Tooltip, Typography } from "antd";
|
||||
import { useShowModal } from "../components/ModalManager";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { configState } from "../state/config";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { Restora } from "../../gen/ts/v1/service.pb";
|
||||
import {
|
||||
EOperation,
|
||||
subscribeToOperations,
|
||||
unsubscribeFromOperations,
|
||||
} from "../state/oplog";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY } from "../constants";
|
||||
|
||||
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
const showModal = useShowModal();
|
||||
const alertsApi = useAlertApi()!;
|
||||
|
||||
// Gracefully handle deletions by checking if the plan is still in the config.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Repo } from "../../gen/ts/v1/config.pb";
|
||||
import { Button, Flex, Tabs, Tooltip, Typography } from "antd";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { configState } from "../state/config";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { Restora } from "../../gen/ts/v1/service.pb";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY } from "../constants";
|
||||
|
||||
export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
const alertsApi = useAlertApi()!;
|
||||
|
||||
// Gracefully handle deletions by checking if the plan is still in the config.
|
||||
const config = useRecoilValue(configState);
|
||||
let repoInConfig = config.repos?.find((p) => p.id === repo.id);
|
||||
if (!repoInConfig) {
|
||||
return <p>Repo was deleted.</p>;
|
||||
}
|
||||
repo = repoInConfig;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="small" align="center" wrap="wrap">
|
||||
<Typography.Title>
|
||||
{repo.id}
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: "Tree View",
|
||||
children: (
|
||||
<>
|
||||
<OperationTree
|
||||
req={{ repoId: repo.id!, lastN: "" + MAX_OPERATION_HISTORY }}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: "Operation List",
|
||||
children: (
|
||||
<>
|
||||
<h2>Backup Action History</h2>
|
||||
<OperationList
|
||||
req={{ repoId: repo.id!, lastN: "" + MAX_OPERATION_HISTORY }}
|
||||
showPlan={true}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user