"use client"; import { Box, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core"; import { clientApi } from "@homarr/api/client"; import type { TracearrDashboardData } from "@homarr/integrations/types"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { IntegrationErrorIndicator } from "../common/integration-error-indicator"; import { getUsableWidgetQueryData } from "../common/query-state"; import { NoIntegrationDataError } from "../errors/no-data-integration"; import { formatTotalTracearrBitrate } from "./bitrate"; import { RecentActivityList } from "./recent-activity-section"; import { attachTracearrSource } from "./source"; import { StatsBar } from "./stats-section"; import { StreamsList } from "./streams-section"; import { ViolationsList } from "./violations-section"; const ADVANCED_GRID_BREAKPOINT = 800; const ADVANCED_GRID_GAP_PX = 16; const ADVANCED_GRID_PADDING_PX = 16; type TracearrSectionOptions = Pick< WidgetComponentProps<"tracearr">["options"], "showStats" | "showStreams" | "showViolations" | "showRecentActivity" >; export const getTracearrSectionVisibility = (options: TracearrSectionOptions, isAdvanced: boolean) => ({ stats: isAdvanced || options.showStats, streams: isAdvanced || options.showStreams, violations: isAdvanced || options.showViolations, recentActivity: isAdvanced || options.showRecentActivity, }); export default function TracearrWidget({ options, integrationIds, width, displayMode = "compact", }: WidgetComponentProps<"tracearr">) { if (integrationIds.length === 0) { throw new NoIntegrationDataError(); } return ; } interface TracearrContentProps { integrationIds: string[]; options: WidgetComponentProps<"tracearr">["options"]; width: number; displayMode: "compact" | "advanced"; } function TracearrContent({ integrationIds, options, width, displayMode }: TracearrContentProps) { const t = useI18n("widget.tracearr"); const dashboardQuery = clientApi.widget.tracearr.getDashboard.useQuery({ integrationIds }); const dashboardData = getUsableWidgetQueryData(dashboardQuery) ?? []; const successfulDashboards = dashboardData.flatMap((item) => item.dashboard === null ? [] : [{ ...item, dashboard: item.dashboard }], ); // Merge data from all integrations const combinedData = successfulDashboards.reduce( (acc, { dashboard }) => { const { stats, streams, violations, recentActivity } = dashboard; const vData = violations ?? { data: [], meta: { total: 0, page: 1, pageSize: 5 } }; const aData = recentActivity ?? { data: [], meta: { total: 0, page: 1, pageSize: 5 } }; return { stats: { activeStreams: acc.stats.activeStreams + stats.activeStreams, totalUsers: acc.stats.totalUsers + stats.totalUsers, totalSessions: acc.stats.totalSessions + stats.totalSessions, recentViolations: acc.stats.recentViolations + stats.recentViolations, timestamp: stats.timestamp, }, streams: { data: [...acc.streams.data, ...streams.data], summary: { total: acc.streams.summary.total + streams.summary.total, transcodes: acc.streams.summary.transcodes + streams.summary.transcodes, directStreams: acc.streams.summary.directStreams + streams.summary.directStreams, directPlays: acc.streams.summary.directPlays + streams.summary.directPlays, totalBitrate: "—", byServer: [...acc.streams.summary.byServer, ...streams.summary.byServer], }, }, violations: { data: [...(acc.violations?.data ?? []), ...vData.data], meta: { total: (acc.violations?.meta.total ?? 0) + vData.meta.total, page: 1, pageSize: 5, }, }, recentActivity: { data: [...(acc.recentActivity?.data ?? []), ...aData.data], meta: { total: (acc.recentActivity?.meta.total ?? 0) + aData.meta.total, page: 1, pageSize: 5, }, }, }; }, { stats: { activeStreams: 0, totalUsers: 0, totalSessions: 0, recentViolations: 0, timestamp: "" }, streams: { data: [], summary: { total: 0, transcodes: 0, directStreams: 0, directPlays: 0, totalBitrate: "0", byServer: [] }, }, violations: null, recentActivity: null, }, ); const combined: TracearrDashboardData = { ...combinedData, streams: { ...combinedData.streams, summary: { ...combinedData.streams.summary, totalBitrate: formatTotalTracearrBitrate(combinedData.streams.data), }, }, }; const isAdvanced = displayMode === "advanced"; const sectionVisibility = getTracearrSectionVisibility(options, isAdvanced); const noSectionsEnabled = Object.values(sectionVisibility).every((isVisible) => !isVisible); const sourcedStreams = successfulDashboards.flatMap(({ integrationId, integrationName, dashboard }) => attachTracearrSource(dashboard.streams.data, { integrationId, integrationName }), ); const sourcedViolations = successfulDashboards.flatMap(({ integrationId, integrationName, dashboard }) => attachTracearrSource(dashboard.violations?.data ?? [], { integrationId, integrationName }), ); const sourcedRecentActivity = successfulDashboards.flatMap(({ integrationId, integrationName, dashboard }) => attachTracearrSource(dashboard.recentActivity?.data ?? [], { integrationId, integrationName }), ); const showSource = isAdvanced || dashboardData.length > 1; if (isAdvanced) { const isTwoColumn = width >= ADVANCED_GRID_BREAKPOINT; const columnWidth = getAdvancedColumnWidth(width, isTwoColumn); return ( {successfulDashboards.map(({ integrationName }) => integrationName).join(" · ")} {sectionVisibility.stats && ( )} {sectionVisibility.streams && ( )} {sectionVisibility.violations && ( )} {sectionVisibility.recentActivity && ( )} ); } return ( {showSource && ( {successfulDashboards.map(({ integrationName }) => integrationName).join(" · ")} )} {sectionVisibility.stats && ( )} {sectionVisibility.streams && } {sectionVisibility.violations && } {sectionVisibility.recentActivity && ( )} {noSectionsEnabled && ( {t("noSectionsEnabled")} )} ); } export const getAdvancedColumnWidth = (width: number, isTwoColumn: boolean) => { const contentWidth = Math.max(0, width - ADVANCED_GRID_PADDING_PX * 2); return isTwoColumn ? Math.max(0, (contentWidth - ADVANCED_GRID_GAP_PX) / 2) : contentWidth; };