fix: complete widget modernization review
This commit is contained in:
35 files changed
+621
-265
No files matched your search
@@ -1,17 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { everyBeszelIntegrationFailed } from "../beszel-failures";
|
||||
|
||||
describe("everyBeszelIntegrationFailed", () => {
|
||||
it("keeps partial successes", () => {
|
||||
expect(everyBeszelIntegrationFailed([{ error: "offline" }, {}])).toBe(false);
|
||||
});
|
||||
|
||||
it("detects when every integration failed", () => {
|
||||
expect(everyBeszelIntegrationFailed([{ error: "offline" }, { error: "timeout" }])).toBe(true);
|
||||
});
|
||||
|
||||
it("allows an empty integration selection", () => {
|
||||
expect(everyBeszelIntegrationFailed([])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export const everyBeszelIntegrationFailed = (results: readonly unknown[]) =>
|
||||
results.length > 0 &&
|
||||
results.every(
|
||||
(result) => typeof result === "object" && result !== null && "error" in result && result.error !== undefined,
|
||||
);
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
import { everyBeszelIntegrationFailed } from "./beszel-failures";
|
||||
import { BoundedAsyncQueue } from "./bounded-async-queue";
|
||||
|
||||
const logger = createLogger({ module: "beszelRouter" });
|
||||
@@ -21,20 +20,6 @@ const MAX_PENDING_LIVE_EVENTS = 4;
|
||||
|
||||
const errorMessage = (error: unknown) => (error instanceof Error ? error.message : String(error));
|
||||
|
||||
function throwIfEveryBeszelIntegrationFailed(results: readonly unknown[], operation: string) {
|
||||
if (everyBeszelIntegrationFailed(results)) {
|
||||
const firstError = results[0];
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Every Beszel integration failed while loading ${operation}: ${
|
||||
typeof firstError === "object" && firstError !== null && "error" in firstError
|
||||
? String(firstError.error)
|
||||
: "unknown error"
|
||||
}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const beszelRouter = createTRPCRouter({
|
||||
getSystems: publicProcedure
|
||||
.meta({
|
||||
@@ -70,6 +55,7 @@ export const beszelRouter = createTRPCRouter({
|
||||
updatedAt: new Date(0),
|
||||
error: errorMessage(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
logger.debug("getSystems completed", {
|
||||
@@ -78,7 +64,6 @@ export const beszelRouter = createTRPCRouter({
|
||||
resultCount: results.length,
|
||||
errorCount: results.filter((r) => "error" in r).length,
|
||||
});
|
||||
throwIfEveryBeszelIntegrationFailed(results, "systems");
|
||||
return results;
|
||||
}),
|
||||
|
||||
@@ -144,6 +129,7 @@ export const beszelRouter = createTRPCRouter({
|
||||
updatedAt: new Date(0),
|
||||
error: errorMessage(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
logger.debug("getAlerts completed", {
|
||||
@@ -152,7 +138,6 @@ export const beszelRouter = createTRPCRouter({
|
||||
resultCount: results.length,
|
||||
errorCount: results.filter((r) => "error" in r).length,
|
||||
});
|
||||
throwIfEveryBeszelIntegrationFailed(results, "alerts");
|
||||
return results;
|
||||
}),
|
||||
|
||||
|
||||
@@ -27,19 +27,35 @@ export const calendarRouter = createTRPCRouter({
|
||||
)
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput);
|
||||
const { data } = await innerHandler.getDataAsync();
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { integrationIds: _integrationIds, ...handlerInput } = input;
|
||||
const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput);
|
||||
const { data } = await innerHandler.getDataAsync();
|
||||
|
||||
return {
|
||||
events: data,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
};
|
||||
});
|
||||
return {
|
||||
events: data,
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
events: [],
|
||||
integration: {
|
||||
id: integration.id,
|
||||
name: integration.name,
|
||||
kind: integration.kind,
|
||||
},
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { MediaRequest, MediaRequestStats } from "@homarr/integrations/types";
|
||||
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||
import {
|
||||
mediaRequestListInputSchema,
|
||||
@@ -13,6 +15,12 @@ import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from
|
||||
import { settleIntegrationQueries } from "../../settle-integrations";
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
interface IntegrationQueryResult<TData> {
|
||||
integration: { id: string; name: string; kind: IntegrationKindByCategory<"mediaRequest"> };
|
||||
data: TData;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const mediaRequestsRouter = createTRPCRouter({
|
||||
getLatestRequests: publicProcedure
|
||||
.meta({
|
||||
@@ -25,11 +33,26 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.input(mediaRequestListInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const results = await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { data } = await mediaRequestListRequestHandler.handler(integration, input).getDataAsync();
|
||||
return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data };
|
||||
});
|
||||
return results
|
||||
const results: IntegrationQueryResult<MediaRequest[]>[] = await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { data } = await mediaRequestListRequestHandler.handler(integration, input).getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
const requests = results
|
||||
.flatMap(({ data, integration }) =>
|
||||
data.map((request) => ({
|
||||
...request,
|
||||
@@ -47,6 +70,12 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
mediaRequestStatusConfiguration[dataB.status].position
|
||||
);
|
||||
});
|
||||
return {
|
||||
requests,
|
||||
failedIntegrations: results.flatMap(({ integration, error }) =>
|
||||
error ? [{ integrationId: integration.id, integrationName: integration.name, error }] : [],
|
||||
),
|
||||
};
|
||||
}),
|
||||
getStats: publicProcedure
|
||||
.meta({
|
||||
@@ -58,12 +87,30 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
})
|
||||
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||
.query(async ({ ctx }) => {
|
||||
const results = await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { data } = await mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return { integration: { id: integration.id, name: integration.name, kind: integration.kind }, data };
|
||||
});
|
||||
const results: IntegrationQueryResult<{
|
||||
stats: MediaRequestStats["stats"] | null;
|
||||
users: MediaRequestStats["users"];
|
||||
}>[] = await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { data } = await mediaRequestStatsRequestHandler.handler(integration, {}).getDataAsync();
|
||||
return {
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data: { ...data, stats: data.stats as typeof data.stats | null },
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integration: { id: integration.id, name: integration.name, kind: integration.kind },
|
||||
data: { stats: null, users: [] },
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
return {
|
||||
stats: results.flatMap((result) => result.data.stats),
|
||||
stats: results.flatMap((result) => (result.data.stats ? [result.data.stats] : [])),
|
||||
users: results
|
||||
.map((result) =>
|
||||
result.data.users.map((user) => ({
|
||||
@@ -74,6 +121,9 @@ export const mediaRequestsRouter = createTRPCRouter({
|
||||
.flat()
|
||||
.toSorted(({ requestCount: countA }, { requestCount: countB }) => countB - countA),
|
||||
integrations: results.map((result) => result.integration),
|
||||
failedIntegrations: results.flatMap(({ integration, error }) =>
|
||||
error ? [{ integrationId: integration.id, integrationName: integration.name, error }] : [],
|
||||
),
|
||||
};
|
||||
}),
|
||||
answerRequest: protectedProcedure
|
||||
|
||||
@@ -23,16 +23,30 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
.concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await settleIntegrationQueries(ctx.integrations, async (integration) => {
|
||||
const { data } = await mediaServerRequestHandler
|
||||
.handler(integration, { showOnlyPlaying: input.showOnlyPlaying })
|
||||
.getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationKind: integration.kind,
|
||||
sessions: data,
|
||||
};
|
||||
});
|
||||
return await settleIntegrationQueries(
|
||||
ctx.integrations,
|
||||
async (integration) => {
|
||||
const { data } = await mediaServerRequestHandler
|
||||
.handler(integration, { showOnlyPlaying: input.showOnlyPlaying })
|
||||
.getDataAsync();
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationKind: integration.kind,
|
||||
sessions: data,
|
||||
error: undefined as string | undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
fallback: (integration, error) => ({
|
||||
integrationId: integration.id,
|
||||
integrationName: integration.name,
|
||||
integrationKind: integration.kind,
|
||||
sessions: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
throwOnAllFailures: true,
|
||||
},
|
||||
);
|
||||
}),
|
||||
});
|
||||
Reference in new issue
Block a user