diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..570f0c6 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,17 @@ + + +
+ + + + +Loading pipeline...
++ Load a project to see the DAG visualization +
+{description}
+ )} + + {/* Footer */} +{description}
+ )} + + {/* Footer - Materialization details */} + {!isTest && ( +{description}
+ )} + + {/* Footer */} ++ {children} +
+ ) +} diff --git a/ui/src/components/ui/ConfirmDialog.tsx b/ui/src/components/ui/ConfirmDialog.tsx new file mode 100644 index 0000000..5818d4f --- /dev/null +++ b/ui/src/components/ui/ConfirmDialog.tsx @@ -0,0 +1,172 @@ +import { useState, useEffect } from 'react' +import { AlertTriangle, Trash2, AlertOctagon } from 'lucide-react' +import { Dialog, DialogFooter } from './Dialog' +import { Button } from './Button' +import { Input } from './FormField' + +interface ConfirmDialogProps { + open: boolean + onClose: () => void + onConfirm: () => void + title: string + message: string + confirmLabel?: string + cancelLabel?: string + variant?: 'danger' | 'warning' + loading?: boolean +} + +export function ConfirmDialog({ + open, + onClose, + onConfirm, + title, + message, + confirmLabel = 'Delete', + cancelLabel = 'Cancel', + variant = 'danger', + loading = false, +}: ConfirmDialogProps) { + return ( + + ) +} + +interface CascadeDeleteDialogProps { + open: boolean + onClose: () => void + onConfirm: () => void + entityName: string + entityType: 'source' | 'model' | 'test' | 'exposure' + dependents: Array<{ type: string; name: string }> + loading?: boolean +} + +export function CascadeDeleteDialog({ + open, + onClose, + onConfirm, + entityName, + entityType, + dependents, + loading = false, +}: CascadeDeleteDialogProps) { + const [confirmText, setConfirmText] = useState('') + const confirmPhrase = `delete ${entityName}` + + // Reset confirmation text when dialog opens/closes + useEffect(() => { + if (open) { + setConfirmText('') + } + }, [open]) + + const isConfirmValid = confirmText.toLowerCase() === confirmPhrase.toLowerCase() + + const handleConfirm = () => { + if (isConfirmValid) { + onConfirm() + } + } + + return ( + + ) +} diff --git a/ui/src/components/ui/DependencyTree.tsx b/ui/src/components/ui/DependencyTree.tsx new file mode 100644 index 0000000..1b5ab78 --- /dev/null +++ b/ui/src/components/ui/DependencyTree.tsx @@ -0,0 +1,186 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { ChevronRight, ChevronDown, Database, Boxes, Radio, TestTube2 } from 'lucide-react' +import { cn } from '@/utils/cn' + +type NodeType = 'source' | 'model' | 'exposure' | 'test' + +interface TreeNode { + name: string + type: NodeType + children: TreeNode[] +} + +interface DependencyTreeProps { + title: string + rootName: string + nodes: Map{description}
+ )} +{error}
} +{emptyMessage}
+
+ {highlighted}
+
+ )
+}
diff --git a/ui/src/components/ui/StatCard.tsx b/ui/src/components/ui/StatCard.tsx
new file mode 100644
index 0000000..4a837ab
--- /dev/null
+++ b/ui/src/components/ui/StatCard.tsx
@@ -0,0 +1,87 @@
+import { ReactNode } from 'react'
+import { motion } from 'framer-motion'
+import { TrendingUp, TrendingDown, Minus } from 'lucide-react'
+import { cn } from '@/utils/cn'
+
+interface StatCardProps {
+ title: string
+ value: string | number
+ icon: ReactNode
+ trend?: {
+ value: number
+ label: string
+ }
+ description?: string
+ variant?: 'default' | 'success' | 'warning' | 'error'
+ className?: string
+}
+
+const variantStyles = {
+ default: 'from-slate-800/50 to-slate-900/50 border-slate-800',
+ success: 'from-emerald-900/20 to-slate-900/50 border-emerald-800/50',
+ warning: 'from-yellow-900/20 to-slate-900/50 border-yellow-800/50',
+ error: 'from-red-900/20 to-slate-900/50 border-red-800/50',
+}
+
+const iconVariantStyles = {
+ default: 'bg-slate-800 text-slate-400',
+ success: 'bg-emerald-900/50 text-emerald-400',
+ warning: 'bg-yellow-900/50 text-yellow-400',
+ error: 'bg-red-900/50 text-red-400',
+}
+
+export function StatCard({
+ title,
+ value,
+ icon,
+ trend,
+ description,
+ variant = 'default',
+ className,
+}: StatCardProps) {
+ return (
+ {title}
+{value}
+ {trend && ( +{description}
+ )} +{dagNode?.type}
+{nodeDetails.description}
+
+ {source.topic}
+
+
+ {exposure.consumer_group}
+
+ + {project?.project.description ?? 'Monitor your streaming pipeline at a glance'} +
++ {status?.topics.filter((t) => t.exists).length ?? 0} +
+Active topics
+{runningJobs}
++ {failedJobs > 0 ? `${failedJobs} failed` : 'Running'} +
++ {status?.connectors.filter((c) => c.state === 'RUNNING').length ?? 0} +
+Active sinks
++ {status?.health_score ?? 0}% +
+Overall health
+Pipeline Valid
++ {validationWarnings > 0 + ? `${validationWarnings} warning${validationWarnings > 1 ? 's' : ''}` + : 'No issues found'} +
+Validation Failed
++ {validationErrors} error{validationErrors > 1 ? 's' : ''} +
+{msg.message}
+ {msg.location && ( +{msg.location}
+ )} ++ Plan and apply changes to your streaming infrastructure +
++ Execute data quality tests and validate assertions +
++ Modify your pipeline configuration in the YAML editor +
++ Plan and apply changes to your streaming infrastructure +
++ Check your pipeline configuration for errors and warnings before deploying. +
+ + {validation && ( +Configuration Valid
++ {validation.messages.length} warning(s) +
+Validation Failed
++ {validation.messages.filter((m) => m.level === 'error').length} error(s) +
++ Generate a deployment plan showing what resources will be created, updated, or deleted. +
+ + {plan && ( ++ Deploy the planned changes to your Kafka, Flink, and Connect clusters. +
+ + {plan && !applyResult && ( +Review before applying
++ This will create {plan.filter((c) => c.action === 'create').length} resources + in your streaming infrastructure. +
+Deployment Successful!
++ {applyResult.created.length} resources deployed +
+Deployment Failed
++ {applyResult.errors.length} error(s) +
++ Apply changes to see the result +
+ )} +
+
+
+
{'{{ source("name") }}'} - Reference source
{'{{ ref("model") }}'} - Reference model
{'{{ var("name", "default") }}'} - Variable
Materializations
+topic - Stateless transform
flink - Stateful processing
virtual_topic - Read-time filter
sink - Export to external
Test Types
+schema - Structure validation
sample - Sample N messages
continuous - Flink monitoring
Common Assertions
+not_null - No NULL values
unique - Unique values
accepted_values - Value whitelist
range - Numeric range
Pro Tip
+
+ Type {'{{ '} inside SQL blocks to get
+ intelligent suggestions for source and model references.
+
Exposure not found
+ ++ {isNew ? 'Define a new downstream exposure' : 'Modify the exposure configuration'} +
++ Define service level agreements for this exposure. +
+YAML Error: {yamlError}
++ Downstream applications and services consuming your pipeline +
+No exposures found
++ {exposure.role} +
+ )} ++ {exposure.description} +
+ )} +{selectedExposure.description}
+
+ {selectedExposure.consumer_group}
+
+ Max Latency
++ {selectedExposure.sla.max_latency_ms} + ms +
+Availability
++ {selectedExposure.sla.availability_percent} + % +
++ Choose an exposure from the list to view its details +
+Model not found
+ ++ {isNew ? 'Define a new data transformation model' : 'Modify the model configuration'} +
+
+ Use {'{{ source("name") }}'} and{' '}
+ {'{{ ref("name") }}'} to reference sources and models.
+ Dependencies are automatically discovered.
+
{errors.sink}
} +YAML Error: {yamlError}
++ Data transformations and materializations +
+No models found
++ {model.description} +
+ )} +{selectedModel.description}
+Parallelism
++ {selectedModel.flink.parallelism} +
+Checkpoint
++ {selectedModel.flink.checkpoint_interval_ms / 1000}s +
+State TTL
++ {(selectedModel.flink.state_ttl_ms / 3600000).toFixed(0)}h +
+Watermark
++ {selectedModel.flink.watermark_delay_ms / 1000}s +
+Partitions
++ {selectedModel.topic.partitions} +
+Replication
++ {selectedModel.topic.replication_factor} +
+Retention
++ {(selectedModel.topic.retention_ms / 86400000).toFixed(0)}d +
+{selectedModel.sink.connector}
+ {value}
+ + Choose a model from the list to view its details +
++ Configure your streamt environment and preferences +
+Dark Mode
++ Toggle between light and dark themes +
+{conn.name}
+{conn.url}
+Project Name
+{project.project.name}
+Version
+{project.project.version}
+Description
+{project.project.description}
+{project.sources.length}
+Sources
+{project.models.length}
+Models
+{project.tests.length}
+Tests
+{project.exposures.length}
+Exposures
++ streamt is a declarative streaming data pipeline framework that brings dbt-like + practices to Kafka, Flink, and Kafka Connect. Define your streaming pipelines + using YAML and SQL, and let streamt handle the deployment. +
+ ++ Version 0.1.0 · Apache 2.0 License · Made with love by the streamt team +
+Source not found
+ ++ {isNew ? 'Define a new Kafka topic source' : 'Modify the source configuration'} +
+{errors.columns}
} +YAML Error: {yamlError}
++ External Kafka topics that feed your pipeline +
+No sources found
++ {source.topic} +
++ {source.description} +
+ )} +{selectedSource.topic}
+ {selectedSource.description}
+Warn After
++ {selectedSource.freshness.warn_after_minutes} min +
+Error After
++ {selectedSource.freshness.error_after_minutes} min +
+| + Column + | ++ Type + | ++ Classification + | ++ Description + | +
|---|---|---|---|
+ {col.name}
+ |
+
+ {col.type || 'STRING'}
+ |
+
+ {col.classification ? (
+ |
+ + {col.description || '-'} + | +
+ Choose a source from the list to view its details +
+Test not found
+ ++ {isNew ? 'Define a new data quality test' : 'Modify the test configuration'} +
+{errors.assertions}
} +YAML Error: {yamlError}
++ Data quality assertions and continuous monitoring +
+No tests found
++ Testing: {test.model} +
+{selectedTest.model}
+
+ {selectedTest.description}
++ {testTypeConfig[selectedTest.type].label} Test +
++ {testTypeConfig[selectedTest.type].description} +
+Sample Size
++ {selectedTest.sample_size} +
+Timeout
++ {selectedTest.timeout_seconds}s +
+
+ Columns:{' '}
+
+ {config.columns.join(', ')}
+
+
+ Column:{' '}
+ {config.column}
+
+ Values:{' '}
+
+ [{config.values.join(', ')}]
+
+
+ Min:{' '}
+ {config.min}
+
+ Max:{' '}
+ {config.max}
+
+ {selectedTest.on_failure.actions.length} action(s) configured +
++ Choose a test from the list to view its details +
+