refactor: add request handlers for centralized cached requests (#1504)

* feat: add object base64 hash method

* chore: add script to add package

* feat: add request-handler package

* wip: add request handlers for all jobs and widget api procedures

* wip: remove errors shown in logs, add missing decryption for secrets in cached-request-job-handler

* wip: highly improve request handler, add request handlers for calendar, media-server, indexer-manager and more, add support for multiple inputs from job handler creator

* refactor: move media-server requests to request-handler, add invalidation logic for dns-hole and media requests

* refactor: remove unused integration item middleware

* feat: add invalidation to switch entity action of smart-home

* fix: lint issues

* chore: use integration-kind-by-category instead of union for request-handlers

* fix: build not working for tasks and websocket

* refactor: add more logs

* refactor: readd timestamp logic for diconnect status

* fix: lint and typecheck issue

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-11-23 17:16:44 +01:00
committed by GitHub
parent cdfb61fb28
commit 32ee9f3dcc
73 changed files with 1114 additions and 665 deletions

View File

@@ -21,7 +21,7 @@ import {
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useTimeout } from "@mantine/hooks";
import { useDisclosure } from "@mantine/hooks";
import type { IconProps } from "@tabler/icons-react";
import {
IconAlertTriangle,
@@ -39,13 +39,9 @@ import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
import { clientApi } from "@homarr/api/client";
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
import { humanFileSize } from "@homarr/common";
import { humanFileSize, useIntegrationConnected } from "@homarr/common";
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
import type {
DownloadClientJobsAndStatus,
ExtendedClientStatus,
ExtendedDownloadClientItem,
} from "@homarr/integrations";
import type { ExtendedClientStatus, ExtendedDownloadClientItem } from "@homarr/integrations";
import { useScopedI18n } from "@homarr/translation/client";
import type { WidgetComponentProps } from "../definition";
@@ -82,8 +78,6 @@ const standardIconStyle: IconProps["style"] = {
width: "var(--icon-size)",
};
const invalidateTime = 30000;
export default function DownloadClientsWidget({
isEditMode,
integrationIds,
@@ -103,26 +97,10 @@ export default function DownloadClientsWidget({
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
select(data) {
return data.map((item) =>
dayjs().diff(item.timestamp) < invalidateTime ? item : { ...item, timestamp: new Date(0), data: null },
);
},
},
);
const utils = clientApi.useUtils();
//Invalidate all data after no update for 30 seconds using timer
const invalidationTimer = useTimeout(
() => {
utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) =>
prevData?.map((item) => ({ ...item, timestamp: new Date(0), data: null })),
);
},
invalidateTime,
{ autoInvoke: true },
);
//Translations
const t = useScopedI18n("widget.downloads");
const tCommon = useScopedI18n("common");
@@ -143,32 +121,19 @@ export default function DownloadClientsWidget({
},
{
onData: (data) => {
//Use cyclical update to invalidate data older than 30 seconds from unresponsive integrations
const invalidIndexes = currentItems
//Don't update already invalid data (new Date (0))
.filter(({ timestamp }) => dayjs().diff(timestamp) > invalidateTime && timestamp > new Date(0))
.map(({ integration }) => integration.id);
utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) =>
prevData?.map((item) =>
invalidIndexes.includes(item.integration.id) ? item : { ...item, timestamp: new Date(0), data: null },
),
);
utils.widget.downloads.getJobsAndStatuses.setData({ integrationIds }, (prevData) => {
const updateIndex = currentItems.findIndex((pair) => pair.integration.id === data.integration.id);
if (updateIndex >= 0) {
//Update found index
return prevData?.map((pair, index) => (index === updateIndex ? data : pair));
} else if (integrationIds.includes(data.integration.id)) {
//Append index not found (new integration)
return [...(prevData ?? []), data];
}
return prevData?.map((item) => {
if (item.integration.id !== data.integration.id) return item;
return undefined;
return {
data: data.data,
integration: {
...data.integration,
updatedAt: new Date(),
},
};
});
});
//Reset no update timer
invalidationTimer.clear();
invalidationTimer.start();
},
},
);
@@ -179,16 +144,6 @@ export default function DownloadClientsWidget({
currentItems
//Insure it is only using selected integrations
.filter(({ integration }) => integrationIds.includes(integration.id))
//Removing any integration with no data associated
.filter(
(
pair,
): pair is {
integration: typeof pair.integration;
timestamp: typeof pair.timestamp;
data: DownloadClientJobsAndStatus;
} => pair.data != null,
)
//Construct normalized items list
.flatMap((pair) =>
//Apply user white/black list
@@ -255,7 +210,6 @@ export default function DownloadClientsWidget({
.filter(({ integration }) => integrationIds.includes(integration.id))
.flatMap(({ integration, data }): ExtendedClientStatus => {
const interact = integrationsWithInteractions.includes(integration.id);
if (!data) return { integration, interact };
const isTorrent = getIntegrationKindsByCategory("torrent").some((kind) => kind === integration.kind);
/** Derived from current items */
const { totalUp, totalDown } = data.items
@@ -821,12 +775,7 @@ const ClientsControl = ({ clients, style }: ClientsControlProps) => {
<Group gap="var(--space-size)" style={style}>
<AvatarGroup spacing="calc(var(--space-size)*2)">
{clients.map((client) => (
<Avatar
key={client.integration.id}
src={getIconUrl(client.integration.kind)}
size="var(--image-size)"
bd={client.status ? 0 : "calc(var(--space-size)*0.5) solid var(--mantine-color-red-filled)"}
/>
<ClientAvatar key={client.integration.id} client={client} />
))}
</AvatarGroup>
{someInteract && (
@@ -939,3 +888,21 @@ const ClientsControl = ({ clients, style }: ClientsControlProps) => {
</Group>
);
};
interface ClientAvatarProps {
client: ExtendedClientStatus;
}
const ClientAvatar = ({ client }: ClientAvatarProps) => {
const isConnected = useIntegrationConnected(client.integration.updatedAt, { timeout: 30000 });
return (
<Avatar
key={client.integration.id}
src={getIconUrl(client.integration.kind)}
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
size="var(--image-size)"
bd={client.status ? 0 : "calc(var(--space-size)*0.5) solid var(--mantine-color-red-filled)"}
/>
);
};