fix: misc ui consistency and refresh errors

This commit is contained in:
garethgeorge
2024-06-13 18:16:41 -07:00
parent a5fbc02460
commit 038d0e4bc9
10 changed files with 114 additions and 130 deletions
+3 -2
View File
@@ -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);
+4 -4
View File
@@ -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];
+5 -32
View File
@@ -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 || [])];
+25 -70
View File
@@ -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,
}}
+2 -1
View File
@@ -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>
+4 -3
View File
@@ -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();
}
+46 -5
View File
@@ -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.
};
}
+1 -8
View File
@@ -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,
+3 -4
View File
@@ -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();
}
+21 -1
View File
@@ -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