Give a connector node an icon that is not a code icon

A device read over a proprietary protocol was showing the fallback used
for a function node. A connector's type cannot be in the built-in icon
map — that is the point of it being a connector — so they share a plug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LF61rxW1FG5YCD2J9YqjY
This commit is contained in:
2026-08-16 11:14:35 +02:00
co-authored by Claude Fable 5
parent 4de3cd7c66
commit 42075a6dae
3 changed files with 72 additions and 3 deletions
+41
View File
@@ -0,0 +1,41 @@
/** Screenshot the dashboard and the flow canvas as a user will find them. */
import { mkdir } from "node:fs/promises"
import { chromium } from "@playwright/test"
const APP_URL = process.env.APP_URL || "http://app.localhost"
const OUT = process.env.SCREENSHOT_DIR || "screenshots"
const browser = await chromium.launch()
for (const theme of ["light", "dark"]) {
const dir = `${OUT}/${theme}`
await mkdir(dir, { recursive: true })
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
colorScheme: theme,
})
await context.addInitScript((t) => {
localStorage.setItem("fluksio-ui-theme", t)
}, theme)
const page = await context.newPage()
await page.goto(`${APP_URL}/login`, { waitUntil: "networkidle" })
await page.getByTestId("email-input").fill(process.env.FIRST_SUPERUSER)
await page
.getByTestId("password-input")
.fill(process.env.FIRST_SUPERUSER_PASSWORD)
await page.getByRole("button", { name: /log in/i }).click()
await page.waitForURL(`${APP_URL}/`, { timeout: 15000 })
await page.goto(`${APP_URL}/dashboards/aircon`, { waitUntil: "networkidle" })
await page.waitForTimeout(2500)
await page.screenshot({ path: `${dir}/live-dashboard.png` })
await page.goto(`${APP_URL}/flows/aircon`, { waitUntil: "networkidle" })
await page.keyboard.press("Escape")
await page.waitForTimeout(2500)
await page.screenshot({ path: `${dir}/live-flow.png` })
console.log(` ${theme}: done`)
await context.close()
}
await browser.close()
+22 -1
View File
@@ -303,6 +303,18 @@ function FlowEditorInner({
[nodeTypeInfo], [nodeTypeInfo],
) )
// Which types came from an installed connector rather than the engine. They
// cannot be in the icon map, so they share one.
const pluginTypes = useMemo(
() =>
new Set(
(nodeTypeInfo ?? [])
.filter((info) => info.plugin)
.map((info) => info.type),
),
[nodeTypeInfo],
)
const issuesByNode = useMemo(() => { const issuesByNode = useMemo(() => {
const map = new Map<string, string[]>() const map = new Map<string, string[]>()
for (const issue of issues) { for (const issue of issues) {
@@ -328,11 +340,20 @@ function FlowEditorInner({
flow: flowName, flow: flowName,
typeLabel: typeLabel:
typeLabels.get(definition?.type ?? "") ?? definition?.type ?? "", typeLabels.get(definition?.type ?? "") ?? definition?.type ?? "",
isPlugin: pluginTypes.has(definition?.type ?? ""),
issueText: nodeIssues.join("\n"), issueText: nodeIssues.join("\n"),
} satisfies FlowNodeData, } satisfies FlowNodeData,
} }
}), }),
[canvasNodes, definitions, flowName, issuesByNode, selectedId, typeLabels], [
canvasNodes,
definitions,
flowName,
issuesByNode,
pluginTypes,
selectedId,
typeLabels,
],
) )
// A cheap fingerprint of the wiring: it changes when a name does, but not // A cheap fingerprint of the wiring: it changes when a name does, but not
+9 -2
View File
@@ -10,6 +10,7 @@ import {
Globe, Globe,
Merge, Merge,
Play, Play,
Plug,
Radio, Radio,
Shuffle, Shuffle,
Split, Split,
@@ -58,6 +59,7 @@ export type FlowNodeData = {
definition: NodeDef_Input definition: NodeDef_Input
flow: string flow: string
typeLabel: string typeLabel: string
isPlugin?: boolean
issueText: string issueText: string
[key: string]: unknown [key: string]: unknown
} }
@@ -101,10 +103,15 @@ function PortHandles({
} }
function FlowNodeComponent({ data, selected }: NodeProps) { function FlowNodeComponent({ data, selected }: NodeProps) {
const { definition, flow, typeLabel, issueText } = data as FlowNodeData const { definition, flow, typeLabel, isPlugin, issueText } =
data as FlowNodeData
const live = useNodeStatus(`${flow}.${definition.id}`) const live = useNodeStatus(`${flow}.${definition.id}`)
const emits = useNodeEmits(`${flow}.${definition.id}`) const emits = useNodeEmits(`${flow}.${definition.id}`)
const Icon = NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ?? Code2 const Icon =
NODE_ICONS[definition.type as keyof typeof NODE_ICONS] ??
// A connector's own type cannot be in the map above, and a device is
// not a piece of code.
(isPlugin ? Plug : Code2)
// Whatever is wrong — it failed to load, it failed to run, or the graph // Whatever is wrong — it failed to load, it failed to run, or the graph
// around it does not add up — is the same red dot with the same explanation. // around it does not add up — is the same red dot with the same explanation.