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 { ? `
${ep.params.map((p, pidx) => ` `).join('')}
` : '
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" + } + ] + } ]; }