mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-09-21 23:45:36 +00:00
feat: schedule index operations and stats refresh from repo view
This commit is contained in:
@@ -5,6 +5,7 @@ import { Backrest } from "../gen/ts/v1/service_connect";
|
||||
|
||||
const transport = createConnectTransport({
|
||||
baseUrl: "/",
|
||||
useBinaryFormat: true,
|
||||
});
|
||||
|
||||
export const backrestService = createPromiseClient(Backrest, transport);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react";
|
||||
import { Button, ButtonProps } from "antd";
|
||||
import { useState } from "react";
|
||||
|
||||
export const SpinButton: React.FC<ButtonProps & {
|
||||
onClickAsync: () => Promise<void>;
|
||||
}> = ({ onClickAsync, ...props }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onClick = async () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await onClickAsync();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
loading={loading}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export const MAX_OPERATION_HISTORY = 10000;
|
||||
export const STATUS_OPERATION_HISTORY = 10; // number of operations to load when determining plan / repo status.
|
||||
export const STATS_OPERATION_HISTORY = 100; // number of operations to load when searching for stats for a repo.
|
||||
@@ -9,6 +9,7 @@ import { BackupProgressEntry, ResticSnapshot } from "../../gen/ts/v1/restic_pb";
|
||||
import _ from "lodash";
|
||||
import { formatDuration, formatTime } from "../lib/formatting";
|
||||
import { backrestService } from "../api";
|
||||
import { STATS_OPERATION_HISTORY } from "../constants";
|
||||
|
||||
const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
|
||||
@@ -56,7 +57,7 @@ export const unsubscribeFromOperations = (
|
||||
export const getStatusForPlan = async (plan: string) => {
|
||||
const req = new GetOperationsRequest({
|
||||
planId: plan,
|
||||
lastN: BigInt(8),
|
||||
lastN: BigInt(STATS_OPERATION_HISTORY),
|
||||
});
|
||||
return await getStatus(req);
|
||||
}
|
||||
@@ -64,7 +65,7 @@ export const getStatusForPlan = async (plan: string) => {
|
||||
export const getStatusForRepo = async (repo: string) => {
|
||||
const req = new GetOperationsRequest({
|
||||
repoId: repo,
|
||||
lastN: BigInt(8),
|
||||
lastN: BigInt(STATS_OPERATION_HISTORY),
|
||||
});
|
||||
return await getStatus(req);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY } from "../constants";
|
||||
import { backrestService } from "../api";
|
||||
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
|
||||
import { SpinButton } from "../components/SpinButton";
|
||||
|
||||
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
const alertsApi = useAlertApi()!;
|
||||
@@ -30,29 +31,29 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePruneNow = () => {
|
||||
const handlePruneNow = async () => {
|
||||
try {
|
||||
backrestService.prune({ value: plan.id });
|
||||
await backrestService.prune({ value: plan.id });
|
||||
alertsApi.success("Prune scheduled.");
|
||||
} catch (e: any) {
|
||||
alertsApi.error("Failed to schedule prune: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlockNow = () => {
|
||||
const handleUnlockNow = async () => {
|
||||
try {
|
||||
alertsApi.info("Unlocking repo...");
|
||||
backrestService.unlock({ value: plan.repo! });
|
||||
await backrestService.unlock({ value: plan.repo! });
|
||||
alertsApi.success("Repo unlocked.");
|
||||
} catch (e: any) {
|
||||
alertsApi.error("Failed to unlock repo: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearErrorHistory = () => {
|
||||
const handleClearErrorHistory = async () => {
|
||||
try {
|
||||
alertsApi.info("Clearing error history...");
|
||||
backrestService.clearHistory({ planId: plan.id, onlyFailed: true });
|
||||
await backrestService.clearHistory({ planId: plan.id, onlyFailed: true });
|
||||
alertsApi.success("Error history cleared.");
|
||||
} catch (e: any) {
|
||||
alertsApi.error("Failed to clear error history: " + e.message);
|
||||
@@ -67,23 +68,23 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
<Flex gap="small" align="center" wrap="wrap">
|
||||
<Button type="primary" onClick={handleBackupNow}>
|
||||
<SpinButton type="primary" onClickAsync={handleBackupNow}>
|
||||
Backup Now
|
||||
</Button>
|
||||
</SpinButton>
|
||||
<Tooltip title="Runs a prune operation on the repository that will remove old snapshots and free up space">
|
||||
<Button type="default" onClick={handlePruneNow}>
|
||||
<SpinButton type="default" onClickAsync={handlePruneNow}>
|
||||
Prune Now
|
||||
</Button>
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Removes lockfiles and checks the repository for errors. Only run if you are sure the repo is not being accessed by another system">
|
||||
<Button type="default" onClick={handleUnlockNow}>
|
||||
<SpinButton type="default" onClickAsync={handleUnlockNow}>
|
||||
Unlock Repo
|
||||
</Button>
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Removes failed operations from the list">
|
||||
<Button type="default" onClick={handleClearErrorHistory}>
|
||||
<SpinButton type="default" onClickAsync={handleClearErrorHistory}>
|
||||
Clear Error History
|
||||
</Button>
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tabs
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Repo } from "../../gen/ts/v1/config_pb";
|
||||
import { Col, Empty, Flex, Row, Spin, Tabs, Typography } from "antd";
|
||||
import { Col, Empty, Flex, Row, Spin, TabsProps, Tabs, Tooltip, Typography } from "antd";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { configState } from "../state/config";
|
||||
import { useAlertApi } from "../components/Alerts";
|
||||
import { OperationList } from "../components/OperationList";
|
||||
import { OperationTree } from "../components/OperationTree";
|
||||
import { MAX_OPERATION_HISTORY } from "../constants";
|
||||
import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants";
|
||||
import { GetOperationsRequest } from "../../gen/ts/v1/service_pb";
|
||||
import { getOperations } from "../state/oplog";
|
||||
import { RepoStats } from "../../gen/ts/v1/restic_pb";
|
||||
import { formatBytes, formatDate, formatTime } from "../lib/formatting";
|
||||
import { formatBytes, formatTime } from "../lib/formatting";
|
||||
import { Operation } from "../../gen/ts/v1/operations_pb";
|
||||
import { backrestService } from "../api";
|
||||
import { StringValue } from "@bufbuild/protobuf";
|
||||
import { SpinButton } from "../components/SpinButton";
|
||||
|
||||
export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
const alertsApi = useAlertApi()!;
|
||||
@@ -21,7 +24,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setStatsOperation(null);
|
||||
getOperations(new GetOperationsRequest({ repoId: repo.id!, lastN: BigInt(100) })).then((operations) => {
|
||||
getOperations(new GetOperationsRequest({ repoId: repo.id!, lastN: BigInt(STATS_OPERATION_HISTORY) })).then((operations) => {
|
||||
for (const op of operations) {
|
||||
if (op.op.case === "operationStats") {
|
||||
const stats = op.op.value.stats;
|
||||
@@ -37,6 +40,11 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
});
|
||||
}, [repo.id]);
|
||||
|
||||
// Task handlers
|
||||
const handleIndexNow = async () => {
|
||||
await backrestService.indexSnapshots(new StringValue({ value: repo.id! }));
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -64,6 +72,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
}
|
||||
</>
|
||||
),
|
||||
destroyInactiveTabPane: true,
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
@@ -76,6 +85,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
/>
|
||||
</>
|
||||
),
|
||||
destroyInactiveTabPane: true,
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
@@ -89,6 +99,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
/>
|
||||
</>
|
||||
),
|
||||
destroyInactiveTabPane: true,
|
||||
},
|
||||
]
|
||||
return (
|
||||
@@ -98,6 +109,18 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
{repo.id}
|
||||
</Typography.Title>
|
||||
</Flex>
|
||||
<Flex gap="small" align="center" wrap="wrap">
|
||||
<Tooltip title="Indexes the snapshots in the repository. Snapshots are also indexed automatically after each backup.">
|
||||
<SpinButton type="default" onClickAsync={handleIndexNow}>
|
||||
Index Snapshots
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Computes stats for the repository. May take some time to refresh.">
|
||||
<SpinButton type="default" onClickAsync={handleIndexNow}>
|
||||
Index Snapshots
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Tabs
|
||||
defaultActiveKey={items[0].key}
|
||||
items={items}
|
||||
|
||||
Reference in New Issue
Block a user