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

@@ -1,4 +1,5 @@
import { useMemo } from "react";
"use client";
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
@@ -15,14 +16,11 @@ export default function MediaServerWidget({
integrationIds,
isEditMode,
options,
itemId,
}: WidgetComponentProps<"mediaRequests-requestList">) {
const t = useScopedI18n("widget.mediaRequests-requestList");
const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery(
{
integrationIds,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
itemId: itemId!,
},
{
refetchOnMount: false,
@@ -30,30 +28,39 @@ export default function MediaServerWidget({
refetchOnReconnect: false,
},
);
const utils = clientApi.useUtils();
clientApi.widget.mediaRequests.subscribeToLatestRequests.useSubscription(
{
integrationIds,
},
{
onData(data) {
utils.widget.mediaRequests.getLatestRequests.setData({ integrationIds }, (prevData) => {
if (!prevData) return [];
const sortedMediaRequests = useMemo(
() =>
mediaRequests
.filter((group) => group != null)
.flatMap((group) => group.data)
.flatMap(({ medias, integration }) => medias.map((media) => ({ ...media, integrationId: integration.id })))
.sort(({ status: statusA }, { status: statusB }) => {
if (statusA === MediaRequestStatus.PendingApproval) {
return -1;
}
if (statusB === MediaRequestStatus.PendingApproval) {
return 1;
}
return 0;
}),
[mediaRequests],
const filteredData = prevData.filter(({ integrationId }) => integrationId !== data.integrationId);
const newData = filteredData.concat(
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
);
return newData.sort(({ status: statusA }, { status: statusB }) => {
if (statusA === MediaRequestStatus.PendingApproval) {
return -1;
}
if (statusB === MediaRequestStatus.PendingApproval) {
return 1;
}
return 0;
});
});
},
},
);
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
if (integrationIds.length === 0) throw new NoIntegrationSelectedError();
if (sortedMediaRequests.length === 0) throw new NoIntegrationDataError();
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
return (
<ScrollArea
@@ -62,7 +69,7 @@ export default function MediaServerWidget({
style={{ pointerEvents: isEditMode ? "none" : undefined }}
>
<Stack className="mediaRequests-list-list" gap="2cqmin" p="2cqmin">
{sortedMediaRequests.map((mediaRequest) => (
{mediaRequests.map((mediaRequest) => (
<Card
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`}
key={mediaRequest.id}

View File

@@ -1,4 +1,5 @@
import { useMemo } from "react";
"use client";
import { ActionIcon, Avatar, Card, Grid, Group, Space, Stack, Text, Tooltip } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import type { Icon } from "@tabler/icons-react";
@@ -27,14 +28,11 @@ import classes from "./component.module.css";
export default function MediaServerWidget({
integrationIds,
isEditMode,
itemId,
}: WidgetComponentProps<"mediaRequests-requestStats">) {
const t = useScopedI18n("widget.mediaRequests-requestStats");
const [requestStats] = clientApi.widget.mediaRequests.getStats.useSuspenseQuery(
{
integrationIds,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
itemId: itemId!,
},
{
refetchOnMount: false,
@@ -45,67 +43,51 @@ export default function MediaServerWidget({
const { width, height, ref } = useElementSize();
const baseData = useMemo(
() => requestStats.filter((group) => group != null).flatMap((group) => group.data),
[requestStats],
);
const stats = useMemo(() => baseData.flatMap(({ stats }) => stats), [baseData]);
const users = useMemo(
() =>
baseData
.flatMap(({ integration, users }) =>
users.flatMap((user) => ({ ...user, appKind: integration.kind, appName: integration.name })),
)
.sort(({ requestCount: countA }, { requestCount: countB }) => countB - countA),
[baseData],
);
if (integrationIds.length === 0) throw new NoIntegrationSelectedError();
if (users.length === 0 || stats.length === 0) throw new NoIntegrationDataError();
if (requestStats.users.length === 0 && requestStats.stats.length === 0) throw new NoIntegrationDataError();
//Add processing and available
const data = [
{
name: "approved",
icon: IconThumbUp,
number: stats.reduce((count, { approved }) => count + approved, 0),
number: requestStats.stats.reduce((count, { approved }) => count + approved, 0),
},
{
name: "pending",
icon: IconHourglass,
number: stats.reduce((count, { pending }) => count + pending, 0),
number: requestStats.stats.reduce((count, { pending }) => count + pending, 0),
},
{
name: "processing",
icon: IconLoaderQuarter,
number: stats.reduce((count, { processing }) => count + processing, 0),
number: requestStats.stats.reduce((count, { processing }) => count + processing, 0),
},
{
name: "declined",
icon: IconThumbDown,
number: stats.reduce((count, { declined }) => count + declined, 0),
number: requestStats.stats.reduce((count, { declined }) => count + declined, 0),
},
{
name: "available",
icon: IconPlayerPlay,
number: stats.reduce((count, { available }) => count + available, 0),
number: requestStats.stats.reduce((count, { available }) => count + available, 0),
},
{
name: "tv",
icon: IconDeviceTv,
number: stats.reduce((count, { tv }) => count + tv, 0),
number: requestStats.stats.reduce((count, { tv }) => count + tv, 0),
},
{
name: "movie",
icon: IconMovie,
number: stats.reduce((count, { movie }) => count + movie, 0),
number: requestStats.stats.reduce((count, { movie }) => count + movie, 0),
},
{
name: "total",
icon: IconReceipt,
number: stats.reduce((count, { total }) => count + total, 0),
number: requestStats.stats.reduce((count, { total }) => count + total, 0),
},
] satisfies { name: keyof RequestStats; icon: Icon; number: number }[];
@@ -156,7 +138,7 @@ export default function MediaServerWidget({
gap="2cqmin"
style={{ overflow: "hidden" }}
>
{users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => (
{requestStats.users.slice(0, Math.max(Math.floor((height / width) * 5), 1)).map((user) => (
<Card
className={combineClasses(
"mediaRequests-stats-users-user-wrapper",
@@ -170,12 +152,12 @@ export default function MediaServerWidget({
radius="2.5cqmin"
>
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="2cqmin" display="flex">
<Tooltip label={user.appName}>
<Tooltip label={user.integration.name}>
<Avatar
className="mediaRequests-stats-users-user-avatar"
size="12.5cqmin"
src={user.avatar}
bd={`0.5cqmin solid ${user.appKind === "overseerr" ? "#ECB000" : "#6677CC"}`}
bd={`0.5cqmin solid ${user.integration.kind === "overseerr" ? "#ECB000" : "#6677CC"}`}
/>
</Tooltip>
<Stack className="mediaRequests-stats-users-user-infos" gap="2cqmin">