feat: number range slider
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 394 KiB After Width: | Height: | Size: 395 KiB |
@@ -25,6 +25,100 @@ class NodeDetailsComponent extends Component {
|
|||||||
return (r << 16) + (g << 8) + b;
|
return (r << 16) + (g << 8) + b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parameter component renderers
|
||||||
|
renderSelectComponent(p, formId, pidx) {
|
||||||
|
return `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderColorComponent(p, formId, pidx) {
|
||||||
|
const defaultValue = p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0;
|
||||||
|
return `<div class="color-input-container">
|
||||||
|
<input id="${formId}-field-${pidx}"
|
||||||
|
data-param-name="${p.name}"
|
||||||
|
data-param-location="${p.location || 'body'}"
|
||||||
|
data-param-type="${p.type || 'string'}"
|
||||||
|
data-param-required="${p.required ? '1' : '0'}"
|
||||||
|
class="param-input color-picker"
|
||||||
|
type="color"
|
||||||
|
value="${this.rgbIntToHex(defaultValue)}">
|
||||||
|
<input type="text"
|
||||||
|
class="color-rgb-display"
|
||||||
|
readonly
|
||||||
|
value="${defaultValue}"
|
||||||
|
style="margin-left: 5px; width: 80px; font-size: 0.8em;">
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderNumberRangeComponent(p, formId, pidx) {
|
||||||
|
const defaultValue = p.default !== undefined ? p.default : 0;
|
||||||
|
const maxValue = p.value || 100;
|
||||||
|
return `<div class="number-range-container">
|
||||||
|
<input id="${formId}-field-${pidx}"
|
||||||
|
data-param-name="${p.name}"
|
||||||
|
data-param-location="${p.location || 'body'}"
|
||||||
|
data-param-type="${p.type || 'string'}"
|
||||||
|
data-param-required="${p.required ? '1' : '0'}"
|
||||||
|
class="param-input range-slider"
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="${maxValue}"
|
||||||
|
value="${defaultValue}">
|
||||||
|
<div class="range-display">
|
||||||
|
<span class="range-value">${defaultValue}</span>
|
||||||
|
<span class="range-max">/ ${maxValue}</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTextComponent(p, formId, pidx) {
|
||||||
|
const defaultValue = p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : '';
|
||||||
|
return `<input id="${formId}-field-${pidx}"
|
||||||
|
data-param-name="${p.name}"
|
||||||
|
data-param-location="${p.location || 'body'}"
|
||||||
|
data-param-type="${p.type || 'string'}"
|
||||||
|
data-param-required="${p.required ? '1' : '0'}"
|
||||||
|
class="param-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="${p.location || 'body'} • ${p.type || 'string'}"
|
||||||
|
value="${defaultValue}">`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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() {
|
setupViewModelListeners() {
|
||||||
@@ -441,34 +535,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
? `<div class="endpoint-params">${ep.params.map((p, pidx) => `
|
? `<div class="endpoint-params">${ep.params.map((p, pidx) => `
|
||||||
<label class="endpoint-param" for="${formId}-field-${pidx}">
|
<label class="endpoint-param" for="${formId}-field-${pidx}">
|
||||||
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
|
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
|
||||||
${ (Array.isArray(p.values) && p.values.length > 1)
|
${this.renderParameterComponent(p, formId, pidx)}
|
||||||
? `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`
|
|
||||||
: (p.type === 'color')
|
|
||||||
? `<div class="color-input-container">
|
|
||||||
<input id="${formId}-field-${pidx}"
|
|
||||||
data-param-name="${p.name}"
|
|
||||||
data-param-location="${p.location || 'body'}"
|
|
||||||
data-param-type="${p.type || 'string'}"
|
|
||||||
data-param-required="${p.required ? '1' : '0'}"
|
|
||||||
class="param-input color-picker"
|
|
||||||
type="color"
|
|
||||||
value="${this.rgbIntToHex(p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0)}">
|
|
||||||
<input type="text"
|
|
||||||
class="color-rgb-display"
|
|
||||||
readonly
|
|
||||||
value="${p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0}"
|
|
||||||
style="margin-left: 5px; width: 80px; font-size: 0.8em;">
|
|
||||||
</div>`
|
|
||||||
: `<input id="${formId}-field-${pidx}"
|
|
||||||
data-param-name="${p.name}"
|
|
||||||
data-param-location="${p.location || 'body'}"
|
|
||||||
data-param-type="${p.type || 'string'}"
|
|
||||||
data-param-required="${p.required ? '1' : '0'}"
|
|
||||||
class="param-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="${p.location || 'body'} • ${p.type || 'string'}"
|
|
||||||
value="${p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : ''}">`
|
|
||||||
}
|
|
||||||
</label>
|
</label>
|
||||||
`).join('')}</div>`
|
`).join('')}</div>`
|
||||||
: '<div class="endpoint-params none">No parameters</div>';
|
: '<div class="endpoint-params none">No parameters</div>';
|
||||||
@@ -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) {
|
escapeHtml(str) {
|
||||||
|
|||||||
@@ -2244,8 +2244,6 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.endpoint-form {
|
.endpoint-form {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
||||||
gap: 0.5rem 0.75rem;
|
gap: 0.5rem 0.75rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -3794,6 +3792,77 @@ html {
|
|||||||
font-family: 'Courier New', monospace !important;
|
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 {
|
.color-rgb-display:focus {
|
||||||
border-color: rgba(139, 92, 246, 0.5) !important;
|
border-color: rgba(139, 92, 246, 0.5) !important;
|
||||||
background: rgba(139, 92, 246, 0.08) !important;
|
background: rgba(139, 92, 246, 0.08) !important;
|
||||||
|
|||||||
@@ -137,7 +137,62 @@ class MockDataGenerator {
|
|||||||
{ uri: "/api/tasks/control", method: "POST" },
|
{ uri: "/api/tasks/control", method: "POST" },
|
||||||
{ uri: "/api/cluster/members", method: "GET" },
|
{ uri: "/api/cluster/members", method: "GET" },
|
||||||
{ uri: "/api/node/update", method: "POST" },
|
{ 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user