import { useState } from "react" import { useDataSources, useCreateDataSource, useUpdateDataSource, useDeleteDataSource, } from "@/api/data-sources" import type { DataSource, DataSourceConfig, SourceType } from "@/api/types" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Plus, Pencil, Trash2, X, Eye, EyeOff } from "lucide-react" import { toast } from "sonner" const SOURCE_TYPES: SourceType[] = [ "weather", "media", "rss", "http_json", "webhook", "clock", "static_text", ] const EXTERNAL_TYPES: SourceType[] = ["weather", "media", "rss", "http_json", "webhook"] function defaultConfigFor(sourceType: SourceType): DataSourceConfig { if (sourceType === "clock") return { type: "clock", format: "%H:%M:%S", timezone: "UTC" } if (sourceType === "static_text") return { type: "static_text", text: "" } return { type: "external", url: null, api_key: null, headers: [] } } const EMPTY: DataSource = { id: 0, name: "", source_type: "http_json", poll_interval_secs: 300, config: { type: "external", url: null, api_key: null, headers: [] }, } export function DataSourcesPage() { const { data: sources = [], isLoading } = useDataSources() const create = useCreateDataSource() const update = useUpdateDataSource() const del = useDeleteDataSource() const [editing, setEditing] = useState(null) const [deleting, setDeleting] = useState(null) function openNew() { const nextId = sources.length > 0 ? Math.max(...sources.map((s) => s.id)) + 1 : 1 setEditing({ ...EMPTY, id: nextId }) } function openEdit(ds: DataSource) { setEditing({ ...ds }) } async function save() { if (!editing) return const isNew = !sources.some((s) => s.id === editing.id) try { if (isNew) { await create.mutateAsync(editing) toast.success("Data source created") } else { await update.mutateAsync(editing) toast.success("Data source updated") } setEditing(null) } catch (e) { toast.error(String(e)) } } async function confirmDelete() { if (deleting == null) return try { await del.mutateAsync(deleting) toast.success("Data source deleted") } catch (e) { toast.error(String(e)) } setDeleting(null) } if (isLoading) return
Loading…
return (

Data Sources

Configure external data feeds

{sources.length === 0 ? (

No data sources configured yet.

) : (
{sources.map((ds) => (
{ds.name} {ds.source_type} {ds.poll_interval_secs > 0 && every {ds.poll_interval_secs}s} {ds.config.type === "external" && ds.config.url && ( {ds.config.url} )}
))}
)} {/* Edit / Create Dialog */} !o && setEditing(null)}> {editing && sources.some((s) => s.id === editing.id) ? "Edit Data Source" : "New Data Source"} {editing && ( )} {/* Delete Confirmation */} !o && setDeleting(null)} > Delete data source? This will permanently remove this data source. Widgets referencing it will lose their feed. Cancel Delete
) } const SENSITIVE_KEYS = ["password", "secret", "token", "api_key", "apikey"] function isSensitiveKey(key: string) { return SENSITIVE_KEYS.some((s) => key.toLowerCase().includes(s)) } function HeaderRow({ headerKey, headerValue, onChangeKey, onChangeValue, onRemove, }: { headerKey: string headerValue: string onChangeKey: (v: string) => void onChangeValue: (v: string) => void onRemove: () => void }) { const sensitive = isSensitiveKey(headerKey) const [visible, setVisible] = useState(!sensitive) return (
onChangeKey(e.target.value)} placeholder="key" className="flex-1" />
onChangeValue(e.target.value)} placeholder="value" className={sensitive ? "pr-9" : ""} /> {sensitive && ( )}
) } function DataSourceForm({ value, onChange, }: { value: DataSource onChange: (ds: DataSource) => void }) { const set = (k: K, v: DataSource[K]) => onChange({ ...value, [k]: v }) const setConfig = (patch: Partial) => onChange({ ...value, config: { ...value.config, ...patch } as DataSourceConfig }) const onSourceTypeChange = (t: SourceType) => { onChange({ ...value, source_type: t, config: defaultConfigFor(t) }) } const isExternal = value.config.type === "external" const isClock = value.config.type === "clock" const isStaticText = value.config.type === "static_text" return (
set("name", e.target.value)} placeholder="e.g. weather" />
{isExternal && ( <>
setConfig({ url: e.target.value || null })} placeholder="https://..." />
setConfig({ api_key: e.target.value || null })} placeholder="Optional" />
)} {isClock && ( <>
setConfig({ format: e.target.value })} placeholder="%H:%M:%S" />
setConfig({ timezone: e.target.value })} placeholder="Europe/Warsaw" />
)} {isStaticText && (
setConfig({ text: e.target.value })} placeholder="Hello world" />
)}
set("poll_interval_secs", Number(e.target.value))} min={1} />
{isExternal && (
{value.config.headers.map(([k, v], i) => ( { const next = [...value.config.headers] as [string, string][] next[i] = [newKey, v] setConfig({ headers: next }) }} onChangeValue={(newVal) => { const next = [...value.config.headers] as [string, string][] next[i] = [k, newVal] setConfig({ headers: next }) }} onRemove={() => setConfig({ headers: value.config.headers.filter((_, idx) => idx !== i) }) } /> ))}
)}
) }