import { useState } from "react" import { useDataSources, useCreateDataSource, useUpdateDataSource, useDeleteDataSource, } from "@/api/data-sources" import type { DataSource, 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", ] const EMPTY: DataSource = { id: 0, name: "", source_type: "http_json", poll_interval_secs: 300, 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} every {ds.poll_interval_secs}s {ds.url && ( {ds.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 }) return (
set("name", e.target.value)} placeholder="e.g. weather" />
set("url", e.target.value || null)} placeholder="https://..." />
set("api_key", e.target.value || null)} placeholder="Optional" />
set("poll_interval_secs", Number(e.target.value))} min={1} />
{value.headers.map(([k, v], i) => ( { const next = [...value.headers] as [string, string][] next[i] = [newKey, v] set("headers", next) }} onChangeValue={(newVal) => { const next = [...value.headers] as [string, string][] next[i] = [k, newVal] set("headers", next) }} onRemove={() => set("headers", value.headers.filter((_, idx) => idx !== i)) } /> ))}
) }