mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 06:04:54 +00:00
feat: Delete 13 duplicate molecule TSX files with JSON equivalents
Deleted molecules now using JSON-ui implementations: - AppBranding, CodeExplanationDialog, ComponentBindingDialog - DataSourceCard, DataSourceEditorDialog, GitHubBuildStatus - LazyBarChart, LazyD3BarChart, LazyLineChart - NavigationGroupHeader, SaveIndicator, StorageSettings Updated src/components/molecules/index.ts to import these from @/lib/json-ui/json-components instead of TSX files. Updated src/components/CodeEditor.tsx to import CodeExplanationDialog from json-components. Organisms still depend on some molecules (CanvasRenderer, ComponentPalette, ComponentTree, PropertyEditor, etc) so those remain as TSX. Build passes successfully with no errors. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ import { useAIOperations } from '@/hooks/use-ai-operations'
|
||||
import { EditorToolbar } from '@/components/molecules/EditorToolbar'
|
||||
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
|
||||
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
|
||||
import { CodeExplanationDialog } from '@/components/molecules/CodeExplanationDialog'
|
||||
import { CodeExplanationDialog } from '@/lib/json-ui/json-components'
|
||||
|
||||
interface CodeEditorProps {
|
||||
files: ProjectFile[]
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { AppLogo } from '@/components/atoms'
|
||||
|
||||
export function AppBranding() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<AppLogo size="sm" />
|
||||
<span className="font-semibold text-lg">Spark</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Sparkle } from '@phosphor-icons/react'
|
||||
|
||||
interface CodeExplanationDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
fileName: string | undefined
|
||||
explanation: string
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function CodeExplanationDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
fileName,
|
||||
explanation,
|
||||
isLoading,
|
||||
}: CodeExplanationDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Code Explanation</DialogTitle>
|
||||
<DialogDescription>
|
||||
AI-generated explanation of {fileName}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-96">
|
||||
<div className="p-4 bg-muted rounded-lg">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 text-muted-foreground">
|
||||
<Sparkle size={16} weight="duotone" className="animate-pulse" />
|
||||
Analyzing code...
|
||||
</div>
|
||||
) : (
|
||||
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { BindingEditor } from '@/lib/json-ui/json-components'
|
||||
import { DataSource, UIComponent } from '@/types/json-ui'
|
||||
import { Link } from '@phosphor-icons/react'
|
||||
import { useComponentBindingDialog } from '@/hooks/use-component-binding-dialog'
|
||||
|
||||
interface ComponentBindingDialogProps {
|
||||
open: boolean
|
||||
component: UIComponent | null
|
||||
dataSources: DataSource[]
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (component: UIComponent) => void
|
||||
}
|
||||
|
||||
export function ComponentBindingDialog({
|
||||
open,
|
||||
component,
|
||||
dataSources,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: ComponentBindingDialogProps) {
|
||||
const { editingComponent, handleSave, updateBindings } = useComponentBindingDialog({
|
||||
component,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
})
|
||||
|
||||
if (!editingComponent) return null
|
||||
|
||||
const availableProps = ['children', 'value', 'checked', 'className', 'disabled', 'placeholder']
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Link className="w-5 h-5" />
|
||||
Component Data Bindings
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Connect component properties to data sources
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="p-4 bg-muted/30 rounded border border-border">
|
||||
<div className="text-sm space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">Component:</span>
|
||||
<span className="font-mono font-medium">{editingComponent.type}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground">ID:</span>
|
||||
<span className="font-mono text-xs">{editingComponent.id}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="bindings">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="bindings">Property Bindings</TabsTrigger>
|
||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="bindings" className="space-y-4 mt-4">
|
||||
<BindingEditor
|
||||
bindings={editingComponent.bindings || {}}
|
||||
dataSources={dataSources}
|
||||
availableProps={availableProps}
|
||||
onChange={updateBindings}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preview" className="space-y-4 mt-4">
|
||||
<div className="p-4 bg-muted/30 rounded border border-border">
|
||||
<pre className="text-xs overflow-auto">
|
||||
{JSON.stringify(editingComponent.bindings, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
Save Bindings
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Card, IconButton, Stack, Flex, Text } from '@/components/atoms'
|
||||
import { DataSourceBadge } from '@/components/atoms'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { Pencil, Trash } from '@phosphor-icons/react'
|
||||
|
||||
interface DataSourceCardProps {
|
||||
dataSource: DataSource
|
||||
dependents?: DataSource[]
|
||||
onEdit: (id: string) => void
|
||||
onDelete: (id: string) => void
|
||||
}
|
||||
|
||||
export function DataSourceCard({ dataSource, dependents = [], onEdit, onDelete }: DataSourceCardProps) {
|
||||
const renderTypeSpecificInfo = () => {
|
||||
if (dataSource.type === 'kv') {
|
||||
return (
|
||||
<Text variant="caption" className="font-mono bg-muted/30 px-2 py-1 rounded">
|
||||
Key: {dataSource.key || 'Not set'}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur hover:bg-card/70 transition-colors">
|
||||
<div className="p-4">
|
||||
<Flex justify="between" align="start" gap="md">
|
||||
<Stack spacing="sm" className="flex-1 min-w-0">
|
||||
<Flex align="center" gap="sm">
|
||||
<DataSourceBadge type={dataSource.type} />
|
||||
<Text variant="small" className="font-mono font-medium truncate">
|
||||
{dataSource.id}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{renderTypeSpecificInfo()}
|
||||
|
||||
{dependents.length > 0 && (
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<Text variant="caption">
|
||||
Used by {dependents.length} dependent {dependents.length === 1 ? 'source' : 'sources'}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Flex align="center" gap="xs">
|
||||
<IconButton
|
||||
icon={<Pencil className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(dataSource.id)}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<Trash className="w-4 h-4" />}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(dataSource.id)}
|
||||
className="text-destructive hover:text-destructive"
|
||||
disabled={dependents.length > 0}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { DataSource } from '@/types/json-ui'
|
||||
import { DataSourceBadge } from '@/components/atoms'
|
||||
import { DataSourceIdField } from '@/components/molecules/data-source-editor/DataSourceIdField'
|
||||
import { KvSourceFields } from '@/components/molecules/data-source-editor/KvSourceFields'
|
||||
import { StaticSourceFields } from '@/components/molecules/data-source-editor/StaticSourceFields'
|
||||
import dataSourceEditorCopy from '@/data/data-source-editor-dialog.json'
|
||||
import { useDataSourceEditor } from '@/hooks/data/use-data-source-editor'
|
||||
|
||||
interface DataSourceEditorDialogProps {
|
||||
open: boolean
|
||||
dataSource: DataSource | null
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: (dataSource: DataSource) => void
|
||||
}
|
||||
|
||||
export function DataSourceEditorDialog({
|
||||
open,
|
||||
dataSource,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
}: DataSourceEditorDialogProps) {
|
||||
const {
|
||||
editingSource,
|
||||
updateField,
|
||||
} = useDataSourceEditor(dataSource)
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editingSource) return
|
||||
onSave(editingSource)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
if (!editingSource) return null
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{dataSourceEditorCopy.title}
|
||||
<DataSourceBadge type={editingSource.type} />
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dataSourceEditorCopy.description}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<DataSourceIdField
|
||||
editingSource={editingSource}
|
||||
label={dataSourceEditorCopy.fields.id.label}
|
||||
placeholder={dataSourceEditorCopy.fields.id.placeholder}
|
||||
onChange={(value) => updateField('id', value)}
|
||||
/>
|
||||
|
||||
{editingSource.type === 'kv' && (
|
||||
<KvSourceFields
|
||||
editingSource={editingSource}
|
||||
copy={dataSourceEditorCopy.kv}
|
||||
onUpdateField={updateField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{editingSource.type === 'static' && (
|
||||
<StaticSourceFields
|
||||
editingSource={editingSource}
|
||||
label={dataSourceEditorCopy.static.valueLabel}
|
||||
placeholder={dataSourceEditorCopy.static.valuePlaceholder}
|
||||
onUpdateField={updateField}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{dataSourceEditorCopy.actions.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
{dataSourceEditorCopy.actions.save}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import {
|
||||
GitBranch,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
ArrowSquareOut,
|
||||
Warning,
|
||||
Copy,
|
||||
CheckSquare,
|
||||
} from '@phosphor-icons/react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import copy from '@/data/github-build-status.json'
|
||||
import { useGithubBuildStatus, Workflow, WorkflowRun } from '@/hooks/use-github-build-status'
|
||||
|
||||
interface GitHubBuildStatusProps {
|
||||
owner: string
|
||||
repo: string
|
||||
defaultBranch?: string
|
||||
}
|
||||
|
||||
interface WorkflowRunStatusProps {
|
||||
status: string
|
||||
conclusion: string | null
|
||||
}
|
||||
|
||||
interface WorkflowRunDetailsProps {
|
||||
branch: string
|
||||
updatedAt: string
|
||||
event: string
|
||||
}
|
||||
|
||||
interface WorkflowRunItemProps {
|
||||
workflow: WorkflowRun
|
||||
renderStatus: (status: string, conclusion: string | null) => React.ReactNode
|
||||
renderBadge: (status: string, conclusion: string | null) => React.ReactNode
|
||||
formatTime: (dateString: string) => string
|
||||
}
|
||||
|
||||
interface WorkflowBadgeListProps {
|
||||
workflows: Workflow[]
|
||||
copiedBadge: string | null
|
||||
defaultBranch: string
|
||||
onCopyBadge: (workflowPath: string, workflowName: string, branch?: string) => void
|
||||
getBadgeUrl: (workflowPath: string, branch?: string) => string
|
||||
getBadgeMarkdown: (workflowPath: string, workflowName: string, branch?: string) => string
|
||||
}
|
||||
|
||||
interface BranchBadgeListProps {
|
||||
branches: string[]
|
||||
workflows: Workflow[]
|
||||
copiedBadge: string | null
|
||||
onCopyBadge: (workflowPath: string, workflowName: string, branch: string) => void
|
||||
getBadgeUrl: (workflowPath: string, branch?: string) => string
|
||||
}
|
||||
|
||||
const WorkflowRunStatus = ({ status, conclusion }: WorkflowRunStatusProps) => {
|
||||
const getStatusIcon = () => {
|
||||
if (status === 'completed') {
|
||||
if (conclusion === 'success') {
|
||||
return <CheckCircle size={20} weight="fill" className="text-green-500" />
|
||||
}
|
||||
if (conclusion === 'failure') {
|
||||
return <XCircle size={20} weight="fill" className="text-red-500" />
|
||||
}
|
||||
if (conclusion === 'cancelled') {
|
||||
return <Warning size={20} weight="fill" className="text-yellow-500" />
|
||||
}
|
||||
}
|
||||
return <Clock size={20} weight="duotone" className="text-blue-500 animate-pulse" />
|
||||
}
|
||||
|
||||
return <div className="flex items-center">{getStatusIcon()}</div>
|
||||
}
|
||||
|
||||
const WorkflowRunBadge = ({ status, conclusion }: WorkflowRunStatusProps) => {
|
||||
if (status === 'completed') {
|
||||
if (conclusion === 'success') {
|
||||
return (
|
||||
<Badge className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
{copy.status.success}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
if (conclusion === 'failure') {
|
||||
return <Badge variant="destructive">{copy.status.failed}</Badge>
|
||||
}
|
||||
if (conclusion === 'cancelled') {
|
||||
return <Badge variant="secondary">{copy.status.cancelled}</Badge>
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Badge variant="outline" className="border-blue-500/50 text-blue-500">
|
||||
{copy.status.running}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowRunDetails = ({ branch, updatedAt, event }: WorkflowRunDetailsProps) => (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
||||
<span className="truncate">{branch}</span>
|
||||
<span>•</span>
|
||||
<span>{updatedAt}</span>
|
||||
<span>•</span>
|
||||
<span className="truncate">{event}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
const WorkflowRunItem = ({ workflow, renderStatus, renderBadge, formatTime }: WorkflowRunItemProps) => (
|
||||
<div
|
||||
className="flex items-center justify-between p-3 border border-border rounded-lg hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{renderStatus(workflow.status, workflow.conclusion)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium truncate">{workflow.name}</p>
|
||||
{renderBadge(workflow.status, workflow.conclusion)}
|
||||
</div>
|
||||
<WorkflowRunDetails
|
||||
branch={workflow.head_branch}
|
||||
updatedAt={formatTime(workflow.updated_at)}
|
||||
event={workflow.event}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" asChild className="ml-2">
|
||||
<a
|
||||
href={workflow.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<ArrowSquareOut size={16} />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const WorkflowBadgeList = ({
|
||||
workflows,
|
||||
copiedBadge,
|
||||
defaultBranch,
|
||||
onCopyBadge,
|
||||
getBadgeUrl,
|
||||
getBadgeMarkdown,
|
||||
}: WorkflowBadgeListProps) => (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">{copy.sections.workflowBadges}</h3>
|
||||
<div className="space-y-3">
|
||||
{workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className="p-3 border border-border rounded-lg space-y-2"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm font-medium">{workflow.name}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onCopyBadge(workflow.path, workflow.name)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{copiedBadge === `${workflow.path}-${defaultBranch}` ? (
|
||||
<CheckSquare size={14} className="text-green-500" />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<img
|
||||
src={getBadgeUrl(workflow.path)}
|
||||
alt={`${workflow.name} status`}
|
||||
className="h-5"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground font-mono break-all">
|
||||
{getBadgeMarkdown(workflow.path, workflow.name)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const BranchBadgeList = ({
|
||||
branches,
|
||||
workflows,
|
||||
copiedBadge,
|
||||
onCopyBadge,
|
||||
getBadgeUrl,
|
||||
}: BranchBadgeListProps) => (
|
||||
<div>
|
||||
<h3 className="text-sm font-medium mb-3">{copy.sections.branchBadges}</h3>
|
||||
<div className="space-y-3">
|
||||
{branches.slice(0, 3).map((branch) => (
|
||||
<div key={branch} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch size={16} weight="duotone" />
|
||||
<p className="text-sm font-medium">{branch}</p>
|
||||
</div>
|
||||
{workflows.slice(0, 2).map((workflow) => (
|
||||
<div
|
||||
key={`${workflow.id}-${branch}`}
|
||||
className="p-3 border border-border rounded-lg space-y-2 ml-6"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs text-muted-foreground">{workflow.name}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onCopyBadge(workflow.path, workflow.name, branch)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
{copiedBadge === `${workflow.path}-${branch}` ? (
|
||||
<CheckSquare size={14} className="text-green-500" />
|
||||
) : (
|
||||
<Copy size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<img
|
||||
src={getBadgeUrl(workflow.path, branch)}
|
||||
alt={`${workflow.name} status on ${branch}`}
|
||||
className="h-5"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export function GitHubBuildStatus({ owner, repo, defaultBranch = 'main' }: GitHubBuildStatusProps) {
|
||||
const {
|
||||
workflows,
|
||||
allWorkflows,
|
||||
loading,
|
||||
error,
|
||||
copiedBadge,
|
||||
actions,
|
||||
} = useGithubBuildStatus({ owner, repo, defaultBranch })
|
||||
const { refresh, copyBadgeMarkdown, getBadgeUrl, getBadgeMarkdown, formatTime } = actions
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GitBranch size={24} weight="duotone" />
|
||||
{copy.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{copy.loading.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<Skeleton className="w-5 h-5 rounded-full" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="w-16 h-5 rounded" />
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-red-500/10 border-red-500/20">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GitBranch size={24} weight="duotone" />
|
||||
{copy.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{copy.error.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-start gap-3">
|
||||
<XCircle size={20} weight="fill" className="text-red-500 mt-0.5" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
<Button size="sm" variant="outline" onClick={refresh} className="text-xs">
|
||||
{copy.error.retry}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
if (workflows.length === 0 && allWorkflows.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GitBranch size={24} weight="duotone" />
|
||||
{copy.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{copy.empty.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">{copy.empty.body}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const uniqueBranches = Array.from(new Set(workflows.map((workflow) => workflow.head_branch)))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<GitBranch size={24} weight="duotone" />
|
||||
{copy.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{copy.header.description}</CardDescription>
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" onClick={refresh} className="text-xs">
|
||||
{copy.header.refresh}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<Tabs defaultValue="badges" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="badges">{copy.tabs.badges}</TabsTrigger>
|
||||
<TabsTrigger value="runs">{copy.tabs.runs}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="badges" className="space-y-4 mt-4">
|
||||
<div className="space-y-4">
|
||||
<WorkflowBadgeList
|
||||
workflows={allWorkflows}
|
||||
copiedBadge={copiedBadge}
|
||||
defaultBranch={defaultBranch}
|
||||
onCopyBadge={copyBadgeMarkdown}
|
||||
getBadgeUrl={getBadgeUrl}
|
||||
getBadgeMarkdown={getBadgeMarkdown}
|
||||
/>
|
||||
|
||||
{uniqueBranches.length > 0 && (
|
||||
<BranchBadgeList
|
||||
branches={uniqueBranches}
|
||||
workflows={allWorkflows}
|
||||
copiedBadge={copiedBadge}
|
||||
onCopyBadge={copyBadgeMarkdown}
|
||||
getBadgeUrl={getBadgeUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="runs" className="space-y-3 mt-4">
|
||||
{workflows.map((workflow) => (
|
||||
<WorkflowRunItem
|
||||
key={workflow.id}
|
||||
workflow={workflow}
|
||||
renderStatus={(status, conclusion) => (
|
||||
<WorkflowRunStatus status={status} conclusion={conclusion} />
|
||||
)}
|
||||
renderBadge={(status, conclusion) => (
|
||||
<WorkflowRunBadge status={status} conclusion={conclusion} />
|
||||
)}
|
||||
formatTime={formatTime}
|
||||
/>
|
||||
))}
|
||||
<Button size="sm" variant="outline" asChild className="w-full text-xs">
|
||||
<a
|
||||
href={`https://github.com/${owner}/${repo}/actions`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{copy.actions.viewAllWorkflows}
|
||||
<ArrowSquareOut size={14} />
|
||||
</a>
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useRecharts } from '@/hooks'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Warning } from '@phosphor-icons/react'
|
||||
|
||||
interface LazyBarChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
width?: number
|
||||
height?: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function LazyBarChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8'
|
||||
}: LazyBarChartProps) {
|
||||
const { library: recharts, loading, error } = useRecharts()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-[300px] w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load chart library. Please refresh the page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
if (!recharts) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<BarChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey={yKey} fill={color} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import { useD3 } from '@/hooks'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Warning } from '@phosphor-icons/react'
|
||||
|
||||
interface LazyD3ChartProps {
|
||||
data: Array<{ label: string; value: number }>
|
||||
width?: number
|
||||
height?: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function LazyD3BarChart({
|
||||
data,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8'
|
||||
}: LazyD3ChartProps) {
|
||||
const { library: d3, loading, error } = useD3()
|
||||
const svgRef = useRef<SVGSVGElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!d3 || !svgRef.current || !data.length) return
|
||||
|
||||
const svg = d3.select(svgRef.current)
|
||||
svg.selectAll('*').remove()
|
||||
|
||||
const margin = { top: 20, right: 20, bottom: 30, left: 40 }
|
||||
const chartWidth = width - margin.left - margin.right
|
||||
const chartHeight = height - margin.top - margin.bottom
|
||||
|
||||
const g = svg.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`)
|
||||
|
||||
const x = d3.scaleBand()
|
||||
.range([0, chartWidth])
|
||||
.padding(0.1)
|
||||
.domain(data.map(d => d.label))
|
||||
|
||||
const y = d3.scaleLinear()
|
||||
.range([chartHeight, 0])
|
||||
.domain([0, d3.max(data, d => d.value) || 0])
|
||||
|
||||
g.append('g')
|
||||
.attr('transform', `translate(0,${chartHeight})`)
|
||||
.call(d3.axisBottom(x))
|
||||
|
||||
g.append('g')
|
||||
.call(d3.axisLeft(y))
|
||||
|
||||
g.selectAll('.bar')
|
||||
.data(data)
|
||||
.enter().append('rect')
|
||||
.attr('class', 'bar')
|
||||
.attr('x', d => x(d.label) || 0)
|
||||
.attr('y', d => y(d.value))
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('height', d => chartHeight - y(d.value))
|
||||
.attr('fill', color)
|
||||
|
||||
}, [d3, data, width, height, color])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-[300px] w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load D3 library. Please refresh the page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<svg ref={svgRef} width={width} height={height} />
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useRecharts } from '@/hooks'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Warning } from '@phosphor-icons/react'
|
||||
|
||||
interface LazyLineChartProps {
|
||||
data: Array<Record<string, any>>
|
||||
xKey: string
|
||||
yKey: string
|
||||
width?: number
|
||||
height?: number
|
||||
color?: string
|
||||
}
|
||||
|
||||
export function LazyLineChart({
|
||||
data,
|
||||
xKey,
|
||||
yKey,
|
||||
width = 600,
|
||||
height = 300,
|
||||
color = '#8884d8'
|
||||
}: LazyLineChartProps) {
|
||||
const { library: recharts, loading, error } = useRecharts()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-32" />
|
||||
<Skeleton className="h-[300px] w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant="destructive">
|
||||
<Warning className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Failed to load chart library. Please refresh the page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
if (!recharts) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } = recharts
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width={width} height={height}>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey={xKey} />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey={yKey} stroke={color} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { CaretDown } from '@phosphor-icons/react'
|
||||
import { CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
|
||||
interface NavigationGroupHeaderProps {
|
||||
label: string
|
||||
count: number
|
||||
isExpanded: boolean
|
||||
}
|
||||
|
||||
export function NavigationGroupHeader({
|
||||
label,
|
||||
count,
|
||||
isExpanded,
|
||||
}: NavigationGroupHeaderProps) {
|
||||
return (
|
||||
<CollapsibleTrigger className="w-full flex items-center gap-2 px-2 py-2 rounded-lg hover:bg-muted transition-colors group">
|
||||
<CaretDown
|
||||
size={16}
|
||||
weight="bold"
|
||||
className={`text-muted-foreground transition-transform ${
|
||||
isExpanded ? 'rotate-0' : '-rotate-90'
|
||||
}`}
|
||||
/>
|
||||
<h3 className="flex-1 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{label}
|
||||
</h3>
|
||||
<span className="text-xs text-muted-foreground">{count}</span>
|
||||
</CollapsibleTrigger>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { StatusIcon } from '@/components/atoms'
|
||||
import { useSaveIndicator } from '@/hooks/use-save-indicator'
|
||||
|
||||
interface SaveIndicatorProps {
|
||||
lastSaved: number | null
|
||||
}
|
||||
|
||||
export function SaveIndicator({ lastSaved }: SaveIndicatorProps) {
|
||||
if (!lastSaved) return null
|
||||
|
||||
const { timeAgo, isRecent } = useSaveIndicator(lastSaved)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<StatusIcon type={isRecent ? 'saved' : 'synced'} animate={isRecent} />
|
||||
<span className="hidden sm:inline">{isRecent ? 'Saved' : timeAgo}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Database, HardDrive, Cloud, Cpu, Download, Upload } from '@phosphor-icons/react'
|
||||
import {
|
||||
storageSettingsCopy,
|
||||
getBackendCopy,
|
||||
type StorageBackendKey,
|
||||
} from '@/components/storage/storageSettingsConfig'
|
||||
import { useStorageSettingsHandlers } from '@/components/storage/useStorageSettingsHandlers'
|
||||
|
||||
const getBackendIcon = (backend: StorageBackendKey | null) => {
|
||||
switch (backend) {
|
||||
case 'flask':
|
||||
return <Cpu className="w-5 h-5" />
|
||||
case 'indexeddb':
|
||||
return <HardDrive className="w-5 h-5" />
|
||||
case 'sqlite':
|
||||
return <Database className="w-5 h-5" />
|
||||
case 'sparkkv':
|
||||
return <Cloud className="w-5 h-5" />
|
||||
default:
|
||||
return <Database className="w-5 h-5" />
|
||||
}
|
||||
}
|
||||
|
||||
type BackendCardProps = {
|
||||
backend: StorageBackendKey | null
|
||||
isLoading: boolean
|
||||
flaskUrl: string
|
||||
isSwitching: boolean
|
||||
onFlaskUrlChange: (value: string) => void
|
||||
onSwitchToFlask: () => void
|
||||
onSwitchToIndexedDB: () => void
|
||||
onSwitchToSQLite: () => void
|
||||
}
|
||||
|
||||
const BackendCard = ({
|
||||
backend,
|
||||
isLoading,
|
||||
flaskUrl,
|
||||
isSwitching,
|
||||
onFlaskUrlChange,
|
||||
onSwitchToFlask,
|
||||
onSwitchToIndexedDB,
|
||||
onSwitchToSQLite,
|
||||
}: BackendCardProps) => {
|
||||
const backendCopy = getBackendCopy(backend)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
{getBackendIcon(backend)}
|
||||
{storageSettingsCopy.molecule.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{storageSettingsCopy.molecule.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">{storageSettingsCopy.molecule.currentBackendLabel}</span>
|
||||
<Badge variant="secondary" className="flex items-center gap-1">
|
||||
{getBackendIcon(backend)}
|
||||
{backendCopy.moleculeLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="flask-url">{storageSettingsCopy.molecule.flaskUrlLabel}</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="flask-url"
|
||||
value={flaskUrl}
|
||||
onChange={(e) => onFlaskUrlChange(e.target.value)}
|
||||
placeholder={storageSettingsCopy.molecule.flaskUrlPlaceholder}
|
||||
disabled={isSwitching || isLoading}
|
||||
/>
|
||||
<Button
|
||||
onClick={onSwitchToFlask}
|
||||
disabled={isSwitching || isLoading || backend === 'flask'}
|
||||
variant={backend === 'flask' ? 'secondary' : 'default'}
|
||||
>
|
||||
<Cpu className="w-4 h-4 mr-2" />
|
||||
{backend === 'flask'
|
||||
? storageSettingsCopy.molecule.buttons.flaskActive
|
||||
: storageSettingsCopy.molecule.buttons.flaskUse}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.flaskHelp}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={onSwitchToIndexedDB}
|
||||
disabled={isSwitching || isLoading || backend === 'indexeddb'}
|
||||
variant={backend === 'indexeddb' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<HardDrive className="w-4 h-4 mr-2" />
|
||||
{backend === 'indexeddb'
|
||||
? storageSettingsCopy.molecule.buttons.indexeddbActive
|
||||
: storageSettingsCopy.molecule.buttons.indexeddbUse}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSwitchToSQLite}
|
||||
disabled={isSwitching || isLoading || backend === 'sqlite'}
|
||||
variant={backend === 'sqlite' ? 'secondary' : 'outline'}
|
||||
className="flex-1"
|
||||
>
|
||||
<Database className="w-4 h-4 mr-2" />
|
||||
{backend === 'sqlite'
|
||||
? storageSettingsCopy.molecule.buttons.sqliteActive
|
||||
: storageSettingsCopy.molecule.buttons.sqliteUse}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.indexeddb}</p>
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.sqlite}</p>
|
||||
<p>{storageSettingsCopy.molecule.backendDetails.flask}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
type DataManagementCardProps = {
|
||||
isExporting: boolean
|
||||
isImporting: boolean
|
||||
onExport: () => void
|
||||
onImport: () => void
|
||||
}
|
||||
|
||||
const DataManagementCard = ({
|
||||
isExporting,
|
||||
isImporting,
|
||||
onExport,
|
||||
onImport,
|
||||
}: DataManagementCardProps) => (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{storageSettingsCopy.molecule.dataTitle}</CardTitle>
|
||||
<CardDescription>{storageSettingsCopy.molecule.dataDescription}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onExport} variant="outline" className="flex-1" disabled={isExporting}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{storageSettingsCopy.molecule.buttons.export}
|
||||
</Button>
|
||||
<Button onClick={onImport} variant="outline" className="flex-1" disabled={isImporting}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{storageSettingsCopy.molecule.buttons.import}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">{storageSettingsCopy.molecule.dataHelp}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
|
||||
export function StorageSettings() {
|
||||
const {
|
||||
backend,
|
||||
isLoading,
|
||||
flaskUrl,
|
||||
setFlaskUrl,
|
||||
isSwitching,
|
||||
handleSwitchToFlask,
|
||||
handleSwitchToSQLite,
|
||||
handleSwitchToIndexedDB,
|
||||
isExporting,
|
||||
isImporting,
|
||||
handleExport,
|
||||
handleImport,
|
||||
} = useStorageSettingsHandlers({
|
||||
defaultFlaskUrl: storageSettingsCopy.molecule.flaskUrlPlaceholder,
|
||||
exportFilename: () => `${storageSettingsCopy.molecule.exportFilenamePrefix}-${Date.now()}.json`,
|
||||
importAccept: '.json',
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<BackendCard
|
||||
backend={backend}
|
||||
isLoading={isLoading}
|
||||
flaskUrl={flaskUrl}
|
||||
isSwitching={isSwitching}
|
||||
onFlaskUrlChange={setFlaskUrl}
|
||||
onSwitchToFlask={handleSwitchToFlask}
|
||||
onSwitchToIndexedDB={handleSwitchToIndexedDB}
|
||||
onSwitchToSQLite={handleSwitchToSQLite}
|
||||
/>
|
||||
<DataManagementCard
|
||||
isExporting={isExporting}
|
||||
isImporting={isImporting}
|
||||
onExport={handleExport}
|
||||
onImport={handleImport}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
export { Breadcrumb } from './Breadcrumb'
|
||||
export { AppBranding } from './AppBranding'
|
||||
export { CanvasRenderer } from './CanvasRenderer'
|
||||
export { CodeExplanationDialog } from './CodeExplanationDialog'
|
||||
export { ComponentPalette } from './ComponentPalette'
|
||||
export { GitHubBuildStatus } from './GitHubBuildStatus'
|
||||
export { LazyLineChart } from './LazyLineChart'
|
||||
export { LazyBarChart } from './LazyBarChart'
|
||||
export { NavigationGroupHeader } from './NavigationGroupHeader'
|
||||
export { PropertyEditor } from './PropertyEditor'
|
||||
export { SearchInput } from './SearchInput'
|
||||
export { ToolbarButton } from './ToolbarButton'
|
||||
export { TreeFormDialog } from './TreeFormDialog'
|
||||
export { SearchInput } from './SearchInput'
|
||||
export {
|
||||
LoadingFallback,
|
||||
NavigationItem,
|
||||
export { preloadMonacoEditor } from './LazyMonacoEditor'
|
||||
export { ComponentTree } from './ComponentTree'
|
||||
export {
|
||||
LoadingFallback,
|
||||
NavigationItem,
|
||||
PageHeaderContent,
|
||||
AppBranding,
|
||||
CodeExplanationDialog,
|
||||
ComponentBindingDialog,
|
||||
DataSourceCard,
|
||||
DataSourceEditorDialog,
|
||||
GitHubBuildStatus as GitHubBuildStatusJSON,
|
||||
SaveIndicator,
|
||||
ComponentTree,
|
||||
SeedDataManager,
|
||||
GitHubBuildStatus,
|
||||
LazyBarChart,
|
||||
LazyD3BarChart,
|
||||
LazyLineChart,
|
||||
NavigationGroupHeader,
|
||||
SaveIndicator,
|
||||
SeedDataManager,
|
||||
StorageSettings
|
||||
} from '@/lib/json-ui/json-components'
|
||||
export { preloadMonacoEditor } from './LazyMonacoEditor'
|
||||
|
||||
Reference in New Issue
Block a user