/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ import { useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import { Check, X, HelpCircle, ChevronRight } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { jmapMapToArray, SECRET_MASK } from '@/lib/jmapUtils'; import { Badge } from '@/components/ui/badge'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import { resolveSchema, resolveVariantForm, resolveForm } from '@/lib/schemaResolver'; import { formatSize, formatDuration } from '@/lib/durationFormat'; import type { Schema, Field, FieldType, FormField, Form, Fields, EnumVariant } from '@/types/schema'; export interface DynamicViewProps { schema: Schema; objectName: string; viewName: string; data: Record; visibleFields?: Set; } export function DynamicView({ schema, objectName, viewName, data, visibleFields }: DynamicViewProps) { const resolved = useMemo(() => { const sch = resolveSchema(schema, objectName); if (!sch) return null; let fields: Fields | null = null; let form: Form | null = null; let variantSchemaName: string | undefined; if (sch.type === 'single') { fields = sch.fields; form = resolveForm(schema, viewName, objectName, sch.schemaName); } else { const variantName = typeof data['@type'] === 'string' ? data['@type'] : undefined; if (variantName) { const variant = sch.variants.find((v) => v.name === variantName); if (variant?.schemaName) { variantSchemaName = variant.schemaName; fields = schema.fields[variant.schemaName] ?? null; form = resolveVariantForm(schema, viewName, objectName, variant.schemaName); } } const parentForm = resolveForm(schema, viewName, objectName, objectName); if (parentForm && form) { form = { ...form, sections: [...parentForm.sections, ...form.sections] }; } else if (parentForm) { form = parentForm; } } return { sch, fields, form, variantSchemaName }; }, [schema, objectName, viewName, data]); if (!resolved) return null; const { fields, form, sch } = resolved; const sections: { title?: string; items: { ff: FormField; field: Field }[] }[] = []; if (form) { for (const section of form.sections) { const items: { ff: FormField; field: Field }[] = []; for (const ff of section.fields) { if (ff.name === '@type') continue; if (visibleFields && !visibleFields.has(ff.name)) continue; const field = fields?.properties[ff.name]; if (!field) continue; if (isEmptyValue(data[ff.name])) continue; items.push({ ff, field }); } if (items.length > 0) { sections.push({ title: section.title, items }); } } } else if (fields) { const items: { ff: FormField; field: Field }[] = []; for (const [name, field] of Object.entries(fields.properties)) { if (visibleFields && !visibleFields.has(name)) continue; if (isEmptyValue(data[name])) continue; items.push({ ff: { name, label: name }, field }); } if (items.length > 0) { sections.push({ items }); } } const variantLabel = sch.type === 'multiple' && typeof data['@type'] === 'string' ? sch.variants.find((v) => v.name === data['@type'])?.label : undefined; return (
{variantLabel && ( {variantLabel} )} {sections.map((section, si) => ( {section.title && ( {section.title} )}
{section.items.map(({ ff, field }) => ( ))}
))}
); } function ViewField({ label, field, value, schema }: { label: string; field: Field; value: unknown; schema: Schema }) { const isBlock = isBlockType(field.type, value); return (
{label} {field.description && (
{field.description.replace(/\\n/g, '\n')}
)}
); } function isBlockType(type: FieldType, value: unknown): boolean { if (type.type === 'object' || type.type === 'objectList') return true; if (type.type === 'map') return true; if (type.type === 'string' && (type.format === 'text' || type.format === 'html')) return true; if (type.type === 'set' && value && typeof value === 'object') { return Object.keys(value as Record).length > 5; } return false; } function ViewValue({ type, value, schema }: { type: FieldType; value: unknown; schema: Schema }) { if (value === null || value === undefined) { return -; } switch (type.type) { case 'string': return ; case 'number': return ; case 'utcDateTime': return ; case 'boolean': return value === true ? : ; case 'enum': return ; case 'blobId': return {String(value)}; case 'objectId': return {String(value)}; case 'object': return ; case 'objectList': return ; case 'set': return ; case 'map': return ; default: return {JSON.stringify(value)}; } } function StringValue({ value, format }: { value: unknown; format: string }) { const str = String(value); if (!str) return -; if (format === 'text' || format === 'html' || str.includes('\n')) { return
{str}
; } if (format === 'secret' || format === 'secretText') { return {SECRET_MASK}; } return {str}; } function NumberValue({ value, format }: { value: unknown; format: string }) { const num = typeof value === 'number' ? value : Number(value); if (isNaN(num)) return {String(value)}; switch (format) { case 'size': return {formatSize(num)}; case 'duration': return {formatDuration(num)}; default: return {num.toLocaleString()}; } } function DateTimeValue({ value }: { value: unknown }) { if (!value) return -; const str = String(value); const d = new Date(str); const formatted = !isNaN(d.getTime()) ? new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short' }).format(d) : str; return {formatted}; } function EnumValue({ value, enumName, schema }: { value: unknown; enumName: string; schema: Schema }) { const str = String(value); const variants = schema.enums[enumName] ?? []; const variant = variants.find((v: EnumVariant) => v.name === str); if (variant) { return ( {variant.label} ); } return {str}; } function ObjectValue({ value, objectName, schema, defaultOpen = false, }: { value: unknown; objectName: string; schema: Schema; defaultOpen?: boolean; }) { if (!value || typeof value !== 'object') { return -; } const data = value as Record; const sch = resolveSchema(schema, objectName); let fields: Record | undefined; let form: Form | null = null; let variantLabel: string | undefined; if (sch?.type === 'multiple') { const variantName = typeof data['@type'] === 'string' ? data['@type'] : undefined; if (variantName) { const variant = sch.variants.find((v) => v.name === variantName); variantLabel = variant?.label; if (variant?.schemaName) { fields = schema.fields[variant.schemaName]?.properties; form = schema.forms[variant.schemaName] ?? null; } } } else if (sch?.type === 'single') { fields = schema.fields[sch.schemaName]?.properties; form = schema.forms[sch.schemaName] ?? schema.forms[objectName] ?? null; } const entries = buildViewEntries(data, fields, form); if (entries.length === 0 && !variantLabel) { return null; } const triggerLabel = variantLabel ?? findDisplayValue(data) ?? objectName.replace(/^x:/, ''); return (
{entries.map(({ key, label, field, value: val }) => { if (!field) { return (
{label} {typeof val === 'object' ? JSON.stringify(val) : String(val ?? '-')}
); } const block = isBlockType(field.type, val); return (
{label} {field.description && }
); })}
); } function ObjectListValue({ value, objectName, schema }: { value: unknown; objectName: string; schema: Schema }) { const items = jmapMapToArray>(value); if (items.length === 0) { return -; } return (
{items.map((item, i) => ( ))}
); } function SetValue({ value, classType, schema, }: { value: unknown; classType: { type: string; enumName?: string }; schema: Schema; }) { if (!value || typeof value !== 'object') { return -; } const keys = Object.keys(value as Record); if (keys.length === 0) { return -; } const labels = keys.map((k) => { if (classType.type === 'enum' && classType.enumName) { const variants = schema.enums[classType.enumName] ?? []; const v = variants.find((e: EnumVariant) => e.name === k); return v?.label ?? k; } return k; }); return (
{labels.map((label, i) => ( {label} ))}
); } function MapValue({ value, keyClass, valueClass, schema, }: { value: unknown; keyClass: { type: string; enumName?: string }; valueClass: { type: string; objectName?: string }; schema: Schema; }) { if (!value || typeof value !== 'object') { return -; } const entries = Object.entries(value as Record); if (entries.length === 0) { return -; } return (
{entries.map(([k, v]) => { let keyLabel = k; if (keyClass.type === 'enum' && keyClass.enumName) { const variants = schema.enums[keyClass.enumName] ?? []; const variant = variants.find((e: EnumVariant) => e.name === k); if (variant) keyLabel = variant.label; } if (valueClass.type === 'object' && valueClass.objectName) { return (
{keyLabel}
); } return (
{keyLabel} {typeof v === 'object' ? JSON.stringify(v) : String(v ?? '-')}
); })}
); } function FieldTooltip({ description }: { description: string }) { return (
{description.replace(/\\n/g, '\n')}
); } interface ViewEntry { key: string; label: string; field: Field | undefined; value: unknown; } function buildViewEntries( data: Record, fields?: Record, form?: Form | null, ): ViewEntry[] { const entries: ViewEntry[] = []; const seen = new Set(); if (form) { for (const section of form.sections) { for (const ff of section.fields) { if (ff.name === '@type') continue; if (isEmptyValue(data[ff.name])) continue; seen.add(ff.name); entries.push({ key: ff.name, label: ff.label, field: fields?.[ff.name], value: data[ff.name], }); } } } for (const [key, val] of Object.entries(data)) { if (key === '@type' || key === 'id' || seen.has(key)) continue; if (isEmptyValue(val)) continue; entries.push({ key, label: key, field: fields?.[key], value: val, }); } return entries; } function isEmptyValue(value: unknown): boolean { if (value === null || value === undefined) return true; if (value === '') return true; if (Array.isArray(value) && value.length === 0) return true; if (typeof value === 'object' && value !== null) { return Object.keys(value as Record).length === 0; } return false; } function findDisplayValue(data: Record): string | null { for (const key of [ 'name', 'domain', 'headerFrom', 'envelopeFrom', 'sourceIp', 'orgName', 'organizationName', 'email', 'from', 'to', 'subject', 'policyDomain', ]) { const val = data[key]; if (typeof val === 'string' && val.length > 0) return val; } for (const val of Object.values(data)) { if (typeof val === 'string' && val.length > 0 && val.length <= 80) return val; } return null; }