Files
karma/ui/src/Components/Fetcher/index.tsx
2026-03-23 11:00:11 +00:00

270 lines
7.7 KiB
TypeScript

import { use, useEffect, useRef, useState, FC } from "react";
import { reaction, toJS } from "mobx";
import { observer } from "mobx-react-lite";
import { addSeconds } from "date-fns/addSeconds";
import { differenceInSeconds } from "date-fns/differenceInSeconds";
import { CSSTransition } from "react-transition-group";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPause } from "@fortawesome/free-solid-svg-icons/faPause";
import { faPlay } from "@fortawesome/free-solid-svg-icons/faPlay";
import { AlertStore, AlertStoreStatuses } from "Stores/AlertStore";
import type { Settings } from "Stores/Settings";
import { ThemeContext } from "Components/Theme";
import { TooltipWrapper } from "Components/TooltipWrapper";
const PauseButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
const context = use(ThemeContext);
const nodeRef = useRef<HTMLSpanElement>(null);
return (
<TooltipWrapper title="Click to resume updates">
<CSSTransition
in={true}
appear={true}
classNames="components-animation-fade"
timeout={context.animations.duration}
nodeRef={nodeRef}
>
<span ref={nodeRef} className="d-inline-block">
<FontAwesomeIcon
className="cursor-pointer text-muted components-fetcher-icon mx-2 fa-fw"
icon={faPause}
onClick={alertStore.status.resume}
/>
</span>
</CSSTransition>
</TooltipWrapper>
);
};
const PlayButton: FC<{ alertStore: AlertStore }> = ({ alertStore }) => {
const context = use(ThemeContext);
const nodeRef = useRef<HTMLSpanElement>(null);
return (
<TooltipWrapper title="Click to pause updates">
<CSSTransition
in={true}
appear={true}
classNames="components-animation-fade"
timeout={context.animations.duration}
nodeRef={nodeRef}
>
<span ref={nodeRef} className="d-inline-block">
<FontAwesomeIcon
className="cursor-pointer text-muted components-fetcher-icon mx-2 fa-fw"
icon={faPlay}
onClick={alertStore.status.pause}
/>
</span>
</CSSTransition>
</TooltipWrapper>
);
};
const Dots: FC<{ alertStore: AlertStore; dots: number }> = observer(
({ alertStore, dots }) => {
return (
<div
className={`cursor-pointer components-fetcher ${
alertStore.info.isRetrying ? "retrying" : ""
} ${
alertStore.status.value.toString() ===
AlertStoreStatuses.Processing.toString()
? "processing"
: ""
} ${
dots === 0 ||
alertStore.status.value.toString() ===
AlertStoreStatuses.Fetching.toString()
? "fetching"
: ""
}`}
>
{Array.from(Array(9).keys()).map((i) => (
<div
key={i}
className={`dot ${i === 4 ? "dot-middle" : ""} ${
i < dots ? "visible" : "hidden"
}`}
></div>
))}
</div>
);
},
);
const Fetcher: FC<{
alertStore: AlertStore;
settingsStore: Settings;
}> = observer(({ alertStore, settingsStore }) => {
const timerRef = useRef<number | undefined>(undefined);
const [percentLeft, setPercentLeft] = useState<number>(100);
const [isHover, setIsHover] = useState(false);
const getSortSettings = () => {
const sortSettings = {
useDefaults: false,
sortOrder: "",
sortLabel: "",
sortReverse: "",
};
sortSettings.useDefaults =
settingsStore.gridConfig.config.sortOrder ===
settingsStore.gridConfig.options.default.value;
if (sortSettings.useDefaults === true) {
return sortSettings;
}
sortSettings.sortOrder = settingsStore.gridConfig.config.sortOrder;
// don't sort if sorting is disabled
if (
sortSettings.sortOrder === settingsStore.gridConfig.options.disabled.value
)
return sortSettings;
sortSettings.sortReverse =
settingsStore.gridConfig.config.reverseSort !== null
? settingsStore.gridConfig.config.reverseSort === true
? "1"
: "0"
: "";
if (settingsStore.gridConfig.config.sortLabel !== null) {
sortSettings.sortLabel = settingsStore.gridConfig.config.sortLabel;
}
return sortSettings;
};
const fetchIfIdle = () => {
const now = new Date();
const nextTick = addSeconds(
alertStore.status.lastUpdateAt,
settingsStore.fetchConfig.config.interval,
);
const secondsLeft = differenceInSeconds(nextTick, now);
setPercentLeft(
Math.max(
0,
(secondsLeft / settingsStore.fetchConfig.config.interval) * 100,
),
);
const pastDeadline = now >= nextTick;
const status = alertStore.status.value.toString();
const updateInProgress =
status === AlertStoreStatuses.Fetching.toString() ||
status === AlertStoreStatuses.Processing.toString();
if (pastDeadline && !updateInProgress && !alertStore.status.paused) {
callFetch();
}
};
const callFetch = () => {
if (alertStore.status.paused) return;
const sortSettings = getSortSettings();
alertStore.fetchWithThrottle(
settingsStore.multiGridConfig.config.gridLabel,
settingsStore.multiGridConfig.config.gridSortReverse,
sortSettings.sortOrder,
sortSettings.sortLabel,
sortSettings.sortReverse === "1",
Object.values(alertStore.ui.gridGroupLimits).length > 0
? toJS(Object.values(alertStore.ui.gridGroupLimits)[0])
: {},
settingsStore.alertGroupConfig.config.defaultRenderCount,
alertStore.ui.groupAlertLimits,
);
};
useEffect(() => {
return () => window.clearInterval(timerRef.current);
}, []);
useEffect(
() =>
reaction(
() =>
JSON.stringify({
filters: alertStore.filters.values
.map((f: { raw: string }) => f.raw)
.join(" "),
grid: {
sortOrder: toJS(settingsStore.gridConfig.config.sortOrder),
sortLabel: toJS(settingsStore.gridConfig.config.sortLabel),
gridGroupLimits: toJS(alertStore.ui.gridGroupLimits),
defaultGroupLimit: toJS(
settingsStore.alertGroupConfig.config.defaultRenderCount,
),
groupAlertLimits: toJS(alertStore.ui.groupAlertLimits),
},
multigrid: {
gridLabel: toJS(settingsStore.multiGridConfig.config.gridLabel),
gridSortReverse: toJS(
settingsStore.multiGridConfig.config.gridSortReverse,
),
reverseSort: toJS(settingsStore.gridConfig.config.reverseSort),
},
}),
() => {
callFetch();
},
{ fireImmediately: true },
),
[],
);
useEffect(
() =>
reaction(
() => alertStore.status.paused,
(paused) => {
if (paused) {
window.clearInterval(timerRef.current);
timerRef.current = undefined;
} else {
timerRef.current = window.setInterval(
() => window.requestAnimationFrame(fetchIfIdle),
1000,
);
}
},
{ fireImmediately: true },
),
[],
);
const dots = Math.max(0, Math.min(9, percentLeft / 10));
return (
<div
className="navbar-brand py-0 me-2 d-none d-sm-block"
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
{alertStore.info.upgradeNeeded ? null : alertStore.status.paused ? (
<PauseButton alertStore={alertStore} />
) : isHover ? (
<PlayButton alertStore={alertStore} />
) : (
<Dots alertStore={alertStore} dots={dots} />
)}
</div>
);
});
export { Fetcher, Dots };