mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-25 14:14:57 +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 { EditorToolbar } from '@/components/molecules/EditorToolbar'
|
||||||
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
|
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
|
||||||
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
|
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
|
||||||
import { CodeExplanationDialog } from '@/components/molecules/CodeExplanationDialog'
|
import { CodeExplanationDialog } from '@/lib/json-ui/json-components'
|
||||||
|
|
||||||
interface CodeEditorProps {
|
interface CodeEditorProps {
|
||||||
files: ProjectFile[]
|
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 { Breadcrumb } from './Breadcrumb'
|
||||||
export { AppBranding } from './AppBranding'
|
|
||||||
export { CanvasRenderer } from './CanvasRenderer'
|
export { CanvasRenderer } from './CanvasRenderer'
|
||||||
export { CodeExplanationDialog } from './CodeExplanationDialog'
|
|
||||||
export { ComponentPalette } from './ComponentPalette'
|
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 { PropertyEditor } from './PropertyEditor'
|
||||||
|
export { SearchInput } from './SearchInput'
|
||||||
export { ToolbarButton } from './ToolbarButton'
|
export { ToolbarButton } from './ToolbarButton'
|
||||||
export { TreeFormDialog } from './TreeFormDialog'
|
export { TreeFormDialog } from './TreeFormDialog'
|
||||||
export { SearchInput } from './SearchInput'
|
export { preloadMonacoEditor } from './LazyMonacoEditor'
|
||||||
|
export { ComponentTree } from './ComponentTree'
|
||||||
export {
|
export {
|
||||||
LoadingFallback,
|
LoadingFallback,
|
||||||
NavigationItem,
|
NavigationItem,
|
||||||
PageHeaderContent,
|
PageHeaderContent,
|
||||||
|
AppBranding,
|
||||||
|
CodeExplanationDialog,
|
||||||
ComponentBindingDialog,
|
ComponentBindingDialog,
|
||||||
|
DataSourceCard,
|
||||||
DataSourceEditorDialog,
|
DataSourceEditorDialog,
|
||||||
GitHubBuildStatus as GitHubBuildStatusJSON,
|
GitHubBuildStatus,
|
||||||
SaveIndicator,
|
LazyBarChart,
|
||||||
ComponentTree,
|
|
||||||
SeedDataManager,
|
|
||||||
LazyD3BarChart,
|
LazyD3BarChart,
|
||||||
|
LazyLineChart,
|
||||||
|
NavigationGroupHeader,
|
||||||
|
SaveIndicator,
|
||||||
|
SeedDataManager,
|
||||||
StorageSettings
|
StorageSettings
|
||||||
} from '@/lib/json-ui/json-components'
|
} from '@/lib/json-ui/json-components'
|
||||||
export { preloadMonacoEditor } from './LazyMonacoEditor'
|
|
||||||
|
|||||||
Reference in New Issue
Block a user