feat: introduce global config dialog #19
@@ -103,6 +103,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cluster-header-right">
|
<div class="cluster-header-right">
|
||||||
|
<button class="config-btn" id="config-wifi-btn" title="Configure WiFi settings for visible nodes">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
|
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
Config
|
||||||
|
</button>
|
||||||
<button class="deploy-btn" id="deploy-firmware-btn" title="Deploy firmware to visible nodes">
|
<button class="deploy-btn" id="deploy-firmware-btn" title="Deploy firmware to visible nodes">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px;">
|
||||||
<path d="M12 16V4"/>
|
<path d="M12 16V4"/>
|
||||||
@@ -266,6 +273,7 @@
|
|||||||
<script src="./scripts/components/OverlayDialogComponent.js"></script>
|
<script src="./scripts/components/OverlayDialogComponent.js"></script>
|
||||||
<script src="./scripts/components/FirmwareComponent.js"></script>
|
<script src="./scripts/components/FirmwareComponent.js"></script>
|
||||||
<script src="./scripts/components/FirmwareUploadComponent.js"></script>
|
<script src="./scripts/components/FirmwareUploadComponent.js"></script>
|
||||||
|
<script src="./scripts/components/WiFiConfigComponent.js"></script>
|
||||||
<!-- Container/view components after their deps -->
|
<!-- Container/view components after their deps -->
|
||||||
<script src="./scripts/components/FirmwareViewComponent.js"></script>
|
<script src="./scripts/components/FirmwareViewComponent.js"></script>
|
||||||
<script src="./scripts/components/ClusterViewComponent.js"></script>
|
<script src="./scripts/components/ClusterViewComponent.js"></script>
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ class ClusterViewComponent extends Component {
|
|||||||
// Set up deploy button event listener
|
// Set up deploy button event listener
|
||||||
this.setupDeployButton();
|
this.setupDeployButton();
|
||||||
|
|
||||||
|
// Set up config button event listener
|
||||||
|
this.setupConfigButton();
|
||||||
|
|
||||||
// Initialize overlay dialog
|
// Initialize overlay dialog
|
||||||
this.initializeOverlayDialog();
|
this.initializeOverlayDialog();
|
||||||
|
|
||||||
@@ -112,6 +115,23 @@ class ClusterViewComponent extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupConfigButton() {
|
||||||
|
logger.debug('ClusterViewComponent: Setting up config button...');
|
||||||
|
|
||||||
|
const configBtn = this.findElement('#config-wifi-btn');
|
||||||
|
logger.debug('ClusterViewComponent: Found config button:', !!configBtn, configBtn);
|
||||||
|
|
||||||
|
if (configBtn) {
|
||||||
|
logger.debug('ClusterViewComponent: Adding click event listener to config button');
|
||||||
|
this.addEventListener(configBtn, 'click', this.handleConfig.bind(this));
|
||||||
|
logger.debug('ClusterViewComponent: Event listener added successfully');
|
||||||
|
} else {
|
||||||
|
logger.error('ClusterViewComponent: Config button not found!');
|
||||||
|
logger.debug('ClusterViewComponent: Container HTML:', this.container.innerHTML);
|
||||||
|
logger.debug('ClusterViewComponent: All buttons in container:', this.container.querySelectorAll('button'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initializeOverlayDialog() {
|
initializeOverlayDialog() {
|
||||||
// Create overlay container if it doesn't exist
|
// Create overlay container if it doesn't exist
|
||||||
let overlayContainer = document.getElementById('cluster-overlay-dialog');
|
let overlayContainer = document.getElementById('cluster-overlay-dialog');
|
||||||
@@ -160,6 +180,28 @@ class ClusterViewComponent extends Component {
|
|||||||
this.openFirmwareUploadDrawer(filteredMembers);
|
this.openFirmwareUploadDrawer(filteredMembers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleConfig() {
|
||||||
|
logger.debug('ClusterViewComponent: Config button clicked, opening WiFi config drawer...');
|
||||||
|
|
||||||
|
// Get current filtered members from cluster members component
|
||||||
|
const filteredMembers = this.clusterMembersComponent ? this.clusterMembersComponent.getFilteredMembers() : [];
|
||||||
|
|
||||||
|
if (!filteredMembers || filteredMembers.length === 0) {
|
||||||
|
this.showConfirmationDialog({
|
||||||
|
title: 'No Nodes Available',
|
||||||
|
message: 'No nodes available for WiFi configuration. Please ensure cluster members are loaded and visible.',
|
||||||
|
confirmText: 'OK',
|
||||||
|
cancelText: null,
|
||||||
|
onConfirm: () => {},
|
||||||
|
onCancel: null
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open drawer with WiFi configuration interface
|
||||||
|
this.openWiFiConfigDrawer(filteredMembers);
|
||||||
|
}
|
||||||
|
|
||||||
openFirmwareUploadDrawer(targetNodes) {
|
openFirmwareUploadDrawer(targetNodes) {
|
||||||
logger.debug('ClusterViewComponent: Opening firmware upload drawer for', targetNodes.length, 'nodes');
|
logger.debug('ClusterViewComponent: Opening firmware upload drawer for', targetNodes.length, 'nodes');
|
||||||
|
|
||||||
@@ -255,6 +297,81 @@ class ClusterViewComponent extends Component {
|
|||||||
}, true); // Hide terminal button for firmware upload
|
}, true); // Hide terminal button for firmware upload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openWiFiConfigDrawer(targetNodes) {
|
||||||
|
logger.debug('ClusterViewComponent: Opening WiFi config drawer for', targetNodes.length, 'nodes');
|
||||||
|
|
||||||
|
// Get display name for drawer title
|
||||||
|
const nodeCount = targetNodes.length;
|
||||||
|
const displayName = `Configuration - ${nodeCount} node${nodeCount !== 1 ? 's' : ''}`;
|
||||||
|
|
||||||
|
// Open drawer with content callback (hide terminal button for WiFi config)
|
||||||
|
this.clusterMembersComponent.drawer.openDrawer(displayName, (contentContainer, setActiveComponent) => {
|
||||||
|
// Create WiFi config view model and component
|
||||||
|
const wifiConfigVM = new WiFiConfigViewModel();
|
||||||
|
wifiConfigVM.setTargetNodes(targetNodes);
|
||||||
|
|
||||||
|
// Create HTML for WiFi configuration interface
|
||||||
|
contentContainer.innerHTML = `
|
||||||
|
<div class="wifi-config-drawer">
|
||||||
|
<div class="tabs-container">
|
||||||
|
<div class="tabs-header">
|
||||||
|
<button class="tab-button active" data-tab="wifi">WiFi</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-content active" id="wifi-tab">
|
||||||
|
<div class="wifi-config-section">
|
||||||
|
<div class="wifi-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="wifi-ssid">SSID (Network Name)</label>
|
||||||
|
<input type="text" id="wifi-ssid" placeholder="Enter WiFi network name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="wifi-password">Password</label>
|
||||||
|
<input type="password" id="wifi-password" placeholder="Enter WiFi password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wifi-divider"></div>
|
||||||
|
|
||||||
|
<div class="affected-nodes-info">
|
||||||
|
<div class="nodes-count">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="margin-right:6px; vertical-align: -2px;">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="9" cy="7" r="4"/>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>
|
||||||
|
Affected Nodes: <span id="affected-nodes-count">${targetNodes.length}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wifi-actions">
|
||||||
|
<button class="config-btn" id="apply-wifi-config" disabled>
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wifi-progress-container">
|
||||||
|
<!-- Progress will be shown here during configuration -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create and mount WiFi config component
|
||||||
|
const wifiConfigComponent = new WiFiConfigComponent(contentContainer, wifiConfigVM, this.eventBus);
|
||||||
|
setActiveComponent(wifiConfigComponent);
|
||||||
|
wifiConfigComponent.mount();
|
||||||
|
|
||||||
|
}, null, () => {
|
||||||
|
// Close callback - clear any config state
|
||||||
|
logger.debug('ClusterViewComponent: WiFi config drawer closed');
|
||||||
|
}, true); // Hide terminal button for WiFi config
|
||||||
|
}
|
||||||
|
|
||||||
async handleRefresh() {
|
async handleRefresh() {
|
||||||
logger.debug('ClusterViewComponent: Refresh button clicked, performing full refresh...');
|
logger.debug('ClusterViewComponent: Refresh button clicked, performing full refresh...');
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
(function(){
|
(function(){
|
||||||
// Simple readiness flag once all component constructors are present
|
// Simple readiness flag once all component constructors are present
|
||||||
function allReady(){
|
function allReady(){
|
||||||
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent);
|
return !!(window.PrimaryNodeComponent && window.ClusterMembersComponent && window.NodeDetailsComponent && window.FirmwareComponent && window.ClusterViewComponent && window.FirmwareViewComponent && window.TopologyGraphComponent && window.MemberCardOverlayComponent && window.ClusterStatusComponent && window.DrawerComponent && window.WiFiConfigComponent);
|
||||||
}
|
}
|
||||||
window.waitForComponentsReady = function(timeoutMs = 5000){
|
window.waitForComponentsReady = function(timeoutMs = 5000){
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
313
public/scripts/components/WiFiConfigComponent.js
Normal file
313
public/scripts/components/WiFiConfigComponent.js
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
// WiFi Configuration Component
|
||||||
|
class WiFiConfigComponent extends Component {
|
||||||
|
constructor(container, viewModel, eventBus) {
|
||||||
|
super(container, viewModel, eventBus);
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: Constructor called');
|
||||||
|
logger.debug('WiFiConfigComponent: Container:', container);
|
||||||
|
|
||||||
|
// Track form state
|
||||||
|
this.formValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
logger.debug('WiFiConfigComponent: Mounting...');
|
||||||
|
super.mount();
|
||||||
|
|
||||||
|
this.setupFormValidation();
|
||||||
|
this.setupApplyButton();
|
||||||
|
this.setupProgressDisplay();
|
||||||
|
|
||||||
|
// Initial validation to ensure button starts disabled
|
||||||
|
this.validateForm();
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: Mounted successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFormValidation() {
|
||||||
|
logger.debug('WiFiConfigComponent: Setting up form validation...');
|
||||||
|
|
||||||
|
const ssidInput = this.findElement('#wifi-ssid');
|
||||||
|
const passwordInput = this.findElement('#wifi-password');
|
||||||
|
const applyBtn = this.findElement('#apply-wifi-config');
|
||||||
|
|
||||||
|
if (!ssidInput || !passwordInput || !applyBtn) {
|
||||||
|
logger.error('WiFiConfigComponent: Required form elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add input event listeners
|
||||||
|
this.addEventListener(ssidInput, 'input', this.validateForm.bind(this));
|
||||||
|
this.addEventListener(passwordInput, 'input', this.validateForm.bind(this));
|
||||||
|
|
||||||
|
// Initial validation
|
||||||
|
this.validateForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupApplyButton() {
|
||||||
|
logger.debug('WiFiConfigComponent: Setting up apply button...');
|
||||||
|
|
||||||
|
const applyBtn = this.findElement('#apply-wifi-config');
|
||||||
|
|
||||||
|
if (applyBtn) {
|
||||||
|
this.addEventListener(applyBtn, 'click', this.handleApply.bind(this));
|
||||||
|
logger.debug('WiFiConfigComponent: Apply button event listener added');
|
||||||
|
} else {
|
||||||
|
logger.error('WiFiConfigComponent: Apply button not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupProgressDisplay() {
|
||||||
|
logger.debug('WiFiConfigComponent: Setting up progress display...');
|
||||||
|
|
||||||
|
// Subscribe to view model changes
|
||||||
|
this.viewModel.subscribe('isConfiguring', (isConfiguring) => {
|
||||||
|
this.updateApplyButton(isConfiguring);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewModel.subscribe('configProgress', (progress) => {
|
||||||
|
this.updateProgressDisplay(progress);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewModel.subscribe('configResults', (results) => {
|
||||||
|
this.updateResultsDisplay(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
validateForm() {
|
||||||
|
logger.debug('WiFiConfigComponent: Validating form...');
|
||||||
|
|
||||||
|
const ssidInput = this.findElement('#wifi-ssid');
|
||||||
|
const passwordInput = this.findElement('#wifi-password');
|
||||||
|
const applyBtn = this.findElement('#apply-wifi-config');
|
||||||
|
|
||||||
|
if (!ssidInput || !passwordInput || !applyBtn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssid = ssidInput.value.trim();
|
||||||
|
const password = passwordInput.value.trim();
|
||||||
|
|
||||||
|
this.formValid = ssid.length > 0 && password.length > 0;
|
||||||
|
|
||||||
|
// Update apply button state
|
||||||
|
applyBtn.disabled = !this.formValid;
|
||||||
|
|
||||||
|
// Update view model
|
||||||
|
this.viewModel.setCredentials(ssid, password);
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: Form validation complete. Valid:', this.formValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateApplyButton(isConfiguring) {
|
||||||
|
logger.debug('WiFiConfigComponent: Updating apply button. Configuring:', isConfiguring);
|
||||||
|
|
||||||
|
const applyBtn = this.findElement('#apply-wifi-config');
|
||||||
|
|
||||||
|
if (!applyBtn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isConfiguring) {
|
||||||
|
applyBtn.disabled = true;
|
||||||
|
applyBtn.classList.add('loading');
|
||||||
|
applyBtn.innerHTML = `Apply`;
|
||||||
|
} else {
|
||||||
|
applyBtn.disabled = !this.formValid;
|
||||||
|
applyBtn.classList.remove('loading');
|
||||||
|
applyBtn.innerHTML = `Apply`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressDisplay(progress) {
|
||||||
|
logger.debug('WiFiConfigComponent: Updating progress display:', progress);
|
||||||
|
|
||||||
|
const progressContainer = this.findElement('#wifi-progress-container');
|
||||||
|
|
||||||
|
if (!progressContainer || !progress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { current, total, status } = progress;
|
||||||
|
const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||||
|
|
||||||
|
progressContainer.innerHTML = `
|
||||||
|
<div class="upload-progress-info">
|
||||||
|
<div class="overall-progress">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" id="wifi-progress-bar" style="width: ${percentage}%; background-color: #60a5fa;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">${current}/${total} Configured (${percentage}%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-summary" id="wifi-progress-summary">
|
||||||
|
<span>Status: ${status}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgressBar(totalNodes) {
|
||||||
|
logger.debug('WiFiConfigComponent: Showing initial progress bar for', totalNodes, 'nodes');
|
||||||
|
|
||||||
|
const progressContainer = this.findElement('#wifi-progress-container');
|
||||||
|
|
||||||
|
if (!progressContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressContainer.innerHTML = `
|
||||||
|
<div class="upload-progress-info">
|
||||||
|
<div class="overall-progress">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" id="wifi-progress-bar" style="width: 0%; background-color: #60a5fa;"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-text">0/${totalNodes} Configured (0%)</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-summary" id="wifi-progress-summary">
|
||||||
|
<span>Status: Preparing configuration...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResultsDisplay(results) {
|
||||||
|
logger.debug('WiFiConfigComponent: Updating results display:', results);
|
||||||
|
|
||||||
|
const progressContainer = this.findElement('#wifi-progress-container');
|
||||||
|
|
||||||
|
if (!progressContainer || !results || results.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsHtml = results.map(result => `
|
||||||
|
<div class="result-item ${result.success ? 'success' : 'error'}">
|
||||||
|
<div class="result-node">
|
||||||
|
<span class="node-name">${result.node.hostname || result.node.ip}</span>
|
||||||
|
<span class="node-ip">${result.node.ip}</span>
|
||||||
|
</div>
|
||||||
|
<div class="result-status">
|
||||||
|
<span class="status-indicator ${result.success ? 'success' : 'error'}">
|
||||||
|
${result.success ? 'Success' : 'Failed'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
${result.error ? `<div class="result-error">${this.escapeHtml(result.error)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Append results to existing progress container
|
||||||
|
const existingProgress = progressContainer.querySelector('.upload-progress-info');
|
||||||
|
if (existingProgress) {
|
||||||
|
existingProgress.innerHTML += `
|
||||||
|
<div class="results-section">
|
||||||
|
<div class="results-list">
|
||||||
|
${resultsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleApply() {
|
||||||
|
logger.debug('WiFiConfigComponent: Apply button clicked');
|
||||||
|
|
||||||
|
if (!this.formValid) {
|
||||||
|
logger.warn('WiFiConfigComponent: Form is not valid, cannot apply');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssid = this.findElement('#wifi-ssid').value.trim();
|
||||||
|
const password = this.findElement('#wifi-password').value.trim();
|
||||||
|
const targetNodes = this.viewModel.get('targetNodes');
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: Applying WiFi config to', targetNodes.length, 'nodes');
|
||||||
|
logger.debug('WiFiConfigComponent: SSID:', ssid);
|
||||||
|
|
||||||
|
// Start configuration
|
||||||
|
this.viewModel.startConfiguration();
|
||||||
|
|
||||||
|
// Show initial progress bar
|
||||||
|
this.showProgressBar(targetNodes.length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Update progress
|
||||||
|
this.viewModel.updateConfigProgress(0, targetNodes.length, 'Starting configuration...');
|
||||||
|
|
||||||
|
// Apply configuration to each node
|
||||||
|
for (let i = 0; i < targetNodes.length; i++) {
|
||||||
|
const node = targetNodes[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug('WiFiConfigComponent: Configuring node:', node.ip);
|
||||||
|
|
||||||
|
// Update progress
|
||||||
|
this.viewModel.updateConfigProgress(i + 1, targetNodes.length, `Configuring ${node.hostname || node.ip}...`);
|
||||||
|
|
||||||
|
// Make API call to configure WiFi
|
||||||
|
const result = await this.configureNodeWiFi(node, ssid, password);
|
||||||
|
|
||||||
|
// Add successful result
|
||||||
|
this.viewModel.addConfigResult({
|
||||||
|
node,
|
||||||
|
success: true,
|
||||||
|
error: null
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: Successfully configured node:', node.ip);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('WiFiConfigComponent: Failed to configure node:', node.ip, error);
|
||||||
|
|
||||||
|
// Add failed result
|
||||||
|
this.viewModel.addConfigResult({
|
||||||
|
node,
|
||||||
|
success: false,
|
||||||
|
error: error.message || 'Configuration failed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between requests to avoid overwhelming the nodes
|
||||||
|
if (i < targetNodes.length - 1) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete configuration
|
||||||
|
this.viewModel.updateConfigProgress(targetNodes.length, targetNodes.length, 'Configuration complete');
|
||||||
|
this.viewModel.completeConfiguration();
|
||||||
|
|
||||||
|
logger.debug('WiFiConfigComponent: WiFi configuration completed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('WiFiConfigComponent: WiFi configuration failed:', error);
|
||||||
|
this.viewModel.completeConfiguration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async configureNodeWiFi(node, ssid, password) {
|
||||||
|
logger.debug('WiFiConfigComponent: Configuring WiFi for node:', node.ip);
|
||||||
|
|
||||||
|
const response = await window.apiClient.callEndpoint({
|
||||||
|
ip: node.ip,
|
||||||
|
method: 'POST',
|
||||||
|
uri: '/api/network/wifi/config',
|
||||||
|
params: [
|
||||||
|
{ name: 'ssid', value: ssid, location: 'body' },
|
||||||
|
{ name: 'password', value: password, location: 'body' }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || 'Failed to configure WiFi');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
logger.debug('WiFiConfigComponent: Unmounting...');
|
||||||
|
super.unmount();
|
||||||
|
logger.debug('WiFiConfigComponent: Unmounted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.WiFiConfigComponent = WiFiConfigComponent;
|
||||||
@@ -1031,4 +1031,82 @@ class ClusterFirmwareViewModel extends ViewModel {
|
|||||||
|
|
||||||
return file && targetNodes && targetNodes.length > 0 && !isUploading;
|
return file && targetNodes && targetNodes.length > 0 && !isUploading;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WiFi Configuration View Model
|
||||||
|
class WiFiConfigViewModel extends ViewModel {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.set('targetNodes', []);
|
||||||
|
this.set('ssid', '');
|
||||||
|
this.set('password', '');
|
||||||
|
this.set('isConfiguring', false);
|
||||||
|
this.set('configProgress', null);
|
||||||
|
this.set('configResults', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set target nodes (filtered from cluster view)
|
||||||
|
setTargetNodes(nodes) {
|
||||||
|
this.set('targetNodes', nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set WiFi credentials
|
||||||
|
setCredentials(ssid, password) {
|
||||||
|
this.set('ssid', ssid);
|
||||||
|
this.set('password', password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start configuration
|
||||||
|
startConfiguration() {
|
||||||
|
this.set('isConfiguring', true);
|
||||||
|
this.set('configProgress', {
|
||||||
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
status: 'Preparing...'
|
||||||
|
});
|
||||||
|
this.set('configResults', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update configuration progress
|
||||||
|
updateConfigProgress(current, total, status) {
|
||||||
|
this.set('configProgress', {
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add configuration result
|
||||||
|
addConfigResult(result) {
|
||||||
|
const results = this.get('configResults');
|
||||||
|
results.push(result);
|
||||||
|
this.set('configResults', results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete configuration
|
||||||
|
completeConfiguration() {
|
||||||
|
this.set('isConfiguring', false);
|
||||||
|
this.set('configProgress', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset configuration state
|
||||||
|
resetConfiguration() {
|
||||||
|
this.set('ssid', '');
|
||||||
|
this.set('password', '');
|
||||||
|
this.set('configProgress', null);
|
||||||
|
this.set('configResults', []);
|
||||||
|
this.set('isConfiguring', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if apply is enabled
|
||||||
|
isApplyEnabled() {
|
||||||
|
const ssid = this.get('ssid');
|
||||||
|
const password = this.get('password');
|
||||||
|
const targetNodes = this.get('targetNodes');
|
||||||
|
const isConfiguring = this.get('isConfiguring');
|
||||||
|
|
||||||
|
return ssid && password && targetNodes && targetNodes.length > 0 && !isConfiguring;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.WiFiConfigViewModel = WiFiConfigViewModel;
|
||||||
@@ -401,6 +401,46 @@ p {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Config Button */
|
||||||
|
.config-btn {
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.05) 100%);
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0.75rem 1.25rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
backdrop-filter: var(--backdrop-blur);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn:hover {
|
||||||
|
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);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-btn.loading {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* Deploy Button */
|
/* Deploy Button */
|
||||||
.deploy-btn {
|
.deploy-btn {
|
||||||
background: linear-gradient(135deg, rgba(74, 222, 128, 0.2) 0%, rgba(74, 222, 128, 0.1) 100%);
|
background: linear-gradient(135deg, rgba(74, 222, 128, 0.2) 0%, rgba(74, 222, 128, 0.1) 100%);
|
||||||
@@ -440,6 +480,218 @@ p {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* WiFi Configuration Styles */
|
||||||
|
.wifi-config-drawer {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-config-section {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-config-section h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-form {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #60a5fa;
|
||||||
|
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input::placeholder {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-primary);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.affected-nodes-info {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes-count span {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions .config-btn {
|
||||||
|
background: linear-gradient(135deg, rgba(74, 222, 128, 0.2) 0%, rgba(74, 222, 128, 0.1) 100%);
|
||||||
|
border: 1px solid rgba(74, 222, 128, 0.3);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions .config-btn:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(74, 222, 128, 0.3) 0%, rgba(74, 222, 128, 0.15) 100%);
|
||||||
|
border-color: rgba(74, 222, 128, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(74, 222, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions .config-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions .config-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wifi-actions .config-btn.loading {
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Results Section */
|
||||||
|
.results-section {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
padding-top: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-section h3 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item.success {
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border-color: rgba(34, 197, 94, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item.error {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: rgba(239, 68, 68, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-node {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-node .node-name {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-node .node-ip {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status .status-indicator {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 9999px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status .status-indicator.success {
|
||||||
|
background: rgba(34, 197, 94, 0.2);
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-status .status-indicator.error {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-error {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ef4444;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
/* Cluster Header Right */
|
/* Cluster Header Right */
|
||||||
.cluster-header-right {
|
.cluster-header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -4847,8 +5099,7 @@ select.param-input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clear-filters-btn {
|
.clear-filters-btn {
|
||||||
align-self: center;
|
display: none;
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-node-info {
|
.primary-node-info {
|
||||||
@@ -4879,6 +5130,14 @@ select.param-input:focus {
|
|||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-btn,
|
||||||
|
.deploy-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#cluster-members-container {
|
#cluster-members-container {
|
||||||
@@ -5089,6 +5348,7 @@ html {
|
|||||||
.upload-btn,
|
.upload-btn,
|
||||||
.upload-btn-compact,
|
.upload-btn-compact,
|
||||||
.deploy-btn,
|
.deploy-btn,
|
||||||
|
.config-btn,
|
||||||
.cap-call-btn,
|
.cap-call-btn,
|
||||||
.progress-refresh-btn,
|
.progress-refresh-btn,
|
||||||
.clear-btn,
|
.clear-btn,
|
||||||
@@ -5126,6 +5386,14 @@ html {
|
|||||||
#firmware-view .deploy-btn:hover:not(:disabled)::before {
|
#firmware-view .deploy-btn:hover:not(:disabled)::before {
|
||||||
left: -100% !important;
|
left: -100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Disable hover effects for cluster view buttons on touch devices */
|
||||||
|
.config-btn:hover,
|
||||||
|
.deploy-btn:hover:not(:disabled),
|
||||||
|
.refresh-btn:hover {
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cluster view specific error styling */
|
/* Cluster view specific error styling */
|
||||||
|
|||||||
Reference in New Issue
Block a user