Skip to content

Commit

Permalink
fix: misc ui consistency and refresh errors
Browse files Browse the repository at this point in the history
  • Loading branch information
garethgeorge committed Jun 14, 2024
1 parent 91e0fda commit 793666c
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 131 deletions.
5 changes: 3 additions & 2 deletions webui/src/components/ActivityBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions webui/src/components/HooksFormList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
Expand Down
37 changes: 5 additions & 32 deletions webui/src/components/OperationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() => {
Expand All @@ -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 || [])];
Expand Down
97 changes: 26 additions & 71 deletions webui/src/components/OperationTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
() => {
Expand All @@ -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(() => {
Expand All @@ -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>
);
}

Expand Down Expand Up @@ -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();

if (!rect || !innerRect) {
return;
}

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;
const refRect = ref.current!.getBoundingClientRect();
const innerRect = innerRef.current!.getBoundingClientRect();

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;
}

const maxOverflow = innerRect.height - window.innerHeight;
setTopY(topY);
setBottomY(bottomY);

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);
Expand All @@ -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,
}}
Expand Down
3 changes: 2 additions & 1 deletion webui/src/components/ScheduleFormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
7 changes: 4 additions & 3 deletions webui/src/components/StatsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
51 changes: 46 additions & 5 deletions webui/src/state/oplog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -23,14 +23,15 @@ 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);
}
await new Promise((accept, _) =>
setTimeout(accept, nextConnWaitUntil - new Date().getTime()),
);
subscribers.forEach((subscriber) => subscriber(undefined, new Error("reconnecting")));
}
})();

Expand All @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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.
};
}

Expand Down
9 changes: 1 addition & 8 deletions webui/src/views/AddPlanModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,16 @@ import {
Radio,
InputNumber,
Row,
Card,
Col,
Collapse,
FormInstance,
Checkbox,
} from "antd";
import React, { useEffect, useState } from "react";
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,
Expand Down
Loading

0 comments on commit 793666c

Please sign in to comment.