Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions dashboard/src/app/api/manual-retry-state/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';

const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
const API_KEY = process.env.EXOSPHERE_API_KEY;
Comment on lines +3 to +4
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enforce HTTPS for API_BASE_URL in production

Reduce misconfig/SSRF risk by requiring https when not running locally.

-const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
+const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
+if (process.env.NODE_ENV === 'production' && API_BASE_URL.startsWith('http://')) {
+  throw new Error('EXOSPHERE_STATE_MANAGER_URI must use https in production');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
const API_KEY = process.env.EXOSPHERE_API_KEY;
const API_BASE_URL = process.env.EXOSPHERE_STATE_MANAGER_URI || 'http://localhost:8000';
if (process.env.NODE_ENV === 'production' && API_BASE_URL.startsWith('http://')) {
throw new Error('EXOSPHERE_STATE_MANAGER_URI must use https in production');
}
const API_KEY = process.env.EXOSPHERE_API_KEY;
🤖 Prompt for AI Agents
In dashboard/src/app/api/manual-retry-state/route.ts around lines 3 to 4, the
API_BASE_URL allows plain HTTP which is risky in non-local environments; enforce
HTTPS by validating the value when not running locally and fail-fast if it's
insecure. Update the code to: read API_BASE_URL from env, allow http only if the
host is localhost, 127.0.0.1 or when NODE_ENV is 'development', otherwise
require it to start with "https://"; if the value is missing or uses "http://"
in production, throw an error or exit so the app won’t start with an insecure
endpoint; keep the existing fallback to 'http://localhost:8000' only for
local/dev runs.


export async function POST(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const namespace = searchParams.get('namespace');
const stateId = searchParams.get('stateId');

if (!namespace || !stateId) {
return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
}
Comment on lines +12 to +14
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate namespace/stateId to prevent path abuse and tighten input contract

Currently any string (including slashes or path traversal sequences) is accepted and interpolated into the downstream path. Constrain to a safe charset and reasonable length.

Apply this diff:

-    if (!namespace || !stateId) {
-      return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
-    }
+    if (!namespace || !stateId) {
+      return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
+    }
+    const safeId = /^[A-Za-z0-9._-]{1,128}$/;
+    if (!safeId.test(namespace) || !safeId.test(stateId)) {
+      return NextResponse.json({ error: 'Invalid namespace or stateId format' }, { status: 400 });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!namespace || !stateId) {
return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
}
if (!namespace || !stateId) {
return NextResponse.json({ error: 'Namespace and stateId are required' }, { status: 400 });
}
const safeId = /^[A-Za-z0-9._-]{1,128}$/;
if (!safeId.test(namespace) || !safeId.test(stateId)) {
return NextResponse.json({ error: 'Invalid namespace or stateId format' }, { status: 400 });
}
🤖 Prompt for AI Agents
In dashboard/src/app/api/manual-retry-state/route.ts around lines 12-14, the
code currently accepts any string for namespace and stateId which allows
slashes/path traversal; validate both using a strict regex (e.g. only letters,
digits, hyphen, underscore) and a sensible max length (e.g. 1-64 chars) and
return a 400 with a clear error if validation fails; update the conditional to
check regex/length for both values and reject inputs containing slashes or
traversal sequences so downstream path interpolation is safe.


if (!API_KEY) {
return NextResponse.json({ error: 'API key not configured' }, { status: 500 });
}

const body = await request.json();

if (!body.fanout_id) {
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
}
Comment on lines +22 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validation for the request body is not fully robust. request.json() can resolve to any value that can be represented by JSON (e.g., null, a string), not just an object. Accessing body.fanout_id on a non-object will throw a TypeError, leading to an unexpected 500 error instead of a 400 Bad Request. It's better to validate the type of body and fanout_id before accessing it.

Suggested change
if (!body.fanout_id) {
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
}
if (typeof body !== 'object' || body === null || typeof body.fanout_id !== 'string' || !body.fanout_id) {
return NextResponse.json({ error: 'A non-empty string `fanout_id` is required in the request body' }, { status: 400 });
}

Comment thread
nk-ag marked this conversation as resolved.
Comment on lines +22 to +24
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Validate fanout_id as UUID v4

Downstream expects a UUID; enforce format early.

-    if (!body.fanout_id) {
+    const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+    if (!body.fanout_id || !uuidV4.test(body.fanout_id)) {
       return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!body.fanout_id) {
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
}
const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
if (!body.fanout_id || !uuidV4.test(body.fanout_id)) {
return NextResponse.json({ error: 'fanout_id is required in request body' }, { status: 400 });
}
🤖 Prompt for AI Agents
In dashboard/src/app/api/manual-retry-state/route.ts around lines 22 to 24, the
code only checks presence of body.fanout_id but does not validate that it is a
UUID v4; update the handler to validate fanout_id format (e.g., using a UUID v4
regex or a utility like validator.isUUID(fanout_id, 4)), and if the check fails
return NextResponse.json({ error: 'fanout_id must be a valid UUID v4' }, {
status: 400 }); ensure the validation runs immediately after the existence check
so downstream receives a correctly formatted UUID.


const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
Comment on lines +26 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add a request timeout to the downstream fetch

External call has no timeout; a hung downstream will tie up server resources.

-    const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 10_000);
+    const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
       method: 'POST',
       headers: {
         'X-API-Key': API_KEY,
         'Content-Type': 'application/json',
       },
-      body: JSON.stringify(body),
+      body: JSON.stringify(body),
+      signal: controller.signal,
     });
+    clearTimeout(timeoutId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10_000);
const response = await fetch(`${API_BASE_URL}/v0/namespace/${namespace}/state/${stateId}/manual-retry`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: controller.signal,
});
clearTimeout(timeoutId);
🤖 Prompt for AI Agents
In dashboard/src/app/api/manual-retry-state/route.ts around lines 26 to 33, the
downstream fetch has no timeout and can hang; wrap the fetch with an
AbortController and a setTimeout (configurable, e.g. 5s) that calls
controller.abort() after the timeout, pass controller.signal to fetch, and clear
the timeout timer on success or error to avoid leaks; ensure you catch the abort
error and return an appropriate timeout response.


if (!response.ok) {
const errorText = await response.text();
throw new Error(`State manager API error: ${response.status} ${response.statusText} - ${errorText}`);
}

const data = await response.json();
return NextResponse.json(data);
Comment thread
nk-ag marked this conversation as resolved.
} catch (error) {
console.error('Error retrying state:', error);
return NextResponse.json(
{ error: 'Failed to retry state' },
{ status: 500 }
);
}
}
63 changes: 58 additions & 5 deletions dashboard/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
--input: #1a2a5a; /* Navy input background */
--ring: #87ceeb; /* Sky blue focus ring */
--chart-1: #87ceeb; /* Sky blue */
--chart-2: #4ade80; /* Green accent */
--chart-3: #fbbf24; /* Yellow accent */
--chart-2: #66d1b5; /* Green accent */
--chart-3: #ffed9e; /* Yellow accent */
--chart-4: #ff6b8a; /* Pink accent */
--chart-5: #a78bfa; /* Purple accent */
--sidebar: #0a1a4a;
Expand All @@ -52,6 +52,44 @@
--sidebar-ring: #87ceeb;
}

/* Light Mode Variables */
.light {
--background: #ffffff; /* White background */
--foreground: #031035; /* Dark navy text */
--card: #f2f7fb; /* Light card background */
--card-foreground: #031035;
--popover: #ffffff;
--popover-foreground: #031035;
--primary: #031035; /* Dark navy primary (keeping dark blue accent) */
--primary-foreground: #ffffff;
--secondary: #f1f5f9; /* Light gray secondary */
--secondary-foreground: #031035;
--muted: #f1f5f9; /* Light muted background */
--muted-foreground: #64748b; /* Medium gray text */
--accent: #031035; /* Keep dark blue accent */
--accent-light: #0a1a4a; /* Keep dark blue accent */
--accent-lighter: #1a2a5a; /* Keep dark blue accent */
--accent-lightest: #2a3a6a; /* Keep dark blue accent */
--accent-foreground: #ffffff;
--destructive: #dc2626; /* Red for errors in light mode */
--border: #e2e8f0; /* Light border */
--input: #ffffff; /* White input background */
--ring: #87ceeb; /* Keep sky blue focus ring */
--chart-1: #87ceeb; /* Sky blue */
--chart-2: #4ade80; /* Green accent */
--chart-3: #cca301; /* Yellow accent */
--chart-4: #ff6b8a; /* Pink accent */
--chart-5: #a78bfa; /* Purple accent */
--sidebar: #f8fafc;
--sidebar-foreground: #031035;
--sidebar-primary: #031035; /* Dark navy for sidebar primary in light mode */
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #f1f5f9;
--sidebar-accent-foreground: #031035;
--sidebar-border: #e2e8f0;
--sidebar-ring: #87ceeb;
}

/* Custom Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
Expand Down Expand Up @@ -113,7 +151,18 @@
}
}


/* Light mode react-flow nodes */
.light .react-flow__node{
background-color: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
&:hover{
background-color: var(--muted);
}
&:active{
background-color: var(--muted);
}
}
Comment thread
nk-ag marked this conversation as resolved.

@theme inline {
--color-background: var(--background);
Expand Down Expand Up @@ -169,9 +218,13 @@
@apply bg-background text-foreground font-sans;
}

/* Custom select dropdown styling for better dark theme support */
/* Custom select dropdown styling for better theme support */
select {
color-scheme: dark;
color-scheme: light dark;
}

.light select {
color-scheme: light;
}
Comment thread
nk-ag marked this conversation as resolved.

select option {
Expand Down
133 changes: 133 additions & 0 deletions dashboard/src/app/graph/[namespace]/[runId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use client';

import React, { useState, useCallback } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { GraphVisualization } from '@/components/GraphVisualization';
import { GraphTemplateDetail } from '@/components/GraphTemplateDetail';
import { ThemeToggle } from '@/components/ThemeToggle';
import { Button } from '@/components/ui/button';
import { ArrowLeft } from 'lucide-react';
import { clientApiService } from '@/services/clientApi';
import { UpsertGraphTemplateResponse } from '@/types/state-manager';

export default function GraphPage() {
const router = useRouter();
const params = useParams();

const namespace = params?.namespace as string;
const runId = params?.runId as string;

// Graph template state
const [graphTemplate, setGraphTemplate] = useState<UpsertGraphTemplateResponse | null>(null);
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false);
const [templateError, setTemplateError] = useState<string | null>(null);

const handleBack = () => {
// Go back to the previous page or close the tab if opened from external link
if (window.history.length > 1) {
router.back();
} else {
window.close();
}
};
Comment thread
nk-ag marked this conversation as resolved.

const handleOpenGraphTemplate = useCallback(async (graphName: string) => {
if (!graphName || !namespace) return;

try {
setIsLoadingTemplate(true);
setTemplateError(null);
const template = await clientApiService.getGraphTemplate(namespace, graphName);
// Add name and namespace to the template
template.name = graphName;
template.namespace = namespace;
setGraphTemplate(template);
Comment on lines +47 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Mutating the template object received from the API directly can lead to unexpected side effects, especially if the data is cached or used elsewhere. It's a better practice to create a new object with the added properties to ensure immutability.

Suggested change
template.name = graphName;
template.namespace = namespace;
setGraphTemplate(template);
setGraphTemplate({
...template,
name: graphName,
namespace: namespace,
});

} catch (err) {
setTemplateError(err instanceof Error ? err.message : 'Failed to load graph template');
} finally {
setIsLoadingTemplate(false);
}
}, [namespace]);

const handleCloseGraphTemplate = () => {
setGraphTemplate(null);
setTemplateError(null);
};

if (!namespace || !runId) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center space-x-4">
<Button
onClick={handleBack}
variant="ghost"
size="sm"
className="flex items-center space-x-2"
>
<ArrowLeft className="w-4 h-4" />
<span>Back</span>
</Button>
<div className="h-6 w-px bg-border" />
<div>
<h1 className="text-xl font-semibold text-foreground">
Graph Visualization
</h1>
<p className="text-sm text-muted-foreground">
Namespace: {namespace} | Run: {runId}
</p>
</div>
</div>
<ThemeToggle />
</div>
</div>
</header>

{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<GraphVisualization
namespace={namespace}
runId={runId}
onGraphTemplateRequest={handleOpenGraphTemplate}
/>
</main>

{/* Graph Template Detail Modal - Inline at bottom */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pb-8">
{templateError && (
<div className="mb-4 p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">{templateError}</p>
</div>
)}

{isLoadingTemplate && (
<div className="mb-4 p-4 bg-muted rounded-lg">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary mr-2"></div>
<p className="text-sm text-muted-foreground">Loading graph template...</p>
</div>
</div>
)}

<GraphTemplateDetail
graphTemplate={graphTemplate}
isOpen={!!graphTemplate}
onClose={handleCloseGraphTemplate}
/>
</div>
</div>
);
}
5 changes: 4 additions & 1 deletion dashboard/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/contexts/ThemeContext";
Comment thread
nk-ag marked this conversation as resolved.
import "./globals.css";

const geistSans = Geist({
Expand Down Expand Up @@ -27,7 +28,9 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
Expand Down
4 changes: 3 additions & 1 deletion dashboard/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { GraphTemplateBuilder } from '@/components/GraphTemplateBuilder';
import { NamespaceOverview } from '@/components/NamespaceOverview';
import { RunsTable } from '@/components/RunsTable';
import { NodeDetailModal } from '@/components/NodeDetailModal';
import { GraphTemplateDetailModal } from '@/components/GraphTemplateDetailModal';
import { GraphTemplateDetailModal} from '@/components/GraphTemplateDetailModal';
import { ThemeToggle } from '@/components/ThemeToggle';
import { clientApiService } from '@/services/clientApi';
import {
NodeRegistration,
Expand Down Expand Up @@ -168,6 +169,7 @@ export default function Dashboard() {
)}
</Select>
</div>
<ThemeToggle />
</div>
</div>
</div>
Expand Down
16 changes: 8 additions & 8 deletions dashboard/src/components/GraphTemplateBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ export const GraphTemplateBuilder: React.FC<GraphTemplateBuilderProps> = ({
return (
<div className="w-full max-w-6xl mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-gray-800">Graph Template Builder</h2>
<h2 className="text-2xl font-bold text-foreground">Graph Template Builder</h2>
{!readOnly && (
<button
onClick={handleSave}
className="px-4 py-2 bg-[#031035] text-white rounded-lg hover:bg-[#0a1a4a] transition-colors"
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
Save Template
</button>
Expand All @@ -93,11 +93,11 @@ export const GraphTemplateBuilder: React.FC<GraphTemplateBuilderProps> = ({
{/* Nodes Section */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Workflow Nodes</h3>
<h3 className="text-lg font-semibold text-foreground">Workflow Nodes</h3>
{!readOnly && (
<button
onClick={addNode}
className="flex items-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
className="flex items-center space-x-2 px-3 py-2 bg-chart-1 text-primary-foreground rounded-md hover:bg-chart-1/90 transition-colors"
>
<Plus className="w-4 h-4" />
<span>Add Node</span>
Expand All @@ -107,9 +107,9 @@ export const GraphTemplateBuilder: React.FC<GraphTemplateBuilderProps> = ({

<div className="space-y-4">
{nodes.map((node, index) => (
<div key={index} className="bg-white border border-gray-200 rounded-lg p-4">
<div key={index} className="bg-card border border-border rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h4 className="text-md font-medium text-gray-800">Node {index + 1}</h4>
<h4 className="text-md font-medium text-card-foreground">Node {index + 1}</h4>
{!readOnly && (
<button
onClick={() => removeNode(index)}
Expand All @@ -122,15 +122,15 @@ export const GraphTemplateBuilder: React.FC<GraphTemplateBuilderProps> = ({

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-card-foreground mb-1">
Node Name
</label>
<input
type="text"
value={node.node_name}
onChange={(e) => updateNode(index, { node_name: e.target.value })}
disabled={readOnly}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:bg-muted bg-input text-foreground"
placeholder="Enter node name"
/>
Comment on lines +125 to 135
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Label not associated with input (a11y).

Wire the label to the input with htmlFor/id.

Apply:

-                  <label className="block text-sm font-medium text-card-foreground mb-1">
+                  <label htmlFor={`node_name_${index}`} className="block text-sm font-medium text-card-foreground mb-1">
                     Node Name
                   </label>
                   <input
                     type="text"
                     value={node.node_name}
                     onChange={(e) => updateNode(index, { node_name: e.target.value })}
                     disabled={readOnly}
-                    className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:bg-muted bg-input text-foreground"
+                    id={`node_name_${index}`}
+                    className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:bg-muted bg-input text-foreground"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<label className="block text-sm font-medium text-card-foreground mb-1">
Node Name
</label>
<input
type="text"
value={node.node_name}
onChange={(e) => updateNode(index, { node_name: e.target.value })}
disabled={readOnly}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100"
className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:bg-muted bg-input text-foreground"
placeholder="Enter node name"
/>
<label htmlFor={`node_name_${index}`} className="block text-sm font-medium text-card-foreground mb-1">
Node Name
</label>
<input
type="text"
value={node.node_name}
onChange={(e) => updateNode(index, { node_name: e.target.value })}
disabled={readOnly}
id={`node_name_${index}`}
className="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:bg-muted bg-input text-foreground"
placeholder="Enter node name"
/>
🧰 Tools
🪛 Biome (2.1.2)

[error] 125-127: A form label must be associated with an input.

Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.

(lint/a11y/noLabelWithoutControl)

🤖 Prompt for AI Agents
In dashboard/src/components/GraphTemplateBuilder.tsx around lines 125 to 135,
the label is not associated with the input which breaks accessibility; add a
unique id to the input (e.g., `node-name-${index}` or use the node's stable id)
and set the label's htmlFor to that same id, preserving existing props (value,
onChange, disabled, className, placeholder) so the label correctly targets the
input for screen readers and click focus.

</div>
Expand Down
Loading
Loading