mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-27 11:26:28 +00:00
fix: misc ui consistency and refresh errors
This commit is contained in:
@@ -18,8 +18,9 @@ export const ActivityBar = () => {
|
||||
const setRefresh = useState<number>(0)[1];
|
||||
|
||||
useEffect(() => {
|
||||
const callback = ({ operation, type }: OperationEvent) => {
|
||||
if (!operation || !type) return;
|
||||
const callback = (event?: OperationEvent, err?: Error) => {
|
||||
if (!event || !event.operation) return;
|
||||
const operation = event.operation;
|
||||
|
||||
setActiveOperations((ops) => {
|
||||
ops = ops.filter((op) => op.id !== operation.id);
|
||||
|
||||
@@ -119,8 +119,8 @@ export const HooksFormList = () => {
|
||||
size="small"
|
||||
style={{ marginBottom: "5px" }}
|
||||
>
|
||||
<Form.Item name={[field.name, "conditions"]}>
|
||||
<HookConditionsTooltip>
|
||||
<HookConditionsTooltip>
|
||||
<Form.Item name={[field.name, "conditions"]}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
allowClear
|
||||
@@ -130,8 +130,8 @@ export const HooksFormList = () => {
|
||||
.getEnumType(Hook_Condition)
|
||||
.values.map((v) => ({ label: v.name, value: v.name }))}
|
||||
/>
|
||||
</HookConditionsTooltip>
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
</HookConditionsTooltip>
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, curValues) => {
|
||||
return prevValues.hooks[index] !== curValues.hooks[index];
|
||||
|
||||
@@ -40,27 +40,7 @@ export const OperationList = ({
|
||||
|
||||
// track backups for this operation tree view.
|
||||
useEffect(() => {
|
||||
if (!req) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backupCollector = new BackupInfoCollector(filter);
|
||||
const lis = (opEvent: OperationEvent) => {
|
||||
if (
|
||||
!req.selector ||
|
||||
!opEvent.operation ||
|
||||
!matchSelector(req.selector, opEvent.operation)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (opEvent.type !== OperationEventType.EVENT_DELETED) {
|
||||
backupCollector.addOperation(opEvent.type!, opEvent.operation!);
|
||||
} else {
|
||||
backupCollector.removeOperation(opEvent.operation!);
|
||||
}
|
||||
};
|
||||
subscribeToOperations(lis);
|
||||
|
||||
const backupCollector = new BackupInfoCollector();
|
||||
backupCollector.subscribe(
|
||||
_.debounce(
|
||||
() => {
|
||||
@@ -71,20 +51,13 @@ export const OperationList = ({
|
||||
setBackups(backups);
|
||||
},
|
||||
100,
|
||||
{ trailing: true }
|
||||
{ leading: true, trailing: true }
|
||||
)
|
||||
);
|
||||
|
||||
getOperations(req)
|
||||
.then((ops) => {
|
||||
backupCollector.bulkAddOperations(ops);
|
||||
})
|
||||
.catch((e) => {
|
||||
alertApi!.error("Failed to fetch operations: " + e.message);
|
||||
});
|
||||
return () => {
|
||||
unsubscribeFromOperations(lis);
|
||||
};
|
||||
return backupCollector.collectFromRequest(req, (err) => {
|
||||
alertApi!.error("API error: " + err.message);
|
||||
});
|
||||
}, [JSON.stringify(req)]);
|
||||
} else {
|
||||
backups = [...(useBackups || [])];
|
||||
|
||||
@@ -54,30 +54,14 @@ export const OperationTree = ({
|
||||
}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
|
||||
const alertApi = useAlertApi();
|
||||
const showModal = useShowModal();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backups, setBackups] = useState<BackupInfo[]>([]);
|
||||
const [selectedBackupId, setSelectedBackupId] = useState<string | null>(null);
|
||||
|
||||
// track backups for this operation tree view.
|
||||
useEffect(() => {
|
||||
setSelectedBackupId(null);
|
||||
const backupCollector = new BackupInfoCollector();
|
||||
const lis = (opEvent: OperationEvent) => {
|
||||
if (
|
||||
!req.selector ||
|
||||
!opEvent.operation ||
|
||||
!matchSelector(req.selector, opEvent.operation)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (opEvent.type !== OperationEventType.EVENT_DELETED) {
|
||||
backupCollector.addOperation(opEvent.type!, opEvent.operation!);
|
||||
} else {
|
||||
backupCollector.removeOperation(opEvent.operation!);
|
||||
}
|
||||
};
|
||||
subscribeToOperations(lis);
|
||||
|
||||
const backupCollector = new BackupInfoCollector();
|
||||
backupCollector.subscribe(
|
||||
_.debounce(
|
||||
() => {
|
||||
@@ -92,19 +76,9 @@ export const OperationTree = ({
|
||||
)
|
||||
);
|
||||
|
||||
getOperations(req)
|
||||
.then((ops) => {
|
||||
backupCollector.bulkAddOperations(ops);
|
||||
})
|
||||
.catch((e) => {
|
||||
alertApi!.error("Failed to fetch operations: " + e.messag);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
unsubscribeFromOperations(lis);
|
||||
};
|
||||
return backupCollector.collectFromRequest(req, (err) => {
|
||||
alertApi!.error("API error: " + err.message);
|
||||
});
|
||||
}, [JSON.stringify(req)]);
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
@@ -113,10 +87,7 @@ export const OperationTree = ({
|
||||
|
||||
if (backups.length === 0) {
|
||||
return (
|
||||
<Empty
|
||||
description={loading ? "Loading..." : "No backups yet."}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
></Empty>
|
||||
<Empty description={""} image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,57 +293,41 @@ const BackupViewContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
const refresh = useState(0)[1];
|
||||
const [offset, setOffset] = useState(0);
|
||||
|
||||
// THE RULES
|
||||
// the top can not be more than windowHeight - divHeight pixels beyond the top
|
||||
// the bottom can not poke more than windowHeight - divHeight pixels beyond the bottom
|
||||
const [topY, setTopY] = useState(0);
|
||||
const [bottomY, setBottomY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !innerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// handle scroll events to keep the fixed container in view.
|
||||
const handleScroll = () => {
|
||||
const rect = ref.current?.getBoundingClientRect();
|
||||
const innerRect = innerRef.current?.getBoundingClientRect();
|
||||
const refRect = ref.current!.getBoundingClientRect();
|
||||
const innerRect = innerRef.current!.getBoundingClientRect();
|
||||
|
||||
if (!rect || !innerRect) {
|
||||
return;
|
||||
let wiggle = Math.max(refRect.height - window.innerHeight, 0);
|
||||
let topY = Math.max(ref.current!.getBoundingClientRect().top, 0);
|
||||
let bottomY = topY;
|
||||
if (topY == 0) {
|
||||
// wiggle only if the top is actually the top edge of the screen.
|
||||
topY -= wiggle;
|
||||
bottomY += wiggle;
|
||||
}
|
||||
|
||||
if (innerRect.height <= window.innerHeight) {
|
||||
if (rect.top <= 0) {
|
||||
console.log("top overflow", rect.top, offset);
|
||||
offset = -rect.top;
|
||||
setOffset(offset);
|
||||
return;
|
||||
}
|
||||
console.log("just do the default stuff");
|
||||
setOffset(0);
|
||||
refresh(Math.random());
|
||||
return;
|
||||
}
|
||||
setTopY(topY);
|
||||
setBottomY(bottomY);
|
||||
|
||||
const maxOverflow = innerRect.height - window.innerHeight;
|
||||
|
||||
if (rect.top + offset < -maxOverflow) {
|
||||
offset = -maxOverflow - rect.top;
|
||||
setOffset(offset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rect.top + offset > 0) {
|
||||
console.log("bottom overflow", rect.top + offset, maxOverflow);
|
||||
offset = -rect.top;
|
||||
setOffset(offset);
|
||||
return;
|
||||
}
|
||||
refresh(Math.random());
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
// attach resize observer to ref to update the width of the fixed container.
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
refresh(Math.random());
|
||||
handleScroll();
|
||||
});
|
||||
if (ref.current) {
|
||||
resizeObserver.observe(ref.current);
|
||||
@@ -398,7 +353,7 @@ const BackupViewContainer = ({ children }: { children: React.ReactNode }) => {
|
||||
ref={innerRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: (rect?.top || 0) + offset,
|
||||
top: Math.max(Math.min(rect?.top || 0, bottomY), topY),
|
||||
left: rect?.left,
|
||||
width: ref.current?.clientWidth,
|
||||
}}
|
||||
|
||||
@@ -85,8 +85,9 @@ export const ScheduleFormItem = ({
|
||||
"month-days",
|
||||
"hours",
|
||||
"minutes",
|
||||
"week-days",
|
||||
]}
|
||||
allowedPeriods={["day", "hour", "month"]}
|
||||
allowedPeriods={["day", "hour", "month", "week"]}
|
||||
clearButton={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -76,10 +76,11 @@ const StatsPanel = ({ repoId }: { repoId: string }) => {
|
||||
|
||||
refreshOperations();
|
||||
|
||||
const handler = (event: OperationEvent) => {
|
||||
const handler = (event?: OperationEvent, err?: Error) => {
|
||||
if (!event || !event.operation) return;
|
||||
if (
|
||||
event.operation?.repoId == repoId &&
|
||||
event.operation?.op?.case === "operationStats"
|
||||
event.operation.repoId == repoId &&
|
||||
event.operation.op?.case === "operationStats"
|
||||
) {
|
||||
refreshOperations();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
STATUS_OPERATION_HISTORY,
|
||||
} from "../constants";
|
||||
|
||||
const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
const subscribers: ((event?: OperationEvent, err?: Error) => void)[] = [];
|
||||
|
||||
// Start fetching and emitting operations.
|
||||
(async () => {
|
||||
@@ -23,7 +23,7 @@ const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
try {
|
||||
for await (const event of backrestService.getOperationEvents({})) {
|
||||
console.log("operation event", event);
|
||||
subscribers.forEach((subscriber) => subscriber(event));
|
||||
subscribers.forEach((subscriber) => subscriber(event, undefined));
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("operations stream died with exception: ", e);
|
||||
@@ -31,6 +31,7 @@ const subscribers: ((event: OperationEvent) => void)[] = [];
|
||||
await new Promise((accept, _) =>
|
||||
setTimeout(accept, nextConnWaitUntil - new Date().getTime()),
|
||||
);
|
||||
subscribers.forEach((subscriber) => subscriber(undefined, new Error("reconnecting")));
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -42,14 +43,14 @@ export const getOperations = async (
|
||||
};
|
||||
|
||||
export const subscribeToOperations = (
|
||||
callback: (event: OperationEvent) => void,
|
||||
callback: (event?: OperationEvent, err?: Error) => void,
|
||||
) => {
|
||||
subscribers.push(callback);
|
||||
console.log("subscribed to operations, subscriber count: ", subscribers.length);
|
||||
};
|
||||
|
||||
export const unsubscribeFromOperations = (
|
||||
callback: (event: OperationEvent) => void,
|
||||
callback: (event?: OperationEvent, err?: Error) => void,
|
||||
) => {
|
||||
const index = subscribers.indexOf(callback);
|
||||
if (index > -1) {
|
||||
@@ -144,6 +145,46 @@ export class BackupInfoCollector {
|
||||
!shouldHideOperation(op),
|
||||
) { }
|
||||
|
||||
public reset() {
|
||||
this.operationsByFlowId = new Map();
|
||||
this.backupsByFlowId = new Map();
|
||||
}
|
||||
|
||||
public collectFromRequest(request: GetOperationsRequest, onError?: (cb: Error) => void): () => void {
|
||||
getOperations(request).then((ops) => {
|
||||
this.bulkAddOperations(ops);
|
||||
}).catch(onError);
|
||||
|
||||
const cb = (event?: OperationEvent, err?: Error) => {
|
||||
if (event) {
|
||||
if (
|
||||
!request.selector ||
|
||||
!event.operation ||
|
||||
!matchSelector(request.selector, event.operation)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (event.type !== OperationEventType.EVENT_DELETED) {
|
||||
this.addOperation(event.type!, event.operation!);
|
||||
} else {
|
||||
this.removeOperation(event.operation!);
|
||||
}
|
||||
} else if (err) {
|
||||
if (onError) onError(err);
|
||||
console.error("error in operations stream: ", err);
|
||||
getOperations(request).then((ops) => {
|
||||
this.reset();
|
||||
this.bulkAddOperations(ops);
|
||||
}).catch(onError);
|
||||
}
|
||||
}
|
||||
subscribeToOperations(cb);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromOperations(cb);
|
||||
};
|
||||
}
|
||||
|
||||
private createBackup(operations: Operation[]): BackupInfo {
|
||||
// deduplicate and sort operations.
|
||||
operations.sort((a, b) => {
|
||||
@@ -204,13 +245,13 @@ export class BackupInfoCollector {
|
||||
displayTime,
|
||||
displayType,
|
||||
status,
|
||||
operations,
|
||||
backupLastStatus,
|
||||
snapshotInfo,
|
||||
forgotten,
|
||||
snapshotId: snapshotId,
|
||||
planId: operations[0].planId,
|
||||
repoId: operations[0].repoId,
|
||||
operations: [...operations], // defensive copy.
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,8 @@ import {
|
||||
Radio,
|
||||
InputNumber,
|
||||
Row,
|
||||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
FormInstance,
|
||||
Checkbox,
|
||||
} from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
@@ -20,12 +18,7 @@ import { useShowModal } from "../components/ModalManager";
|
||||
import { Plan, RetentionPolicy } from "../../gen/ts/v1/config_pb";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { URIAutocomplete } from "../components/URIAutocomplete";
|
||||
import {
|
||||
formatError,
|
||||
formatErrorAlert,
|
||||
useAlertApi,
|
||||
} from "../components/Alerts";
|
||||
import { Cron } from "react-js-cron";
|
||||
import { formatErrorAlert, useAlertApi } from "../components/Alerts";
|
||||
import { namePattern, validateForm } from "../lib/formutil";
|
||||
import {
|
||||
HooksFormList,
|
||||
|
||||
@@ -18,8 +18,6 @@ import { ActivityBar } from "../components/ActivityBar";
|
||||
import { OperationEvent, OperationStatus } from "../../gen/ts/v1/operations_pb";
|
||||
import {
|
||||
colorForStatus,
|
||||
getStatusForPlan,
|
||||
getStatusForRepo,
|
||||
getStatusForSelector,
|
||||
subscribeToOperations,
|
||||
unsubscribeFromOperations,
|
||||
@@ -309,8 +307,9 @@ const IconForResource = ({
|
||||
};
|
||||
load();
|
||||
const refresh = _.debounce(load, 1000, { maxWait: 5000, trailing: true });
|
||||
const callback = ({ operation }: OperationEvent) => {
|
||||
if (!operation) return;
|
||||
const callback = (event?: OperationEvent, err?: Error) => {
|
||||
if (!event || !event.operation) return;
|
||||
const operation = event.operation;
|
||||
if (operation.planId === planId || operation.repoId === repoId) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,21 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlockNow = async () => {
|
||||
try {
|
||||
alertsApi.info("Unlocking repo...");
|
||||
await backrestService.doRepoTask(
|
||||
new DoRepoTaskRequest({
|
||||
repoId: repo.id!,
|
||||
task: DoRepoTaskRequest_Task.UNLOCK,
|
||||
})
|
||||
);
|
||||
alertsApi.success("Repo unlocked.");
|
||||
} catch (e: any) {
|
||||
alertsApi.error("Failed to unlock repo: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatsNow = async () => {
|
||||
try {
|
||||
await backrestService.doRepoTask(
|
||||
@@ -96,7 +111,6 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
label: "Tree View",
|
||||
children: (
|
||||
<>
|
||||
<h3>Browse Backups</h3>
|
||||
<OperationTree
|
||||
req={
|
||||
new GetOperationsRequest({
|
||||
@@ -168,6 +182,12 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
|
||||
</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">
|
||||
<SpinButton type="default" onClickAsync={handleUnlockNow}>
|
||||
Unlock Repo
|
||||
</SpinButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Runs a prune operation on the repository that will remove old snapshots and free up space">
|
||||
<SpinButton type="default" onClickAsync={handlePruneNow}>
|
||||
Prune Now
|
||||
|
||||
Reference in New Issue
Block a user