diff --git a/assets/topology.png b/assets/topology.png
index 1d0059b..db6624a 100644
Binary files a/assets/topology.png and b/assets/topology.png differ
diff --git a/public/scripts/components/NodeDetailsComponent.js b/public/scripts/components/NodeDetailsComponent.js
index 7d88f02..4e87aea 100644
--- a/public/scripts/components/NodeDetailsComponent.js
+++ b/public/scripts/components/NodeDetailsComponent.js
@@ -25,6 +25,100 @@ class NodeDetailsComponent extends Component {
return (r << 16) + (g << 8) + b;
}
+ // Parameter component renderers
+ renderSelectComponent(p, formId, pidx) {
+ return ``;
+ }
+
+ renderColorComponent(p, formId, pidx) {
+ const defaultValue = p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0;
+ return `
+
+
+
`;
+ }
+
+ renderNumberRangeComponent(p, formId, pidx) {
+ const defaultValue = p.default !== undefined ? p.default : 0;
+ const maxValue = p.value || 100;
+ return `
+
+
+ ${defaultValue}
+ / ${maxValue}
+
+
`;
+ }
+
+ renderTextComponent(p, formId, pidx) {
+ const defaultValue = p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : '';
+ return ``;
+ }
+
+ // Component map for parameter types
+ getParameterComponentMap() {
+ const components = {
+ select: this.renderSelectComponent,
+ color: this.renderColorComponent,
+ numberRange: this.renderNumberRangeComponent,
+ text: this.renderTextComponent
+ };
+
+ // Bind all methods to this context
+ return Object.fromEntries(
+ Object.entries(components).map(([key, method]) => [key, method.bind(this)])
+ );
+ }
+
+ // Component type determination rules
+ getComponentType(p) {
+ const typeRules = [
+ { condition: () => Array.isArray(p.values) && p.values.length > 1, type: 'select' },
+ { condition: () => p.type === 'color', type: 'color' },
+ { condition: () => p.type === 'numberRange', type: 'numberRange' }
+ ];
+
+ const matchedRule = typeRules.find(rule => rule.condition());
+ return matchedRule ? matchedRule.type : 'text';
+ }
+
+ // Main parameter renderer that uses the component map
+ renderParameterComponent(p, formId, pidx) {
+ const componentMap = this.getParameterComponentMap();
+ const componentType = this.getComponentType(p);
+ const renderer = componentMap[componentType];
+
+ return renderer(p, formId, pidx);
+ }
+
setupViewModelListeners() {
@@ -441,34 +535,7 @@ class NodeDetailsComponent extends Component {
? ``
: 'No parameters
';
@@ -608,6 +675,17 @@ class NodeDetailsComponent extends Component {
}
});
});
+
+ // Add event listeners for range sliders
+ const rangeSliders = this.findAllElements('.range-slider');
+ rangeSliders.forEach(rangeInput => {
+ this.addEventListener(rangeInput, 'input', (e) => {
+ const rangeDisplay = rangeInput.parentElement.querySelector('.range-display .range-value');
+ if (rangeDisplay) {
+ rangeDisplay.textContent = e.target.value;
+ }
+ });
+ });
}
escapeHtml(str) {
diff --git a/public/styles/main.css b/public/styles/main.css
index 3ec0bf1..a47dff7 100644
--- a/public/styles/main.css
+++ b/public/styles/main.css
@@ -2244,8 +2244,6 @@ p {
}
.endpoint-form {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.5rem 0.75rem;
margin-top: 0.5rem;
}
@@ -3794,6 +3792,77 @@ html {
font-family: 'Courier New', monospace !important;
}
+/* Number range slider styles */
+.number-range-container {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.range-slider {
+ width: 100%;
+ height: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ outline: none;
+ cursor: pointer;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.range-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ background: var(--accent-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid var(--bg-secondary);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.range-slider::-moz-range-thumb {
+ width: 18px;
+ height: 18px;
+ background: var(--accent-primary);
+ border-radius: 50%;
+ cursor: pointer;
+ border: 2px solid var(--bg-secondary);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.range-slider::-webkit-slider-track {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ height: 6px;
+}
+
+.range-slider::-moz-range-track {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 3px;
+ height: 6px;
+ border: none;
+}
+
+.range-display {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+}
+
+.range-value {
+ font-weight: 600;
+ color: var(--accent-primary);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.range-max {
+ opacity: 0.7;
+}
+
.color-rgb-display:focus {
border-color: rgba(139, 92, 246, 0.5) !important;
background: rgba(139, 92, 246, 0.08) !important;
diff --git a/test/mock-server.js b/test/mock-server.js
index 1eec2dd..d8e9542 100644
--- a/test/mock-server.js
+++ b/test/mock-server.js
@@ -137,7 +137,62 @@ class MockDataGenerator {
{ uri: "/api/tasks/control", method: "POST" },
{ uri: "/api/cluster/members", method: "GET" },
{ uri: "/api/node/update", method: "POST" },
- { uri: "/api/node/restart", method: "POST" }
+ { uri: "/api/node/restart", method: "POST" },
+ {
+ uri: "/api/led/brightness",
+ method: "POST",
+ params: [
+ {
+ name: "brightness",
+ type: "numberRange",
+ location: "body",
+ required: true,
+ value: 255,
+ default: 128
+ }
+ ]
+ },
+ {
+ uri: "/api/led/color",
+ method: "POST",
+ params: [
+ {
+ name: "color",
+ type: "color",
+ location: "body",
+ required: true,
+ default: 16711680
+ }
+ ]
+ },
+ {
+ uri: "/api/sensor/interval",
+ method: "POST",
+ params: [
+ {
+ name: "interval",
+ type: "numberRange",
+ location: "body",
+ required: true,
+ value: 10000,
+ default: 1000
+ }
+ ]
+ },
+ {
+ uri: "/api/system/mode",
+ method: "POST",
+ params: [
+ {
+ name: "mode",
+ type: "string",
+ location: "body",
+ required: true,
+ values: ["normal", "debug", "maintenance"],
+ default: "normal"
+ }
+ ]
+ }
];
}