import React, { useMemo } from 'react';
import '@shared/styles/theme.css';
import { Badge, Expandable } from '@shared/components';
import { useToolData } from '@shared/hooks/useToolData';
import type { ValidationSummaryData, ValidationError, ValidationWarning } from '@shared/types';
interface NodeGroup {
node: string;
errors: ValidationError[];
warnings: ValidationWarning[];
}
function SeverityBar({ errorCount, warningCount }: { errorCount: number; warningCount: number }) {
const total = errorCount + warningCount;
if (total === 0) {
return (
);
}
const errorPct = (errorCount / total) * 100;
const warningPct = (warningCount / total) * 100;
return (
{errorCount > 0 && (
)}
{warningCount > 0 && (
)}
{errorCount} error{errorCount !== 1 ? 's' : ''}
{' · '}
{warningCount} warning{warningCount !== 1 ? 's' : ''}
);
}
function IssueItem({ issue, variant }: { issue: ValidationError | ValidationWarning; variant: 'error' | 'warning' }) {
const color = variant === 'error' ? 'var(--n8n-error)' : 'var(--n8n-warning)';
const fix = 'fix' in issue ? issue.fix : undefined;
return (
{issue.message}
{issue.property && (
{issue.property}
)}
{fix && (
→ {fix}
)}
);
}
function NodeGroupSection({ group }: { group: NodeGroup }) {
const errCount = group.errors.length;
const warnCount = group.warnings.length;
return (
0}
>
{errCount > 0 && {errCount} error{errCount !== 1 ? 's' : ''}}
{warnCount > 0 && {warnCount} warning{warnCount !== 1 ? 's' : ''}}
{group.errors.map((err, i) => (
))}
{group.warnings.map((warn, i) => (
))}
);
}
export default function App() {
const { data: raw, error, isConnected } = useToolData();
const inner = raw?.data || raw;
const errors: ValidationError[] = inner?.errors || raw?.errors || [];
const warnings: ValidationWarning[] = inner?.warnings || raw?.warnings || [];
const nodeGroups = useMemo(() => {
if (errors.length === 0 && warnings.length === 0) return null;
const hasNodes = errors.some((e) => e.node) || warnings.some((w) => w.node);
const uniqueNodes = new Set([
...errors.filter((e) => e.node).map((e) => e.node!),
...warnings.filter((w) => w.node).map((w) => w.node!),
]);
if (!hasNodes || uniqueNodes.size <= 1) return null;
const groups: NodeGroup[] = [];
for (const node of uniqueNodes) {
groups.push({
node,
errors: errors.filter((e) => e.node === node),
warnings: warnings.filter((w) => w.node === node),
});
}
// Ungrouped items
const ungroupedErrors = errors.filter((e) => !e.node);
const ungroupedWarnings = warnings.filter((w) => !w.node);
if (ungroupedErrors.length > 0 || ungroupedWarnings.length > 0) {
groups.push({ node: 'General', errors: ungroupedErrors, warnings: ungroupedWarnings });
}
// Sort: most issues first
groups.sort((a, b) => (b.errors.length + b.warnings.length) - (a.errors.length + a.warnings.length));
return groups;
}, [errors, warnings]);
if (error) {
return Error: {error}
;
}
if (!isConnected) {
return Connecting...
;
}
if (!raw) {
return Waiting for data...
;
}
const valid = inner.valid ?? raw.valid ?? false;
const displayName = raw.displayName || raw.data?.workflowName;
const suggestions: string[] = inner?.suggestions || raw?.suggestions || [];
const errorCount = raw.summary?.errorCount ?? inner?.summary?.errorCount ?? errors.length;
const warningCount = raw.summary?.warningCount ?? inner?.summary?.warningCount ?? warnings.length;
return (
{valid ? 'Valid' : 'Invalid'}
{displayName && (
{displayName}
)}
{nodeGroups ? (
nodeGroups.map((group) => (
))
) : (
<>
{errors.length > 0 && (
{errors.map((err, i) => (
))}
)}
{warnings.length > 0 && (
{warnings.map((warn, i) => (
))}
)}
>
)}
{suggestions.length > 0 && (
{suggestions.map((suggestion, i) => (
- → {suggestion}
))}
)}
);
}