mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-03 09:03:07 +00:00
feat: UI/UX redesign for MCP Apps - 3 new apps + enhanced existing (#583)
Add workflow-list, execution-history, and health-dashboard apps. Redesign operation-result with operation-aware headers, detail panels, and copy-to-clipboard. Fix React hooks violations in validation-summary and execution-history (useMemo after early returns). Add local preview harness for development. Update tests for 5-app config. Conceived by Romuald Członkowski - www.aiadvisors.pl/en Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
c601581714
commit
89146186d8
@@ -1,107 +1,207 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import '@shared/styles/theme.css';
|
||||
import { Card, Badge, Expandable } from '@shared/components';
|
||||
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 (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
height: '6px',
|
||||
borderRadius: '3px',
|
||||
background: 'var(--n8n-success)',
|
||||
marginBottom: '6px',
|
||||
}} />
|
||||
<div style={{ fontSize: '12px', color: 'var(--n8n-success)', fontWeight: 500 }}>
|
||||
All checks passed
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const errorPct = (errorCount / total) * 100;
|
||||
const warningPct = (warningCount / total) * 100;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<div style={{
|
||||
height: '6px',
|
||||
borderRadius: '3px',
|
||||
background: 'var(--n8n-border)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
}}>
|
||||
{errorCount > 0 && (
|
||||
<div style={{ width: `${errorPct}%`, background: 'var(--n8n-error)', minWidth: '4px' }} />
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<div style={{ width: `${warningPct}%`, background: 'var(--n8n-warning)', minWidth: '4px' }} />
|
||||
)}
|
||||
</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--color-text-secondary, var(--n8n-text-muted))', marginTop: '6px' }}>
|
||||
<span style={{ color: 'var(--n8n-error)', fontWeight: 500 }}>{errorCount}</span> error{errorCount !== 1 ? 's' : ''}
|
||||
{' · '}
|
||||
<span style={{ color: 'var(--n8n-warning)', fontWeight: 500 }}>{warningCount}</span> warning{warningCount !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div style={{
|
||||
padding: '6px 10px',
|
||||
marginBottom: '4px',
|
||||
borderLeft: `3px solid ${color}`,
|
||||
fontSize: '12px',
|
||||
}}>
|
||||
<div style={{ color: 'var(--color-text-primary, var(--n8n-text))' }}>{issue.message}</div>
|
||||
{issue.property && (
|
||||
<div style={{ color: 'var(--color-text-secondary, var(--n8n-text-muted))', fontSize: '11px', marginTop: '2px' }}>
|
||||
{issue.property}
|
||||
</div>
|
||||
)}
|
||||
{fix && (
|
||||
<div style={{ color, fontSize: '11px', marginTop: '2px' }}>
|
||||
→ {fix}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeGroupSection({ group }: { group: NodeGroup }) {
|
||||
const errCount = group.errors.length;
|
||||
const warnCount = group.warnings.length;
|
||||
|
||||
return (
|
||||
<Expandable
|
||||
title={group.node}
|
||||
count={errCount + warnCount}
|
||||
defaultOpen={errCount > 0}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: '8px', marginBottom: '6px', flexWrap: 'wrap' }}>
|
||||
{errCount > 0 && <Badge variant="error">{errCount} error{errCount !== 1 ? 's' : ''}</Badge>}
|
||||
{warnCount > 0 && <Badge variant="warning">{warnCount} warning{warnCount !== 1 ? 's' : ''}</Badge>}
|
||||
</div>
|
||||
{group.errors.map((err, i) => (
|
||||
<IssueItem key={`e-${i}`} issue={err} variant="error" />
|
||||
))}
|
||||
{group.warnings.map((warn, i) => (
|
||||
<IssueItem key={`w-${i}`} issue={warn} variant="warning" />
|
||||
))}
|
||||
</Expandable>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { data: raw, error, isConnected } = useToolData<ValidationSummaryData>();
|
||||
|
||||
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 <div style={{ padding: '16px', color: '#ef4444' }}>Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (!isConnected) {
|
||||
return <div style={{ padding: '16px', color: '#9ca3af' }}>Connecting...</div>;
|
||||
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Connecting...</div>;
|
||||
}
|
||||
|
||||
if (!raw) {
|
||||
return <div style={{ padding: '16px', color: '#9ca3af' }}>Waiting for data...</div>;
|
||||
return <div style={{ padding: '16px', color: 'var(--n8n-text-muted)' }}>Waiting for data...</div>;
|
||||
}
|
||||
|
||||
// n8n_validate_workflow wraps result in { success, data: {...} }
|
||||
// validate_node and validate_workflow return data directly
|
||||
const inner = raw.data || raw;
|
||||
const valid = inner.valid ?? raw.valid ?? false;
|
||||
const displayName = raw.displayName || raw.data?.workflowName;
|
||||
const errors: ValidationError[] = inner.errors || raw.errors || [];
|
||||
const warnings: ValidationWarning[] = inner.warnings || raw.warnings || [];
|
||||
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;
|
||||
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 (
|
||||
<div style={{ maxWidth: '480px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
||||
<Badge variant={valid ? 'success' : 'error'}>
|
||||
{valid ? 'Valid' : 'Invalid'}
|
||||
</Badge>
|
||||
{displayName && (
|
||||
<span style={{ fontSize: '14px', color: 'var(--n8n-text-muted)' }}>{displayName}</span>
|
||||
<span style={{ fontSize: '14px', color: 'var(--color-text-secondary, var(--n8n-text-muted))' }}>{displayName}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div style={{ display: 'flex', gap: '16px', fontSize: '13px' }}>
|
||||
<div>
|
||||
<span style={{ color: 'var(--n8n-error)' }}>{errorCount}</span> errors
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ color: 'var(--n8n-warning)' }}>{warningCount}</span> warnings
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<SeverityBar errorCount={errorCount} warningCount={warningCount} />
|
||||
|
||||
{errors.length > 0 && (
|
||||
<Expandable title="Errors" count={errors.length} defaultOpen>
|
||||
{errors.map((err, i) => (
|
||||
<div key={i} style={{
|
||||
padding: '8px',
|
||||
marginBottom: '6px',
|
||||
background: 'var(--n8n-error-light)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: 'var(--n8n-error)',
|
||||
}}>
|
||||
{(err.type || err.node) && (
|
||||
<div style={{ fontWeight: 600 }}>{err.type || err.node}</div>
|
||||
)}
|
||||
{err.property && <div style={{ opacity: 0.8 }}>Property: {err.property}</div>}
|
||||
<div>{err.message}</div>
|
||||
{err.fix && (
|
||||
<div style={{ marginTop: '4px', fontStyle: 'italic', opacity: 0.9 }}>Fix: {err.fix}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Expandable>
|
||||
)}
|
||||
{nodeGroups ? (
|
||||
nodeGroups.map((group) => (
|
||||
<NodeGroupSection key={group.node} group={group} />
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{errors.length > 0 && (
|
||||
<Expandable title="Errors" count={errors.length} defaultOpen>
|
||||
{errors.map((err, i) => (
|
||||
<IssueItem key={i} issue={err} variant="error" />
|
||||
))}
|
||||
</Expandable>
|
||||
)}
|
||||
|
||||
{warnings.length > 0 && (
|
||||
<Expandable title="Warnings" count={warnings.length}>
|
||||
{warnings.map((warn, i) => (
|
||||
<div key={i} style={{
|
||||
padding: '8px',
|
||||
marginBottom: '6px',
|
||||
background: 'var(--n8n-warning-light)',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: 'var(--n8n-warning)',
|
||||
}}>
|
||||
{(warn.type || warn.node) && (
|
||||
<div style={{ fontWeight: 600 }}>{warn.type || warn.node}</div>
|
||||
)}
|
||||
{warn.property && <div style={{ opacity: 0.8 }}>Property: {warn.property}</div>}
|
||||
<div>{warn.message}</div>
|
||||
</div>
|
||||
))}
|
||||
</Expandable>
|
||||
{warnings.length > 0 && (
|
||||
<Expandable title="Warnings" count={warnings.length}>
|
||||
{warnings.map((warn, i) => (
|
||||
<IssueItem key={i} issue={warn} variant="warning" />
|
||||
))}
|
||||
</Expandable>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<Expandable title="Suggestions" count={suggestions.length}>
|
||||
<ul style={{ paddingLeft: '16px', fontSize: '12px' }}>
|
||||
{suggestions.map((suggestion, i) => (
|
||||
<li key={i} style={{ padding: '2px 0', color: 'var(--n8n-info)' }}>{suggestion}</li>
|
||||
<li key={i} style={{ padding: '2px 0', color: 'var(--n8n-info)' }}>→ {suggestion}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Expandable>
|
||||
|
||||
Reference in New Issue
Block a user