feat(ui): add taps DataTable, CRUD modal with test expression to RouteDetail
- Replace taps tab placeholder with full DataTable showing all route taps - Add columns: attribute, processor, expression, language, target, type, enabled toggle, actions - Add tap modal with form fields: attribute name, processor select, language, target, expression, type selector - Implement inline enable/disable toggle per tap row - Add ConfirmDialog for tap deletion - Add test expression section with Recent Exchange and Custom Payload tabs - Add save/edit/delete tap operations via application config update - Add all supporting CSS module classes (no inline styles) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -326,3 +326,113 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tapActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tap modal */
|
||||||
|
.tapModalBody {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapFormRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapFormRow > * {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monoTextarea {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeSelector {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeOption {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeOptionActive {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tapModalFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test expression */
|
||||||
|
.testSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testTabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testTabBtn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testTabBtnActive {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testBody {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testResult {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testSuccess {
|
||||||
|
background: var(--bg-success-subtle, #0f2a1a);
|
||||||
|
border: 1px solid var(--border-success, #166534);
|
||||||
|
color: var(--text-success, #4ade80);
|
||||||
|
}
|
||||||
|
|
||||||
|
.testError {
|
||||||
|
background: var(--bg-error-subtle, #2a0f0f);
|
||||||
|
border: 1px solid var(--border-error, #991b1b);
|
||||||
|
color: var(--text-error, #f87171);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import {
|
|||||||
MonoText,
|
MonoText,
|
||||||
Sparkline,
|
Sparkline,
|
||||||
Toggle,
|
Toggle,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
FormField,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Collapsible,
|
||||||
|
ConfirmDialog,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import type { KpiItem, Column } from '@cameleer/design-system';
|
import type { KpiItem, Column } from '@cameleer/design-system';
|
||||||
import { useGlobalFilters } from '@cameleer/design-system';
|
import { useGlobalFilters } from '@cameleer/design-system';
|
||||||
@@ -21,7 +29,8 @@ import { useRouteCatalog } from '../../api/queries/catalog';
|
|||||||
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
import { useDiagramByRoute } from '../../api/queries/diagrams';
|
||||||
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
|
import { useProcessorMetrics } from '../../api/queries/processor-metrics';
|
||||||
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
import { useStatsTimeseries, useSearchExecutions, useExecutionStats } from '../../api/queries/executions';
|
||||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
import { useApplicationConfig, useUpdateApplicationConfig, useTestExpression } from '../../api/queries/commands';
|
||||||
|
import type { TapDefinition } from '../../api/queries/commands';
|
||||||
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
|
import type { ExecutionSummary, AppCatalogEntry, RouteSummary } from '../../api/types';
|
||||||
import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping';
|
import { mapDiagramToRouteNodes, toFlowSegments } from '../../utils/diagram-mapping';
|
||||||
import styles from './RouteDetail.module.css';
|
import styles from './RouteDetail.module.css';
|
||||||
@@ -265,6 +274,24 @@ export default function RouteDetail() {
|
|||||||
const [recentSortField, setRecentSortField] = useState<string>('startTime');
|
const [recentSortField, setRecentSortField] = useState<string>('startTime');
|
||||||
const [recentSortDir, setRecentSortDir] = useState<'asc' | 'desc'>('desc');
|
const [recentSortDir, setRecentSortDir] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
|
// ── Tap modal state ────────────────────────────────────────────────────────
|
||||||
|
const [tapModalOpen, setTapModalOpen] = useState(false);
|
||||||
|
const [editingTap, setEditingTap] = useState<TapDefinition | null>(null);
|
||||||
|
const [tapName, setTapName] = useState('');
|
||||||
|
const [tapProcessor, setTapProcessor] = useState('');
|
||||||
|
const [tapLanguage, setTapLanguage] = useState('simple');
|
||||||
|
const [tapTarget, setTapTarget] = useState<'INPUT' | 'OUTPUT' | 'BOTH'>('OUTPUT');
|
||||||
|
const [tapExpression, setTapExpression] = useState('');
|
||||||
|
const [tapType, setTapType] = useState<'BUSINESS_OBJECT' | 'CORRELATION' | 'EVENT' | 'CUSTOM'>('BUSINESS_OBJECT');
|
||||||
|
const [tapEnabled, setTapEnabled] = useState(true);
|
||||||
|
const [deletingTap, setDeletingTap] = useState<TapDefinition | null>(null);
|
||||||
|
|
||||||
|
// ── Test expression state ──────────────────────────────────────────────────
|
||||||
|
const [testTab, setTestTab] = useState('recent');
|
||||||
|
const [testPayload, setTestPayload] = useState('');
|
||||||
|
const [testResult, setTestResult] = useState<{ result?: string; error?: string } | null>(null);
|
||||||
|
const [testExchangeId, setTestExchangeId] = useState('');
|
||||||
|
|
||||||
const handleRecentSortChange = useCallback((key: string, dir: 'asc' | 'desc') => {
|
const handleRecentSortChange = useCallback((key: string, dir: 'asc' | 'desc') => {
|
||||||
setRecentSortField(key);
|
setRecentSortField(key);
|
||||||
setRecentSortDir(dir);
|
setRecentSortDir(dir);
|
||||||
@@ -299,6 +326,7 @@ export default function RouteDetail() {
|
|||||||
// ── Application config ──────────────────────────────────────────────────────
|
// ── Application config ──────────────────────────────────────────────────────
|
||||||
const config = useApplicationConfig(appId);
|
const config = useApplicationConfig(appId);
|
||||||
const updateConfig = useUpdateApplicationConfig();
|
const updateConfig = useUpdateApplicationConfig();
|
||||||
|
const testExpressionMutation = useTestExpression();
|
||||||
|
|
||||||
const isRecording = config.data?.routeRecording?.[routeId!] !== false;
|
const isRecording = config.data?.routeRecording?.[routeId!] !== false;
|
||||||
|
|
||||||
@@ -455,6 +483,171 @@ export default function RouteDetail() {
|
|||||||
{ label: 'Taps', value: 'taps', count: routeTaps.length },
|
{ label: 'Taps', value: 'taps', count: routeTaps.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Tap helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const processorOptions = useMemo(() => {
|
||||||
|
if (!diagram?.nodes) return [];
|
||||||
|
return (diagram.nodes as Array<{ id?: string; label?: string }>)
|
||||||
|
.filter((n) => n.id)
|
||||||
|
.map((n) => ({ value: n.id!, label: n.label || n.id! }));
|
||||||
|
}, [diagram]);
|
||||||
|
|
||||||
|
function openTapModal(tap: TapDefinition | null) {
|
||||||
|
if (tap) {
|
||||||
|
setEditingTap(tap);
|
||||||
|
setTapName(tap.attributeName);
|
||||||
|
setTapProcessor(tap.processorId);
|
||||||
|
setTapLanguage(tap.language);
|
||||||
|
setTapTarget(tap.target);
|
||||||
|
setTapExpression(tap.expression);
|
||||||
|
setTapType(tap.attributeType);
|
||||||
|
setTapEnabled(tap.enabled);
|
||||||
|
} else {
|
||||||
|
setEditingTap(null);
|
||||||
|
setTapName('');
|
||||||
|
setTapProcessor(processorOptions[0]?.value ?? '');
|
||||||
|
setTapLanguage('simple');
|
||||||
|
setTapTarget('OUTPUT');
|
||||||
|
setTapExpression('');
|
||||||
|
setTapType('BUSINESS_OBJECT');
|
||||||
|
setTapEnabled(true);
|
||||||
|
}
|
||||||
|
setTestResult(null);
|
||||||
|
setTestPayload('');
|
||||||
|
setTestExchangeId('');
|
||||||
|
setTapModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTap() {
|
||||||
|
if (!config.data) return;
|
||||||
|
const tap: TapDefinition = {
|
||||||
|
tapId: editingTap?.tapId || crypto.randomUUID(),
|
||||||
|
processorId: tapProcessor,
|
||||||
|
target: tapTarget,
|
||||||
|
expression: tapExpression,
|
||||||
|
language: tapLanguage,
|
||||||
|
attributeName: tapName,
|
||||||
|
attributeType: tapType,
|
||||||
|
enabled: tapEnabled,
|
||||||
|
version: editingTap ? editingTap.version + 1 : 1,
|
||||||
|
};
|
||||||
|
const taps = editingTap
|
||||||
|
? config.data.taps.map(t => t.tapId === editingTap.tapId ? tap : t)
|
||||||
|
: [...(config.data.taps || []), tap];
|
||||||
|
updateConfig.mutate({ ...config.data, taps });
|
||||||
|
setTapModalOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTap(tap: TapDefinition) {
|
||||||
|
if (!config.data) return;
|
||||||
|
const taps = config.data.taps.filter(t => t.tapId !== tap.tapId);
|
||||||
|
updateConfig.mutate({ ...config.data, taps });
|
||||||
|
setDeletingTap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTapEnabled(tap: TapDefinition) {
|
||||||
|
if (!config.data) return;
|
||||||
|
const taps = config.data.taps.map(t =>
|
||||||
|
t.tapId === tap.tapId ? { ...t, enabled: !t.enabled } : t,
|
||||||
|
);
|
||||||
|
updateConfig.mutate({ ...config.data, taps });
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTestExpression() {
|
||||||
|
if (!appId) return;
|
||||||
|
const body = testTab === 'recent' ? testExchangeId : testPayload;
|
||||||
|
testExpressionMutation.mutate(
|
||||||
|
{ application: appId, expression: tapExpression, language: tapLanguage, body, target: tapTarget },
|
||||||
|
{ onSuccess: (data) => setTestResult(data), onError: (err) => setTestResult({ error: (err as Error).message }) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tapColumns: Column<TapDefinition & { id: string }>[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
key: 'attributeName',
|
||||||
|
header: 'Attribute',
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => <span>{row.attributeName}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'processorId',
|
||||||
|
header: 'Processor',
|
||||||
|
sortable: true,
|
||||||
|
render: (_, row) => <MonoText size="xs">{row.processorId}</MonoText>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'expression',
|
||||||
|
header: 'Expression',
|
||||||
|
render: (_, row) => <MonoText size="xs">{row.expression}</MonoText>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'language',
|
||||||
|
header: 'Language',
|
||||||
|
render: (_, row) => <Badge label={row.language} color="auto" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'target',
|
||||||
|
header: 'Target',
|
||||||
|
render: (_, row) => <Badge label={row.target} color="auto" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'attributeType',
|
||||||
|
header: 'Type',
|
||||||
|
render: (_, row) => <Badge label={row.attributeType} color="auto" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'enabled',
|
||||||
|
header: 'Enabled',
|
||||||
|
width: '80px',
|
||||||
|
render: (_, row) => (
|
||||||
|
<Toggle
|
||||||
|
checked={row.enabled}
|
||||||
|
onChange={() => toggleTapEnabled(row)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'actions' as any,
|
||||||
|
header: '',
|
||||||
|
width: '100px',
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className={styles.tapActions}>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => openTapModal(row)}>Edit</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setDeletingTap(row)}>Del</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], [config.data, processorOptions]);
|
||||||
|
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'simple', label: 'Simple' },
|
||||||
|
{ value: 'jsonpath', label: 'JSONPath' },
|
||||||
|
{ value: 'xpath', label: 'XPath' },
|
||||||
|
{ value: 'jq', label: 'jq' },
|
||||||
|
{ value: 'groovy', label: 'Groovy' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const targetOptions = [
|
||||||
|
{ value: 'INPUT', label: 'Input' },
|
||||||
|
{ value: 'OUTPUT', label: 'Output' },
|
||||||
|
{ value: 'BOTH', label: 'Both' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const typeChoices: Array<{ value: TapDefinition['attributeType']; label: string }> = [
|
||||||
|
{ value: 'BUSINESS_OBJECT', label: 'Business Object' },
|
||||||
|
{ value: 'CORRELATION', label: 'Correlation' },
|
||||||
|
{ value: 'EVENT', label: 'Event' },
|
||||||
|
{ value: 'CUSTOM', label: 'Custom' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentExchangeOptions = useMemo(() =>
|
||||||
|
exchangeRows.slice(0, 20).map(e => ({
|
||||||
|
value: e.executionId,
|
||||||
|
label: `${e.executionId.slice(0, 12)} — ${e.status}`,
|
||||||
|
})),
|
||||||
|
[exchangeRows],
|
||||||
|
);
|
||||||
|
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -632,14 +825,168 @@ export default function RouteDetail() {
|
|||||||
|
|
||||||
{activeTab === 'taps' && (
|
{activeTab === 'taps' && (
|
||||||
<div className={styles.tapsSection}>
|
<div className={styles.tapsSection}>
|
||||||
<div className={styles.emptyState}>
|
<div className={styles.tapsHeader}>
|
||||||
{routeTaps.length === 0
|
<span className={styles.tapsTitle}>Data Extraction Taps</span>
|
||||||
? 'No taps configured for this route. Add a tap to extract business attributes from exchange data.'
|
<Button variant="primary" size="sm" onClick={() => openTapModal(null)}>+ Add Tap</Button>
|
||||||
: `${routeTaps.length} taps configured (${activeTapCount} active)`}
|
|
||||||
</div>
|
</div>
|
||||||
|
{routeTaps.length === 0 ? (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
No taps configured for this route. Add a tap to extract business attributes from exchange data.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DataTable
|
||||||
|
columns={tapColumns}
|
||||||
|
data={routeTaps.map(t => ({ ...t, id: t.tapId }))}
|
||||||
|
flush
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tap Modal */}
|
||||||
|
<Modal open={tapModalOpen} onClose={() => setTapModalOpen(false)} title={editingTap ? 'Edit Tap' : 'Add Tap'} size="lg">
|
||||||
|
<div className={styles.tapModalBody}>
|
||||||
|
<FormField label="Attribute Name">
|
||||||
|
<Input value={tapName} onChange={(e) => setTapName(e.target.value)} placeholder="e.g. orderId" />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Processor">
|
||||||
|
<Select
|
||||||
|
options={processorOptions}
|
||||||
|
value={tapProcessor}
|
||||||
|
onChange={(e) => setTapProcessor(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className={styles.tapFormRow}>
|
||||||
|
<FormField label="Language">
|
||||||
|
<Select
|
||||||
|
options={languageOptions}
|
||||||
|
value={tapLanguage}
|
||||||
|
onChange={(e) => setTapLanguage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
<FormField label="Target">
|
||||||
|
<Select
|
||||||
|
options={targetOptions}
|
||||||
|
value={tapTarget}
|
||||||
|
onChange={(e) => setTapTarget(e.target.value as 'INPUT' | 'OUTPUT' | 'BOTH')}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="Expression">
|
||||||
|
<Textarea
|
||||||
|
className={styles.monoTextarea}
|
||||||
|
value={tapExpression}
|
||||||
|
onChange={(e) => setTapExpression(e.target.value)}
|
||||||
|
placeholder="e.g. ${body.orderId}"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Type">
|
||||||
|
<div className={styles.typeSelector}>
|
||||||
|
{typeChoices.map(tc => (
|
||||||
|
<button
|
||||||
|
key={tc.value}
|
||||||
|
type="button"
|
||||||
|
className={`${styles.typeOption} ${tapType === tc.value ? styles.typeOptionActive : ''}`}
|
||||||
|
onClick={() => setTapType(tc.value)}
|
||||||
|
>
|
||||||
|
{tc.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<div className={styles.tapFormRow}>
|
||||||
|
<FormField label="Enabled">
|
||||||
|
<Toggle checked={tapEnabled} onChange={() => setTapEnabled(!tapEnabled)} />
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test Expression */}
|
||||||
|
<Collapsible title="Test Expression" defaultOpen>
|
||||||
|
<div className={styles.testSection}>
|
||||||
|
<div className={styles.testTabs}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.testTabBtn} ${testTab === 'recent' ? styles.testTabBtnActive : ''}`}
|
||||||
|
onClick={() => setTestTab('recent')}
|
||||||
|
>
|
||||||
|
Recent Exchange
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.testTabBtn} ${testTab === 'custom' ? styles.testTabBtnActive : ''}`}
|
||||||
|
onClick={() => setTestTab('custom')}
|
||||||
|
>
|
||||||
|
Custom Payload
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testTab === 'recent' && (
|
||||||
|
<div className={styles.testBody}>
|
||||||
|
<Select
|
||||||
|
options={recentExchangeOptions.length > 0 ? recentExchangeOptions : [{ value: '', label: 'No recent exchanges' }]}
|
||||||
|
value={testExchangeId}
|
||||||
|
onChange={(e) => setTestExchangeId(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{testTab === 'custom' && (
|
||||||
|
<div className={styles.testBody}>
|
||||||
|
<Textarea
|
||||||
|
className={styles.monoTextarea}
|
||||||
|
value={testPayload}
|
||||||
|
onChange={(e) => setTestPayload(e.target.value)}
|
||||||
|
placeholder='{"orderId": "12345", ...}'
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.testBody}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
onClick={runTestExpression}
|
||||||
|
loading={testExpressionMutation.isPending}
|
||||||
|
disabled={!tapExpression}
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{testResult && (
|
||||||
|
<div className={`${styles.testResult} ${testResult.error ? styles.testError : styles.testSuccess}`}>
|
||||||
|
{testResult.error ?? testResult.result ?? 'No result'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<div className={styles.tapModalFooter}>
|
||||||
|
<Button variant="secondary" onClick={() => setTapModalOpen(false)}>Cancel</Button>
|
||||||
|
<Button variant="primary" onClick={saveTap} disabled={!tapName || !tapProcessor || !tapExpression}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Delete Tap Confirm */}
|
||||||
|
<ConfirmDialog
|
||||||
|
open={!!deletingTap}
|
||||||
|
onClose={() => setDeletingTap(null)}
|
||||||
|
onConfirm={() => deletingTap && deleteTap(deletingTap)}
|
||||||
|
title="Delete Tap"
|
||||||
|
message={`This will remove the tap "${deletingTap?.attributeName ?? ''}" from the configuration.`}
|
||||||
|
confirmText={deletingTap?.attributeName ?? ''}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user