refactor: remove capabilities in favor of endpoints
This commit is contained in:
6
index.js
6
index.js
@@ -466,7 +466,7 @@ app.get('/api/node/status', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
|
// Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=)
|
||||||
app.get('/api/node/capabilities', async (req, res) => {
|
app.get('/api/node/endpoints', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { ip } = req.query;
|
const { ip } = req.query;
|
||||||
|
|
||||||
@@ -476,9 +476,9 @@ app.get('/api/node/capabilities', async (req, res) => {
|
|||||||
const caps = await nodeClient.getCapabilities();
|
const caps = await nodeClient.getCapabilities();
|
||||||
return res.json(caps);
|
return res.json(caps);
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
console.error('Error fetching capabilities from specific node:', innerError);
|
console.error('Error fetching endpoints from specific node:', innerError);
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: 'Failed to fetch capabilities from node',
|
error: 'Failed to fetch endpoints from node',
|
||||||
message: innerError.message
|
message: innerError.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,11 +80,11 @@ class ApiClient {
|
|||||||
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCapabilities(ip) {
|
async getEndpoints(ip) {
|
||||||
return this.request('/api/node/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
|
return this.request('/api/node/endpoints', { method: 'GET', query: ip ? { ip } : undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
async callCapability({ ip, method, uri, params }) {
|
async callEndpoint({ ip, method, uri, params }) {
|
||||||
return this.request('/api/proxy-call', {
|
return this.request('/api/proxy-call', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { ip, method, uri, params }
|
body: { ip, method, uri, params }
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
|
this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this));
|
||||||
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
|
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
|
||||||
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
|
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
|
||||||
this.subscribeToProperty('capabilities', this.handleCapabilitiesUpdate.bind(this));
|
this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle node status update with state preservation
|
// Handle node status update with state preservation
|
||||||
handleNodeStatusUpdate(newStatus, previousStatus) {
|
handleNodeStatusUpdate(newStatus, previousStatus) {
|
||||||
if (newStatus && !this.viewModel.get('isLoading')) {
|
if (newStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('capabilities'));
|
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
handleTasksUpdate(newTasks, previousTasks) {
|
handleTasksUpdate(newTasks, previousTasks) {
|
||||||
const nodeStatus = this.viewModel.get('nodeStatus');
|
const nodeStatus = this.viewModel.get('nodeStatus');
|
||||||
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('capabilities'));
|
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +48,12 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.updateActiveTab(newTab, previousTab);
|
this.updateActiveTab(newTab, previousTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle capabilities update with state preservation
|
// Handle endpoints update with state preservation
|
||||||
handleCapabilitiesUpdate(newCapabilities, previousCapabilities) {
|
handleEndpointsUpdate(newEndpoints, previousEndpoints) {
|
||||||
const nodeStatus = this.viewModel.get('nodeStatus');
|
const nodeStatus = this.viewModel.get('nodeStatus');
|
||||||
const tasks = this.viewModel.get('tasks');
|
const tasks = this.viewModel.get('tasks');
|
||||||
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
if (nodeStatus && !this.viewModel.get('isLoading')) {
|
||||||
this.renderNodeDetails(nodeStatus, tasks, newCapabilities);
|
this.renderNodeDetails(nodeStatus, tasks, newEndpoints);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
const tasks = this.viewModel.get('tasks');
|
const tasks = this.viewModel.get('tasks');
|
||||||
const isLoading = this.viewModel.get('isLoading');
|
const isLoading = this.viewModel.get('isLoading');
|
||||||
const error = this.viewModel.get('error');
|
const error = this.viewModel.get('error');
|
||||||
const capabilities = this.viewModel.get('capabilities');
|
const endpoints = this.viewModel.get('endpoints');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
|
||||||
@@ -79,10 +79,10 @@ class NodeDetailsComponent extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.renderNodeDetails(nodeStatus, tasks, capabilities);
|
this.renderNodeDetails(nodeStatus, tasks, endpoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNodeDetails(nodeStatus, tasks, capabilities) {
|
renderNodeDetails(nodeStatus, tasks, endpoints) {
|
||||||
// Use persisted active tab from the view model, default to 'status'
|
// Use persisted active tab from the view model, default to 'status'
|
||||||
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
|
||||||
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
|
||||||
@@ -92,7 +92,6 @@ class NodeDetailsComponent extends Component {
|
|||||||
<div class="tabs-header">
|
<div class="tabs-header">
|
||||||
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
<button class="tab-button ${activeTab === 'status' ? 'active' : ''}" data-tab="status">Status</button>
|
||||||
<button class="tab-button ${activeTab === 'endpoints' ? 'active' : ''}" data-tab="endpoints">Endpoints</button>
|
<button class="tab-button ${activeTab === 'endpoints' ? 'active' : ''}" data-tab="endpoints">Endpoints</button>
|
||||||
<button class="tab-button ${activeTab === 'capabilities' ? 'active' : ''}" data-tab="capabilities">Capabilities</button>
|
|
||||||
<button class="tab-button ${activeTab === 'tasks' ? 'active' : ''}" data-tab="tasks">Tasks</button>
|
<button class="tab-button ${activeTab === 'tasks' ? 'active' : ''}" data-tab="tasks">Tasks</button>
|
||||||
<button class="tab-button ${activeTab === 'firmware' ? 'active' : ''}" data-tab="firmware">Firmware</button>
|
<button class="tab-button ${activeTab === 'firmware' ? 'active' : ''}" data-tab="firmware">Firmware</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,14 +120,9 @@ class NodeDetailsComponent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
|
||||||
${nodeStatus.api ? nodeStatus.api.map(endpoint =>
|
${this.renderEndpointsTab(endpoints)}
|
||||||
`<div class="endpoint-item">${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}</div>`
|
|
||||||
).join('') : '<div class="endpoint-item">No API endpoints available</div>'}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'capabilities' ? 'active' : ''}" id="capabilities-tab">
|
|
||||||
${this.renderCapabilitiesTab(capabilities)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content ${activeTab === 'tasks' ? 'active' : ''}" id="tasks-tab">
|
<div class="tab-content ${activeTab === 'tasks' ? 'active' : ''}" id="tasks-tab">
|
||||||
${this.renderTasksTab(tasks)}
|
${this.renderTasksTab(tasks)}
|
||||||
@@ -150,18 +144,18 @@ class NodeDetailsComponent extends Component {
|
|||||||
this.setupFirmwareUpload();
|
this.setupFirmwareUpload();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCapabilitiesTab(capabilities) {
|
renderEndpointsTab(endpoints) {
|
||||||
if (!capabilities || !Array.isArray(capabilities.endpoints) || capabilities.endpoints.length === 0) {
|
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
|
||||||
return `
|
return `
|
||||||
<div class="no-capabilities">
|
<div class="no-endpoints">
|
||||||
<div>🧩 No capabilities reported</div>
|
<div>🧩 No endpoints reported</div>
|
||||||
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any capabilities</div>
|
<div style="font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.7;">This node did not return any endpoints</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort endpoints by URI (name), then by method for stable ordering
|
// Sort endpoints by URI (name), then by method for stable ordering
|
||||||
const endpoints = [...capabilities.endpoints].sort((a, b) => {
|
const endpointsList = [...endpoints.endpoints].sort((a, b) => {
|
||||||
const aUri = String(a.uri || '').toLowerCase();
|
const aUri = String(a.uri || '').toLowerCase();
|
||||||
const bUri = String(b.uri || '').toLowerCase();
|
const bUri = String(b.uri || '').toLowerCase();
|
||||||
if (aUri < bUri) return -1;
|
if (aUri < bUri) return -1;
|
||||||
@@ -171,22 +165,22 @@ class NodeDetailsComponent extends Component {
|
|||||||
return aMethod.localeCompare(bMethod);
|
return aMethod.localeCompare(bMethod);
|
||||||
});
|
});
|
||||||
|
|
||||||
const total = endpoints.length;
|
const total = endpointsList.length;
|
||||||
|
|
||||||
// Preserve selection based on a stable key of method+uri if available
|
// Preserve selection based on a stable key of method+uri if available
|
||||||
const selectedKey = String(this.getUIState('capSelectedKey') || '');
|
const selectedKey = String(this.getUIState('endpointSelectedKey') || '');
|
||||||
let selectedIndex = endpoints.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
|
let selectedIndex = endpointsList.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey);
|
||||||
if (selectedIndex === -1) {
|
if (selectedIndex === -1) {
|
||||||
selectedIndex = Number(this.getUIState('capSelectedIndex'));
|
selectedIndex = Number(this.getUIState('endpointSelectedIndex'));
|
||||||
if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) {
|
if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) {
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute padding for aligned display in dropdown
|
// Compute padding for aligned display in dropdown
|
||||||
const maxMethodLen = endpoints.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
|
const maxMethodLen = endpointsList.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0);
|
||||||
|
|
||||||
const selectorOptions = endpoints.map((ep, idx) => {
|
const selectorOptions = endpointsList.map((ep, idx) => {
|
||||||
const method = String(ep.method || '');
|
const method = String(ep.method || '');
|
||||||
const uri = String(ep.uri || '');
|
const uri = String(ep.uri || '');
|
||||||
const padCount = Math.max(1, (maxMethodLen - method.length) + 2);
|
const padCount = Math.max(1, (maxMethodLen - method.length) + 2);
|
||||||
@@ -194,12 +188,12 @@ class NodeDetailsComponent extends Component {
|
|||||||
return `<option value="${idx}" data-method="${method}" data-uri="${uri}" ${idx === selectedIndex ? 'selected' : ''}>${method}${spacer}${uri}</option>`;
|
return `<option value="${idx}" data-method="${method}" data-uri="${uri}" ${idx === selectedIndex ? 'selected' : ''}>${method}${spacer}${uri}</option>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
const items = endpoints.map((ep, idx) => {
|
const items = endpointsList.map((ep, idx) => {
|
||||||
const formId = `cap-form-${idx}`;
|
const formId = `endpoint-form-${idx}`;
|
||||||
const resultId = `cap-result-${idx}`;
|
const resultId = `endpoint-result-${idx}`;
|
||||||
const params = Array.isArray(ep.params) && ep.params.length > 0
|
const params = Array.isArray(ep.params) && ep.params.length > 0
|
||||||
? `<div class="capability-params">${ep.params.map((p, pidx) => `
|
? `<div class="endpoint-params">${ep.params.map((p, pidx) => `
|
||||||
<label class="capability-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)
|
${ (Array.isArray(p.values) && p.values.length > 1)
|
||||||
? `<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>`
|
? `<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>`
|
||||||
@@ -207,54 +201,54 @@ class NodeDetailsComponent extends Component {
|
|||||||
}
|
}
|
||||||
</label>
|
</label>
|
||||||
`).join('')}</div>`
|
`).join('')}</div>`
|
||||||
: '<div class="capability-params none">No parameters</div>';
|
: '<div class="endpoint-params none">No parameters</div>';
|
||||||
return `
|
return `
|
||||||
<div class="capability-item" data-cap-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
|
<div class="endpoint-item" data-endpoint-index="${idx}" style="display:${idx === selectedIndex ? '' : 'none'};">
|
||||||
<div class="capability-header">
|
<div class="endpoint-header">
|
||||||
<span class="cap-method">${ep.method}</span>
|
<span class="endpoint-method">${ep.method}</span>
|
||||||
<span class="cap-uri">${ep.uri}</span>
|
<span class="endpoint-uri">${ep.uri}</span>
|
||||||
<button class="cap-call-btn" data-action="call-capability" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
|
<button class="endpoint-call-btn" data-action="call-endpoint" data-method="${ep.method}" data-uri="${ep.uri}" data-form-id="${formId}" data-result-id="${resultId}">Call</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="${formId}" class="capability-form" onsubmit="return false;">
|
<form id="${formId}" class="endpoint-form" onsubmit="return false;">
|
||||||
${params}
|
${params}
|
||||||
</form>
|
</form>
|
||||||
<div id="${resultId}" class="capability-result" style="display:none;"></div>
|
<div id="${resultId}" class="endpoint-result" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Attach events after render in setupCapabilitiesEvents()
|
// Attach events after render in setupEndpointsEvents()
|
||||||
setTimeout(() => this.setupCapabilitiesEvents(), 0);
|
setTimeout(() => this.setupEndpointsEvents(), 0);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="capability-selector">
|
<div class="endpoint-selector">
|
||||||
<label class="param-name" for="capability-select">Capability</label>
|
<label class="param-name" for="endpoint-select">Endpoint</label>
|
||||||
<select id="capability-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
|
<select id="endpoint-select" class="param-input" style="font-family: monospace;">${selectorOptions}</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="capabilities-list">${items}</div>
|
<div class="endpoints-list">${items}</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
setupCapabilitiesEvents() {
|
setupEndpointsEvents() {
|
||||||
const selector = this.findElement('#capability-select');
|
const selector = this.findElement('#endpoint-select');
|
||||||
if (selector) {
|
if (selector) {
|
||||||
this.addEventListener(selector, 'change', (e) => {
|
this.addEventListener(selector, 'change', (e) => {
|
||||||
const selected = Number(e.target.value);
|
const selected = Number(e.target.value);
|
||||||
const items = Array.from(this.findAllElements('.capability-item'));
|
const items = Array.from(this.findAllElements('.endpoint-item'));
|
||||||
items.forEach((el, idx) => {
|
items.forEach((el, idx) => {
|
||||||
el.style.display = (idx === selected) ? '' : 'none';
|
el.style.display = (idx === selected) ? '' : 'none';
|
||||||
});
|
});
|
||||||
this.setUIState('capSelectedIndex', selected);
|
this.setUIState('endpointSelectedIndex', selected);
|
||||||
const opt = e.target.selectedOptions && e.target.selectedOptions[0];
|
const opt = e.target.selectedOptions && e.target.selectedOptions[0];
|
||||||
if (opt) {
|
if (opt) {
|
||||||
const method = opt.dataset.method || '';
|
const method = opt.dataset.method || '';
|
||||||
const uri = opt.dataset.uri || '';
|
const uri = opt.dataset.uri || '';
|
||||||
this.setUIState('capSelectedKey', `${method} ${uri}`);
|
this.setUIState('endpointSelectedKey', `${method} ${uri}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons = this.findAllElements('.cap-call-btn');
|
const buttons = this.findAllElements('.endpoint-call-btn');
|
||||||
buttons.forEach(btn => {
|
buttons.forEach(btn => {
|
||||||
this.addEventListener(btn, 'click', async (e) => {
|
this.addEventListener(btn, 'click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -281,7 +275,7 @@ class NodeDetailsComponent extends Component {
|
|||||||
if (missing.length > 0) {
|
if (missing.length > 0) {
|
||||||
resultEl.style.display = 'block';
|
resultEl.style.display = 'block';
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-error">
|
<div class="endpoint-call-error">
|
||||||
<div>❌ Missing required fields: ${missing.map(m => this.escapeHtml(m.name)).join(', ')}</div>
|
<div>❌ Missing required fields: ${missing.map(m => this.escapeHtml(m.name)).join(', ')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -293,17 +287,17 @@ class NodeDetailsComponent extends Component {
|
|||||||
resultEl.innerHTML = '<div class="loading">Calling endpoint...</div>';
|
resultEl.innerHTML = '<div class="loading">Calling endpoint...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.viewModel.callCapability(method, uri, params);
|
const response = await this.viewModel.callEndpoint(method, uri, params);
|
||||||
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
|
const pretty = typeof response?.data === 'object' ? JSON.stringify(response.data, null, 2) : String(response?.data ?? '');
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-success">
|
<div class="endpoint-call-success">
|
||||||
<div>✅ Success</div>
|
<div>✅ Success</div>
|
||||||
<pre class="cap-result-pre">${this.escapeHtml(pretty)}</pre>
|
<pre class="endpoint-result-pre">${this.escapeHtml(pretty)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resultEl.innerHTML = `
|
resultEl.innerHTML = `
|
||||||
<div class="cap-call-error">
|
<div class="endpoint-call-error">
|
||||||
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
<div>❌ Error: ${this.escapeHtml(err.message || 'Request failed')}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
error: null,
|
error: null,
|
||||||
activeTab: 'status',
|
activeTab: 'status',
|
||||||
nodeIp: null,
|
nodeIp: null,
|
||||||
capabilities: null,
|
endpoints: null,
|
||||||
tasksSummary: null
|
tasksSummary: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -247,8 +247,8 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
// Load tasks data
|
// Load tasks data
|
||||||
await this.loadTasksData();
|
await this.loadTasksData();
|
||||||
|
|
||||||
// Load capabilities data
|
// Load endpoints data
|
||||||
await this.loadCapabilitiesData();
|
await this.loadEndpointsData();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load node details:', error);
|
console.error('Failed to load node details:', error);
|
||||||
@@ -272,22 +272,22 @@ class NodeDetailsViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load capabilities data with state preservation
|
// Load endpoints data with state preservation
|
||||||
async loadCapabilitiesData() {
|
async loadEndpointsData() {
|
||||||
try {
|
try {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
const response = await window.apiClient.getCapabilities(ip);
|
const response = await window.apiClient.getEndpoints(ip);
|
||||||
this.set('capabilities', response || null);
|
this.set('endpoints', response || null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load capabilities:', error);
|
console.error('Failed to load endpoints:', error);
|
||||||
this.set('capabilities', null);
|
this.set('endpoints', null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invoke a capability against this node
|
// Invoke an endpoint against this node
|
||||||
async callCapability(method, uri, params) {
|
async callEndpoint(method, uri, params) {
|
||||||
const ip = this.get('nodeIp');
|
const ip = this.get('nodeIp');
|
||||||
return window.apiClient.callCapability({ ip, method, uri, params });
|
return window.apiClient.callEndpoint({ ip, method, uri, params });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set active tab with state persistence
|
// Set active tab with state persistence
|
||||||
|
|||||||
@@ -2003,21 +2003,21 @@ p {
|
|||||||
|
|
||||||
|
|
||||||
/* Capabilities Styles */
|
/* Capabilities Styles */
|
||||||
.capabilities-list {
|
.endpoints-list {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom dropdown wrapper and arrow for capability selector */
|
/* Custom dropdown wrapper and arrow for endpoint selector */
|
||||||
.capability-selector {
|
.endpoint-selector {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#capability-select {
|
#endpoint-select {
|
||||||
padding-right: 2rem;
|
padding-right: 2rem;
|
||||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ecf0f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>');
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23ecf0f1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>');
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
@@ -2025,21 +2025,21 @@ p {
|
|||||||
background-size: 12px 12px;
|
background-size: 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-item {
|
.endpoint-item {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-header {
|
.endpoint-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-method {
|
.endpoint-method {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -2050,14 +2050,14 @@ p {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-uri {
|
.endpoint-uri {
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-call-btn {
|
.endpoint-call-btn {
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
||||||
border: 1px solid var(--border-secondary);
|
border: 1px solid var(--border-secondary);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -2070,21 +2070,21 @@ p {
|
|||||||
backdrop-filter: var(--backdrop-blur);
|
backdrop-filter: var(--backdrop-blur);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-call-btn:hover {
|
.endpoint-call-btn:hover {
|
||||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.08) 100%);
|
||||||
border-color: rgba(255, 255, 255, 0.25);
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-form {
|
.endpoint-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-param {
|
.endpoint-param {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
@@ -2124,13 +2124,13 @@ p {
|
|||||||
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15);
|
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-params.none {
|
.endpoint-params.none {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
padding: 0.25rem 0.25rem 0 0.25rem;
|
padding: 0.25rem 0.25rem 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capability-result {
|
.endpoint-result {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
@@ -2138,7 +2138,7 @@ p {
|
|||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-call-success {
|
.endpoint-call-success {
|
||||||
color: var(--accent-success);
|
color: var(--accent-success);
|
||||||
background: rgba(76, 175, 80, 0.1);
|
background: rgba(76, 175, 80, 0.1);
|
||||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||||
@@ -2146,7 +2146,7 @@ p {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-call-error {
|
.endpoint-call-error {
|
||||||
color: #f44336;
|
color: #f44336;
|
||||||
background: rgba(244, 67, 54, 0.1);
|
background: rgba(244, 67, 54, 0.1);
|
||||||
border: 1px solid rgba(244, 67, 54, 0.2);
|
border: 1px solid rgba(244, 67, 54, 0.2);
|
||||||
@@ -2154,7 +2154,7 @@ p {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cap-result-pre {
|
.endpoint-result-pre {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -2321,11 +2321,11 @@ p {
|
|||||||
|
|
||||||
/* Responsive Capability Selector */
|
/* Responsive Capability Selector */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.capability-selector {
|
.endpoint-selector {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
#capability-select {
|
#endpoint-select {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -2334,23 +2334,23 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#capability-select {
|
#endpoint-select {
|
||||||
padding-right: 1.75rem;
|
padding-right: 1.75rem;
|
||||||
background-position: right 0.5rem center;
|
background-position: right 0.5rem center;
|
||||||
background-size: 10px 10px;
|
background-size: 10px 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Capability header mobile wrapping */
|
/* Endpoint header mobile wrapping */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.capability-header {
|
.endpoint-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
.cap-uri {
|
.endpoint-uri {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.cap-call-btn {
|
.endpoint-call-btn {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -2359,7 +2359,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.cap-call-btn {
|
.endpoint-call-btn {
|
||||||
padding: 0.35rem 0.75rem;
|
padding: 0.35rem 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,11 +85,11 @@ class SporeApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get node capabilities
|
* Get node endpoints
|
||||||
* @returns {Promise<Object>} Capabilities response
|
* @returns {Promise<Object>} endpoints response
|
||||||
*/
|
*/
|
||||||
async getCapabilities() {
|
async getCapabilities() {
|
||||||
return this.request('GET', '/api/node/capabilities');
|
return this.request('GET', '/api/node/endpoints');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user