frontend_nyla/src/hooks/useIntegrationsOverview.ts
2026-04-12 18:32:16 +02:00

322 lines
10 KiB
TypeScript

/**
* Aggregates data for the Integrations architecture page.
* Primary payload: GET /api/system/integrations-overview (no fictitious diagram data).
*/
import { useState, useEffect, useCallback } from 'react';
import api from '../api';
import type { FeatureInstance, NavigationMandate } from './useNavigation';
export interface AicoreModuleRow {
connectorType: string;
label: string;
modelCount: number;
}
export interface InfraToolRow {
id: string;
label: string;
}
export type DataLayerItemKind =
| 'userConnection'
| 'dataSource'
| 'featureDataSource'
| 'trusteeAccounting';
export interface DataLayerItem {
kind: DataLayerItemKind;
id: string;
/** userConnection */
displayLabel?: string;
connectionReference?: string;
authority?: string;
/** dataSource */
label?: string;
sourceType?: string;
connectionId?: string;
/** shared */
featureInstanceId?: string | null;
mandateId?: string | null;
/** featureDataSource */
featureCode?: string;
tableName?: string;
/** trusteeAccounting */
instanceLabel?: string;
connectorType?: string;
}
export interface LiveStats {
aiCallCount: number;
aiCallPeriodDays: number;
totalWorkflows: number;
activeWorkflows: number;
totalRuns: number;
totalTokens: number;
}
export interface ExtractorClassRow {
className: string;
extensions: string[];
}
export interface RendererClassRow {
className: string;
formats: string[];
}
export interface IntegrationsDiagramPayload {
aicoreModules: AicoreModuleRow[];
infraTools: InfraToolRow[];
extractorExtensions: string[];
extractorClasses: ExtractorClassRow[];
rendererFormats: string[];
rendererClasses: RendererClassRow[];
dataLayerItems: DataLayerItem[];
liveStats: LiveStats;
errors?: string[];
}
export interface MandateCardData {
id: string;
uiLabel: string;
dotColor: string;
/** "Feature: Instanzbezeichnung" per instance */
moduleChips: string[];
}
export interface UseIntegrationsOverviewResult {
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
diagram: IntegrationsDiagramPayload | null;
mandateCards: MandateCardData[];
workflowChips: string[];
hasNeutralization: boolean;
}
function _dotColorForIndex(index: number): string {
const palette = ['#378ADD', '#1D9E75', '#D85A30', '#8B5CF6', '#EC4899', '#0EA5E9'];
return palette[index % palette.length];
}
function _collectGraphicalEditorInstanceIds(mandates: NavigationMandate[]): string[] {
const ids: string[] = [];
for (const mandate of mandates) {
for (const feature of mandate.features) {
if (feature.uiComponent === 'feature.graphicalEditor') {
for (const inst of feature.instances) {
if (inst.id && !ids.includes(inst.id)) {
ids.push(inst.id);
}
}
}
}
}
return ids;
}
function _hasFeatureCode(mandates: NavigationMandate[], code: string): boolean {
for (const mandate of mandates) {
for (const feature of mandate.features) {
if (feature.uiComponent === `feature.${code}`) {
return true;
}
}
}
return false;
}
function _featureCodeFromUiComponent(uiComponent: string): string {
return uiComponent.startsWith('feature.') ? uiComponent.slice(8) : uiComponent;
}
function _instanceChipLine(inst: FeatureInstance, featureUiComponent: string): string {
const label = (inst.uiLabel || '').trim();
const code = (inst.featureCode || _featureCodeFromUiComponent(featureUiComponent)).trim();
if (label && code) {
return `${label} (${code})`;
}
if (label) {
return label;
}
return code;
}
function _buildMandateCards(mandates: NavigationMandate[]): MandateCardData[] {
return mandates.map((m, i) => {
const moduleChips: string[] = [];
for (const f of m.features) {
for (const inst of f.instances) {
const line = _instanceChipLine(inst, f.uiComponent);
if (line && !moduleChips.includes(line)) {
moduleChips.push(line);
}
}
}
return {
id: m.id,
uiLabel: m.uiLabel,
dotColor: _dotColorForIndex(i),
moduleChips: moduleChips.slice(0, 24),
};
});
}
const _DEFAULT_LIVE_STATS: LiveStats = {
aiCallCount: 0,
aiCallPeriodDays: 30,
totalWorkflows: 0,
activeWorkflows: 0,
totalRuns: 0,
totalTokens: 0,
};
function _normalizeExtractorClasses(raw: unknown): ExtractorClassRow[] {
if (!Array.isArray(raw)) return [];
const out: ExtractorClassRow[] = [];
for (const row of raw) {
if (!row || typeof row !== 'object') continue;
const r = row as Record<string, unknown>;
const className = typeof r.className === 'string' ? r.className : '';
const extensions = Array.isArray(r.extensions) ? (r.extensions as string[]) : [];
if (className && extensions.length) out.push({ className, extensions });
}
return out;
}
function _normalizeRendererClasses(raw: unknown): RendererClassRow[] {
if (!Array.isArray(raw)) return [];
const out: RendererClassRow[] = [];
for (const row of raw) {
if (!row || typeof row !== 'object') continue;
const r = row as Record<string, unknown>;
const className = typeof r.className === 'string' ? r.className : '';
const formats = Array.isArray(r.formats) ? (r.formats as string[]) : [];
if (className && formats.length) out.push({ className, formats });
}
return out;
}
function _normalizeDiagramPayload(raw: unknown): IntegrationsDiagramPayload {
const o = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {};
const rawStats = o.liveStats && typeof o.liveStats === 'object'
? (o.liveStats as Record<string, unknown>)
: {};
return {
aicoreModules: Array.isArray(o.aicoreModules) ? (o.aicoreModules as AicoreModuleRow[]) : [],
infraTools: Array.isArray(o.infraTools) ? (o.infraTools as InfraToolRow[]) : [],
extractorExtensions: Array.isArray(o.extractorExtensions)
? (o.extractorExtensions as string[])
: [],
extractorClasses: _normalizeExtractorClasses(o.extractorClasses),
rendererFormats: Array.isArray(o.rendererFormats) ? (o.rendererFormats as string[]) : [],
rendererClasses: _normalizeRendererClasses(o.rendererClasses),
dataLayerItems: Array.isArray(o.dataLayerItems) ? (o.dataLayerItems as DataLayerItem[]) : [],
liveStats: {
aiCallCount: typeof rawStats.aiCallCount === 'number' ? rawStats.aiCallCount : _DEFAULT_LIVE_STATS.aiCallCount,
aiCallPeriodDays: typeof rawStats.aiCallPeriodDays === 'number' ? rawStats.aiCallPeriodDays : _DEFAULT_LIVE_STATS.aiCallPeriodDays,
totalWorkflows: typeof rawStats.totalWorkflows === 'number' ? rawStats.totalWorkflows : _DEFAULT_LIVE_STATS.totalWorkflows,
activeWorkflows: typeof rawStats.activeWorkflows === 'number' ? rawStats.activeWorkflows : _DEFAULT_LIVE_STATS.activeWorkflows,
totalRuns: typeof rawStats.totalRuns === 'number' ? rawStats.totalRuns : _DEFAULT_LIVE_STATS.totalRuns,
totalTokens: typeof rawStats.totalTokens === 'number' ? rawStats.totalTokens : _DEFAULT_LIVE_STATS.totalTokens,
},
errors: Array.isArray(o.errors) ? (o.errors as string[]) : undefined,
};
}
export function useIntegrationsOverview(): UseIntegrationsOverviewResult {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [diagram, setDiagram] = useState<IntegrationsDiagramPayload | null>(null);
const [mandateCards, setMandateCards] = useState<MandateCardData[]>([]);
const [workflowChips, setWorkflowChips] = useState<string[]>([]);
const [hasNeutralization, setHasNeutralization] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [navResult, diagramResult] = await Promise.allSettled([
api.get('/api/navigation'),
api.get('/api/system/integrations-overview'),
]);
let mandatesForWorkflows: NavigationMandate[] = [];
if (navResult.status === 'fulfilled') {
const blocks = navResult.value.data?.blocks ?? [];
const dynamicBlock = blocks.find((b: { type: string }) => b.type === 'dynamic');
mandatesForWorkflows = dynamicBlock?.mandates ?? [];
setMandateCards(_buildMandateCards(mandatesForWorkflows));
setHasNeutralization(_hasFeatureCode(mandatesForWorkflows, 'neutralization'));
} else {
setMandateCards([]);
setHasNeutralization(false);
setError(
navResult.reason instanceof Error
? navResult.reason.message
: String(navResult.reason),
);
}
if (diagramResult.status === 'fulfilled') {
setDiagram(_normalizeDiagramPayload(diagramResult.value.data));
} else {
setDiagram(_normalizeDiagramPayload({}));
const msg =
diagramResult.reason instanceof Error
? diagramResult.reason.message
: String(diagramResult.reason);
setError((prev) => (prev ? `${prev} | ${msg}` : msg));
}
const geIds = _collectGraphicalEditorInstanceIds(mandatesForWorkflows);
const wfLabels: string[] = [];
const seenWf = new Set<string>();
for (const instanceId of geIds.slice(0, 4)) {
try {
const wfRes = await api.get(`/api/workflows/${instanceId}/workflows`, {
params: { active: 'true' },
});
const wfData = wfRes.data;
const list = Array.isArray(wfData)
? wfData
: (wfData as { items?: { label?: string }[]; workflows?: { label?: string }[] })?.items ??
(wfData as { workflows?: { label?: string }[] })?.workflows ??
[];
for (const w of list) {
const lab = (w as { label?: string }).label;
if (lab && !seenWf.has(lab)) {
seenWf.add(lab);
wfLabels.push(lab);
}
if (wfLabels.length >= 8) break;
}
} catch {
/* ignore */
}
if (wfLabels.length >= 8) break;
}
setWorkflowChips(wfLabels.slice(0, 8));
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Laden fehlgeschlagen');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
return {
loading,
error,
refetch: load,
diagram,
mandateCards,
workflowChips,
hasNeutralization,
};
}