internal data sources (clock, static text), connection indicator, rendering fixes
DataSourceConfig refactored to enum: External/Clock/StaticText. Clock generates formatted time via chrono, static text emits configured string. ESP32: connection status indicator (green/red dot bottom-right), per-widget clear before redraw, RenderEvent enum for local + server messages. Polling uses DataUpdate instead of ScreenUpdate to avoid wiping widget state. Empty mappings passthrough raw source data for internal sources.
This commit is contained in:
@@ -7,7 +7,7 @@ export interface DisplayHint {
|
||||
h_align: HAlign
|
||||
v_align: VAlign
|
||||
}
|
||||
export type SourceType = "weather" | "media" | "rss" | "http_json" | "webhook"
|
||||
export type SourceType = "weather" | "media" | "rss" | "http_json" | "webhook" | "clock" | "static_text"
|
||||
export type SizingType = "fixed" | "flex"
|
||||
export type Direction = "row" | "column"
|
||||
export type JustifyContent = "start" | "center" | "end" | "space_between" | "space_evenly"
|
||||
@@ -29,14 +29,17 @@ export interface Widget {
|
||||
|
||||
export type CreateWidget = Widget
|
||||
|
||||
export type DataSourceConfig =
|
||||
| { type: "external"; url: string | null; api_key: string | null; headers: [string, string][] }
|
||||
| { type: "clock"; format: string; timezone: string }
|
||||
| { type: "static_text"; text: string }
|
||||
|
||||
export interface DataSource {
|
||||
id: number
|
||||
name: string
|
||||
source_type: SourceType
|
||||
poll_interval_secs: number
|
||||
url: string | null
|
||||
api_key: string | null
|
||||
headers: [string, string][]
|
||||
config: DataSourceConfig
|
||||
}
|
||||
|
||||
export interface Sizing {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useUpdateDataSource,
|
||||
useDeleteDataSource,
|
||||
} from "@/api/data-sources"
|
||||
import type { DataSource, SourceType } from "@/api/types"
|
||||
import type { DataSource, DataSourceConfig, SourceType } from "@/api/types"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
@@ -50,16 +50,24 @@ const SOURCE_TYPES: SourceType[] = [
|
||||
"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,
|
||||
url: null,
|
||||
api_key: null,
|
||||
headers: [],
|
||||
config: { type: "external", url: null, api_key: null, headers: [] },
|
||||
}
|
||||
|
||||
export function DataSourcesPage() {
|
||||
@@ -145,10 +153,10 @@ export function DataSourcesPage() {
|
||||
<CardTitle className="text-base">{ds.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{ds.source_type}</Badge>
|
||||
<span>every {ds.poll_interval_secs}s</span>
|
||||
{ds.url && (
|
||||
{ds.poll_interval_secs > 0 && <span>every {ds.poll_interval_secs}s</span>}
|
||||
{ds.config.type === "external" && ds.config.url && (
|
||||
<span className="text-muted-foreground max-w-xs truncate text-xs">
|
||||
{ds.url}
|
||||
{ds.config.url}
|
||||
</span>
|
||||
)}
|
||||
</CardDescription>
|
||||
@@ -196,9 +204,13 @@ export function DataSourcesPage() {
|
||||
onClick={save}
|
||||
disabled={
|
||||
!editing?.name ||
|
||||
(editing.source_type !== "webhook" &&
|
||||
(EXTERNAL_TYPES.includes(editing.source_type) &&
|
||||
editing.source_type !== "webhook" &&
|
||||
editing.poll_interval_secs <= 0) ||
|
||||
(editing.source_type !== "webhook" && !editing.url)
|
||||
(EXTERNAL_TYPES.includes(editing.source_type) &&
|
||||
editing.source_type !== "webhook" &&
|
||||
editing.config.type === "external" &&
|
||||
!editing.config.url)
|
||||
}
|
||||
>
|
||||
Save
|
||||
@@ -302,6 +314,17 @@ function DataSourceForm({
|
||||
const set = <K extends keyof DataSource>(k: K, v: DataSource[K]) =>
|
||||
onChange({ ...value, [k]: v })
|
||||
|
||||
const setConfig = (patch: Partial<DataSourceConfig>) =>
|
||||
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 (
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
@@ -316,7 +339,7 @@ function DataSourceForm({
|
||||
<Label>Source Type</Label>
|
||||
<Select
|
||||
value={value.source_type}
|
||||
onValueChange={(v) => set("source_type", v as SourceType)}
|
||||
onValueChange={(v) => onSourceTypeChange(v as SourceType)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@@ -330,23 +353,61 @@ function DataSourceForm({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>URL</Label>
|
||||
<Input
|
||||
value={value.url ?? ""}
|
||||
onChange={(e) => set("url", e.target.value || null)}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>API Key</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={value.api_key ?? ""}
|
||||
onChange={(e) => set("api_key", e.target.value || null)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isExternal && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>URL</Label>
|
||||
<Input
|
||||
value={value.config.url ?? ""}
|
||||
onChange={(e) => setConfig({ url: e.target.value || null })}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>API Key</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={value.config.api_key ?? ""}
|
||||
onChange={(e) => setConfig({ api_key: e.target.value || null })}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isClock && (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label>Format</Label>
|
||||
<Input
|
||||
value={value.config.format}
|
||||
onChange={(e) => setConfig({ format: e.target.value })}
|
||||
placeholder="%H:%M:%S"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Timezone</Label>
|
||||
<Input
|
||||
value={value.config.timezone}
|
||||
onChange={(e) => setConfig({ timezone: e.target.value })}
|
||||
placeholder="Europe/Warsaw"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isStaticText && (
|
||||
<div className="grid gap-2">
|
||||
<Label>Text</Label>
|
||||
<Input
|
||||
value={value.config.text}
|
||||
onChange={(e) => setConfig({ text: e.target.value })}
|
||||
placeholder="Hello world"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Poll Interval (seconds)</Label>
|
||||
<Input
|
||||
@@ -356,41 +417,44 @@ function DataSourceForm({
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Headers</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
set("headers", [...value.headers, ["", ""]])
|
||||
}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Add
|
||||
</Button>
|
||||
|
||||
{isExternal && (
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Headers</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setConfig({ headers: [...value.config.headers, ["", ""]] })
|
||||
}
|
||||
>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
{value.config.headers.map(([k, v], i) => (
|
||||
<HeaderRow
|
||||
key={i}
|
||||
headerKey={k}
|
||||
headerValue={v}
|
||||
onChangeKey={(newKey) => {
|
||||
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) })
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{value.headers.map(([k, v], i) => (
|
||||
<HeaderRow
|
||||
key={i}
|
||||
headerKey={k}
|
||||
headerValue={v}
|
||||
onChangeKey={(newKey) => {
|
||||
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))
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -199,8 +199,7 @@ export function WidgetsPage() {
|
||||
onClick={save}
|
||||
disabled={
|
||||
!editing?.name ||
|
||||
!editing.data_source_id ||
|
||||
editing.mappings.length === 0
|
||||
!editing.data_source_id
|
||||
}
|
||||
>
|
||||
Save
|
||||
|
||||
Reference in New Issue
Block a user