322 lines
10 KiB
TypeScript
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,
|
|
};
|
|
}
|