netbox-toolkit-plugin 0.1.1__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. netbox_toolkit_plugin/__init__.py +1 -1
  2. netbox_toolkit_plugin/admin.py +11 -7
  3. netbox_toolkit_plugin/api/mixins.py +20 -16
  4. netbox_toolkit_plugin/api/schemas.py +53 -74
  5. netbox_toolkit_plugin/api/serializers.py +10 -11
  6. netbox_toolkit_plugin/api/urls.py +2 -1
  7. netbox_toolkit_plugin/api/views/__init__.py +4 -3
  8. netbox_toolkit_plugin/api/views/command_logs.py +80 -73
  9. netbox_toolkit_plugin/api/views/commands.py +140 -134
  10. netbox_toolkit_plugin/connectors/__init__.py +9 -9
  11. netbox_toolkit_plugin/connectors/base.py +30 -31
  12. netbox_toolkit_plugin/connectors/factory.py +22 -26
  13. netbox_toolkit_plugin/connectors/netmiko_connector.py +18 -28
  14. netbox_toolkit_plugin/connectors/scrapli_connector.py +17 -16
  15. netbox_toolkit_plugin/exceptions.py +0 -7
  16. netbox_toolkit_plugin/filtersets.py +26 -42
  17. netbox_toolkit_plugin/forms.py +13 -11
  18. netbox_toolkit_plugin/migrations/0008_remove_parsed_data_storage.py +26 -0
  19. netbox_toolkit_plugin/models.py +2 -17
  20. netbox_toolkit_plugin/navigation.py +3 -0
  21. netbox_toolkit_plugin/search.py +12 -9
  22. netbox_toolkit_plugin/services/__init__.py +1 -1
  23. netbox_toolkit_plugin/services/command_service.py +7 -10
  24. netbox_toolkit_plugin/services/device_service.py +40 -32
  25. netbox_toolkit_plugin/services/rate_limiting_service.py +4 -3
  26. netbox_toolkit_plugin/{config.py → settings.py} +17 -7
  27. netbox_toolkit_plugin/static/netbox_toolkit_plugin/js/toolkit.js +245 -119
  28. netbox_toolkit_plugin/tables.py +10 -1
  29. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/commandlog.html +16 -84
  30. netbox_toolkit_plugin/templates/netbox_toolkit_plugin/device_toolkit.html +37 -33
  31. netbox_toolkit_plugin/urls.py +10 -3
  32. netbox_toolkit_plugin/utils/connection.py +54 -54
  33. netbox_toolkit_plugin/utils/error_parser.py +128 -109
  34. netbox_toolkit_plugin/utils/logging.py +1 -0
  35. netbox_toolkit_plugin/utils/network.py +74 -47
  36. netbox_toolkit_plugin/views.py +51 -22
  37. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/METADATA +2 -2
  38. netbox_toolkit_plugin-0.1.3.dist-info/RECORD +61 -0
  39. netbox_toolkit_plugin-0.1.1.dist-info/RECORD +0 -60
  40. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/WHEEL +0 -0
  41. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/entry_points.txt +0 -0
  42. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/licenses/LICENSE +0 -0
  43. {netbox_toolkit_plugin-0.1.1.dist-info → netbox_toolkit_plugin-0.1.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * NetBox Toolkit Plugin - Consolidated JavaScript
3
- *
3
+ *
4
4
  * This file contains all the JavaScript functionality for the NetBox Toolkit plugin
5
5
  * to avoid duplication across templates and improve maintainability.
6
6
  */
@@ -8,15 +8,15 @@
8
8
  // Namespace for the toolkit functionality
9
9
  window.NetBoxToolkit = window.NetBoxToolkit || {};
10
10
 
11
- (function(Toolkit) {
11
+ (function (Toolkit) {
12
12
  'use strict';
13
-
13
+
14
14
  // Prevent multiple initialization
15
15
  if (Toolkit.initialized) {
16
16
  console.log('NetBox Toolkit already initialized, skipping');
17
17
  return;
18
18
  }
19
-
19
+
20
20
  /**
21
21
  * Utility functions
22
22
  */
@@ -24,16 +24,16 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
24
24
  /**
25
25
  * Show success state on a button temporarily
26
26
  */
27
- showButtonSuccess: function(btn, successText = '<i class="mdi mdi-check me-1"></i>Copied!', duration = 2000) {
27
+ showButtonSuccess: function (btn, successText = '<i class="mdi mdi-check me-1"></i>Copied!', duration = 2000) {
28
28
  const originalText = btn.innerHTML;
29
29
  const originalClass = btn.className;
30
-
30
+
31
31
  btn.classList.add('copied');
32
32
  btn.innerHTML = successText;
33
33
  btn.style.backgroundColor = 'var(--tblr-success)';
34
34
  btn.style.borderColor = 'var(--tblr-success)';
35
35
  btn.style.color = 'white';
36
-
36
+
37
37
  setTimeout(() => {
38
38
  btn.className = originalClass;
39
39
  btn.innerHTML = originalText;
@@ -42,18 +42,18 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
42
42
  btn.style.color = '';
43
43
  }, duration);
44
44
  },
45
-
45
+
46
46
  /**
47
47
  * Fallback text copy using document.execCommand (legacy browsers)
48
48
  */
49
- fallbackCopyText: function(text, btn) {
49
+ fallbackCopyText: function (text, btn) {
50
50
  const textArea = document.createElement('textarea');
51
51
  textArea.value = text;
52
52
  textArea.style.position = 'fixed';
53
53
  textArea.style.left = '-999999px';
54
54
  textArea.style.top = '-999999px';
55
55
  document.body.appendChild(textArea);
56
-
56
+
57
57
  try {
58
58
  textArea.focus();
59
59
  textArea.select();
@@ -72,7 +72,7 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
72
72
  }
73
73
  }
74
74
  };
75
-
75
+
76
76
  /**
77
77
  * Copy functionality for both parsed data and raw output
78
78
  * Works with multiple element ID patterns for flexibility
@@ -81,45 +81,51 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
81
81
  /**
82
82
  * Initialize copy functionality for both parsed data and raw output buttons
83
83
  */
84
- init: function() {
84
+ init: function () {
85
85
  // Initialize parsed data copy buttons
86
86
  const copyParsedBtns = document.querySelectorAll('.copy-parsed-btn');
87
87
  copyParsedBtns.forEach(btn => {
88
88
  btn.addEventListener('click', this.handleCopyParsedData.bind(this));
89
89
  });
90
-
90
+
91
91
  // Initialize raw output copy buttons
92
92
  const copyOutputBtns = document.querySelectorAll('.copy-output-btn');
93
93
  copyOutputBtns.forEach(btn => {
94
94
  btn.addEventListener('click', this.handleCopyRawOutput.bind(this));
95
95
  });
96
+
97
+ // Initialize CSV download buttons
98
+ const downloadCsvBtns = document.querySelectorAll('.download-csv-btn');
99
+ downloadCsvBtns.forEach(btn => {
100
+ btn.addEventListener('click', this.handleDownloadCSV.bind(this));
101
+ });
96
102
  },
97
-
103
+
98
104
  /**
99
105
  * Handle copying raw command output from pre elements
100
106
  */
101
- handleCopyRawOutput: function(event) {
107
+ handleCopyRawOutput: function (event) {
102
108
  const btn = event.target.closest('.copy-output-btn');
103
109
  if (!btn) return;
104
-
110
+
105
111
  // Find the command output element
106
112
  // Look for .command-output within the same tab pane or nearby
107
113
  const tabPane = btn.closest('.tab-pane') || btn.closest('.card-body') || document;
108
114
  const outputElement = tabPane.querySelector('.command-output');
109
-
115
+
110
116
  if (!outputElement) {
111
117
  console.error('No command output element found');
112
118
  alert('No command output found to copy');
113
119
  return;
114
120
  }
115
-
121
+
116
122
  const outputText = outputElement.textContent || outputElement.innerText;
117
123
  if (!outputText || !outputText.trim()) {
118
124
  console.error('No command output text found');
119
125
  alert('No command output available to copy');
120
126
  return;
121
127
  }
122
-
128
+
123
129
  // Use modern Clipboard API if available
124
130
  if (navigator.clipboard && window.isSecureContext) {
125
131
  navigator.clipboard.writeText(outputText.trim()).then(() => {
@@ -133,44 +139,44 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
133
139
  Toolkit.Utils.fallbackCopyText(outputText.trim(), btn);
134
140
  }
135
141
  },
136
-
142
+
137
143
  /**
138
144
  * Handle copying parsed data from JSON script elements
139
145
  */
140
- handleCopyParsedData: function(event) {
146
+ handleCopyParsedData: function (event) {
141
147
  const btn = event.target.closest('.copy-parsed-btn');
142
148
  if (!btn) return;
143
-
149
+
144
150
  // Try multiple possible element IDs for parsed data
145
151
  const possibleIds = [
146
152
  'parsed-data-json', // device_toolkit.html
147
153
  'commandlog-parsed-data-json' // commandlog.html
148
154
  ];
149
-
155
+
150
156
  let parsedDataElement = null;
151
157
  for (const id of possibleIds) {
152
158
  parsedDataElement = document.getElementById(id);
153
159
  if (parsedDataElement) break;
154
160
  }
155
-
161
+
156
162
  if (!parsedDataElement) {
157
163
  console.error('No parsed data script element found with IDs:', possibleIds);
158
164
  alert('No parsed data found to copy');
159
165
  return;
160
166
  }
161
-
167
+
162
168
  const parsedDataStr = parsedDataElement.textContent;
163
169
  if (!parsedDataStr) {
164
170
  console.error('No parsed data found to copy');
165
171
  alert('No parsed data available');
166
172
  return;
167
173
  }
168
-
174
+
169
175
  try {
170
176
  // Parse and re-stringify for clean formatting
171
177
  const parsedData = JSON.parse(parsedDataStr);
172
178
  const formattedJson = JSON.stringify(parsedData, null, 2);
173
-
179
+
174
180
  // Use modern Clipboard API if available
175
181
  if (navigator.clipboard && window.isSecureContext) {
176
182
  navigator.clipboard.writeText(formattedJson).then(() => {
@@ -187,22 +193,142 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
187
193
  console.error('Error processing parsed data:', err);
188
194
  alert('Failed to process parsed data for copying: ' + err.message);
189
195
  }
196
+ },
197
+
198
+ /**
199
+ * Handle downloading parsed data as CSV
200
+ */
201
+ handleDownloadCSV: function (event) {
202
+ const btn = event.target.closest('.download-csv-btn');
203
+ if (!btn) return;
204
+
205
+ // Try multiple possible element IDs for parsed data
206
+ const possibleIds = [
207
+ 'parsed-data-json', // device_toolkit.html
208
+ 'commandlog-parsed-data-json' // commandlog.html
209
+ ];
210
+
211
+ let parsedDataElement = null;
212
+ for (const id of possibleIds) {
213
+ parsedDataElement = document.getElementById(id);
214
+ if (parsedDataElement) break;
215
+ }
216
+
217
+ if (!parsedDataElement) {
218
+ console.error('No parsed data script element found with IDs:', possibleIds);
219
+ alert('No parsed data found to download');
220
+ return;
221
+ }
222
+
223
+ const parsedDataStr = parsedDataElement.textContent;
224
+ if (!parsedDataStr) {
225
+ console.error('No parsed data found to download');
226
+ alert('No parsed data available');
227
+ return;
228
+ }
229
+
230
+ try {
231
+ const parsedData = JSON.parse(parsedDataStr);
232
+ const csvContent = this.convertToCSV(parsedData);
233
+
234
+ // Create and trigger download
235
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
236
+ const link = document.createElement('a');
237
+
238
+ if (link.download !== undefined) { // Feature detection
239
+ const url = URL.createObjectURL(blob);
240
+ link.setAttribute('href', url);
241
+ link.setAttribute('download', 'parsed_data.csv');
242
+ link.style.visibility = 'hidden';
243
+ document.body.appendChild(link);
244
+ link.click();
245
+ document.body.removeChild(link);
246
+
247
+ // Show success feedback
248
+ Toolkit.Utils.showButtonSuccess(btn, '<i class="mdi mdi-check me-1"></i>Downloaded!');
249
+ } else {
250
+ // Fallback for older browsers
251
+ alert('CSV download not supported in your browser');
252
+ }
253
+ } catch (err) {
254
+ console.error('Error processing parsed data for CSV:', err);
255
+ alert('Failed to process parsed data for CSV download: ' + err.message);
256
+ }
257
+ },
258
+
259
+ /**
260
+ * Convert JSON data to CSV format
261
+ */
262
+ convertToCSV: function (data) {
263
+ if (!data) return '';
264
+
265
+ // Handle different types of data
266
+ if (Array.isArray(data) && data.length > 0) {
267
+ if (typeof data[0] === 'object' && data[0] !== null) {
268
+ // Array of objects - structured data
269
+ const headers = Object.keys(data[0]);
270
+ const csvRows = [];
271
+
272
+ // Add header row
273
+ csvRows.push(headers.map(header => this.escapeCSVField(header)).join(','));
274
+
275
+ // Add data rows
276
+ data.forEach(row => {
277
+ const values = headers.map(header => {
278
+ const value = row[header];
279
+ return this.escapeCSVField(value);
280
+ });
281
+ csvRows.push(values.join(','));
282
+ });
283
+
284
+ return csvRows.join('\n');
285
+ } else {
286
+ // Array of simple values
287
+ return 'Value\n' + data.map(item => this.escapeCSVField(item)).join('\n');
288
+ }
289
+ } else if (typeof data === 'object' && data !== null) {
290
+ // Single object - convert to key-value pairs
291
+ const csvRows = ['Key,Value'];
292
+ Object.entries(data).forEach(([key, value]) => {
293
+ csvRows.push(`${this.escapeCSVField(key)},${this.escapeCSVField(value)}`);
294
+ });
295
+ return csvRows.join('\n');
296
+ } else {
297
+ // Simple value
298
+ return 'Data\n' + this.escapeCSVField(data);
299
+ }
300
+ },
301
+
302
+ /**
303
+ * Escape CSV field values
304
+ */
305
+ escapeCSVField: function (field) {
306
+ if (field === null || field === undefined) return '';
307
+
308
+ const stringField = String(field);
309
+
310
+ // If field contains comma, quote, or newline, wrap in quotes and escape quotes
311
+ if (stringField.includes(',') || stringField.includes('"') || stringField.includes('\n') || stringField.includes('\r')) {
312
+ return '"' + stringField.replace(/"/g, '""') + '"';
313
+ }
314
+
315
+ return stringField;
190
316
  }
191
317
  };
192
-
318
+
193
319
  /**
194
320
  * Modal management for device toolkit
195
321
  */
196
322
  Toolkit.ModalManager = {
197
323
  instance: null,
198
-
324
+
199
325
  /**
200
326
  * Initialize modal functionality
201
327
  */
202
- init: function() {
328
+ init: function () {
203
329
  const credentialModal = document.getElementById('credentialModal');
204
330
  if (!credentialModal) return;
205
-
331
+
206
332
  // Try Bootstrap modal first, fallback to manual control
207
333
  try {
208
334
  if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
@@ -216,56 +342,56 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
216
342
  console.error('Bootstrap modal initialization failed:', error);
217
343
  this.instance = this.createManualModal(credentialModal);
218
344
  }
219
-
345
+
220
346
  this.setupModalEvents(credentialModal);
221
347
  },
222
-
348
+
223
349
  /**
224
350
  * Create manual modal controls for fallback
225
351
  */
226
- createManualModal: function(modalElement) {
352
+ createManualModal: function (modalElement) {
227
353
  return {
228
- show: function() {
354
+ show: function () {
229
355
  modalElement.style.display = 'block';
230
356
  modalElement.classList.add('show');
231
357
  document.body.classList.add('modal-open');
232
-
358
+
233
359
  // Create backdrop
234
360
  const backdrop = document.createElement('div');
235
361
  backdrop.className = 'modal-backdrop fade show';
236
362
  backdrop.id = 'credentialModalBackdrop';
237
363
  document.body.appendChild(backdrop);
238
-
364
+
239
365
  // Trigger shown event
240
366
  const shownEvent = new Event('shown.bs.modal');
241
367
  modalElement.dispatchEvent(shownEvent);
242
368
  },
243
- hide: function() {
369
+ hide: function () {
244
370
  modalElement.style.display = 'none';
245
371
  modalElement.classList.remove('show');
246
372
  document.body.classList.remove('modal-open');
247
-
373
+
248
374
  // Remove backdrop
249
375
  const backdrop = document.getElementById('credentialModalBackdrop');
250
376
  if (backdrop) {
251
377
  backdrop.remove();
252
378
  }
253
-
379
+
254
380
  // Trigger hidden event
255
381
  const hiddenEvent = new Event('hidden.bs.modal');
256
382
  modalElement.dispatchEvent(hiddenEvent);
257
383
  }
258
384
  };
259
385
  },
260
-
386
+
261
387
  /**
262
388
  * Setup modal event handlers
263
389
  */
264
- setupModalEvents: function(credentialModal) {
390
+ setupModalEvents: function (credentialModal) {
265
391
  // Handle close button clicks
266
392
  const modalCloseButton = credentialModal.querySelector('.btn-close');
267
393
  const modalCancelButton = credentialModal.querySelector('.btn-secondary');
268
-
394
+
269
395
  if (modalCloseButton) {
270
396
  modalCloseButton.addEventListener('click', (event) => {
271
397
  event.preventDefault();
@@ -274,7 +400,7 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
274
400
  }
275
401
  });
276
402
  }
277
-
403
+
278
404
  if (modalCancelButton) {
279
405
  modalCancelButton.addEventListener('click', (event) => {
280
406
  event.preventDefault();
@@ -283,7 +409,7 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
283
409
  }
284
410
  });
285
411
  }
286
-
412
+
287
413
  // Handle backdrop clicks for manual modal
288
414
  credentialModal.addEventListener('click', (event) => {
289
415
  if (event.target === credentialModal && this.instance) {
@@ -291,112 +417,112 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
291
417
  }
292
418
  });
293
419
  },
294
-
420
+
295
421
  /**
296
422
  * Show the modal
297
423
  */
298
- show: function() {
424
+ show: function () {
299
425
  if (this.instance) {
300
426
  this.instance.show();
301
427
  }
302
428
  },
303
-
429
+
304
430
  /**
305
431
  * Hide the modal
306
432
  */
307
- hide: function() {
433
+ hide: function () {
308
434
  if (this.instance) {
309
435
  this.instance.hide();
310
436
  }
311
437
  }
312
438
  };
313
-
439
+
314
440
  /**
315
441
  * Command execution functionality for device toolkit
316
442
  */
317
443
  Toolkit.CommandManager = {
318
444
  currentCommandData: null,
319
-
445
+
320
446
  /**
321
447
  * Initialize command execution functionality
322
448
  */
323
- init: function() {
449
+ init: function () {
324
450
  // Initialize modal first
325
451
  Toolkit.ModalManager.init();
326
-
452
+
327
453
  // Setup collapse toggle for connection info
328
454
  this.setupConnectionInfoToggle();
329
-
455
+
330
456
  // Setup command execution
331
457
  this.setupCommandExecution();
332
-
458
+
333
459
  // Setup modal form handlers
334
460
  this.setupModalForm();
335
461
  },
336
-
462
+
337
463
  /**
338
464
  * Setup connection info collapse toggle
339
465
  */
340
- setupConnectionInfoToggle: function() {
466
+ setupConnectionInfoToggle: function () {
341
467
  const connectionInfoCollapse = document.getElementById('connectionInfoCollapse');
342
468
  const connectionInfoToggleButton = document.querySelector('[data-bs-target="#connectionInfoCollapse"]');
343
-
469
+
344
470
  if (connectionInfoCollapse && connectionInfoToggleButton) {
345
471
  connectionInfoCollapse.addEventListener('hidden.bs.collapse', function () {
346
472
  connectionInfoToggleButton.classList.add('collapsed');
347
473
  });
348
-
474
+
349
475
  connectionInfoCollapse.addEventListener('shown.bs.collapse', function () {
350
476
  connectionInfoToggleButton.classList.remove('collapsed');
351
477
  });
352
478
  }
353
479
  },
354
-
480
+
355
481
  /**
356
482
  * Setup command execution event handlers
357
483
  */
358
- setupCommandExecution: function() {
484
+ setupCommandExecution: function () {
359
485
  const commandContainer = document.querySelector('.card-commands');
360
486
  if (!commandContainer) {
361
487
  console.error('Command container not found');
362
488
  return;
363
489
  }
364
-
490
+
365
491
  // Use event delegation to avoid duplicate listeners
366
492
  commandContainer.removeEventListener('click', this.handleRunButtonClick.bind(this));
367
493
  commandContainer.addEventListener('click', this.handleRunButtonClick.bind(this));
368
494
  },
369
-
495
+
370
496
  /**
371
497
  * Handle run button clicks
372
498
  */
373
- handleRunButtonClick: function(event) {
499
+ handleRunButtonClick: function (event) {
374
500
  // Only handle clicks on run buttons
375
501
  const runButton = event.target.closest('.command-run-btn');
376
502
  if (!runButton) return;
377
-
503
+
378
504
  // Prevent any other event handlers from running
379
505
  event.preventDefault();
380
506
  event.stopImmediatePropagation();
381
-
507
+
382
508
  console.log('Run button clicked:', runButton);
383
-
509
+
384
510
  // Prevent double-clicks by checking if already processing
385
511
  if (runButton.dataset.processing === 'true') {
386
512
  console.log('Command already processing, ignoring click');
387
513
  return;
388
514
  }
389
-
515
+
390
516
  // Get the command item and set active state
391
517
  const commandItem = runButton.closest('.command-item');
392
518
  const allCommandItems = document.querySelectorAll('.command-item');
393
519
  allCommandItems.forEach(ci => ci.classList.remove('active'));
394
520
  commandItem.classList.add('active');
395
-
521
+
396
522
  // Store command data for modal
397
523
  const commandId = runButton.getAttribute('data-command-id');
398
524
  const commandName = runButton.getAttribute('data-command-name');
399
-
525
+
400
526
  this.currentCommandData = {
401
527
  id: commandId,
402
528
  name: commandName,
@@ -404,22 +530,22 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
404
530
  button: runButton,
405
531
  originalIconClass: runButton.querySelector('i').className
406
532
  };
407
-
533
+
408
534
  // Update modal content
409
535
  const commandToExecuteElement = document.getElementById('commandToExecute');
410
536
  if (commandToExecuteElement) {
411
537
  commandToExecuteElement.textContent = commandName;
412
538
  }
413
-
539
+
414
540
  // Clear previous credentials and show modal
415
541
  const modalUsernameField = document.getElementById('modalUsername');
416
542
  const modalPasswordField = document.getElementById('modalPassword');
417
543
  if (modalUsernameField) modalUsernameField.value = '';
418
544
  if (modalPasswordField) modalPasswordField.value = '';
419
-
545
+
420
546
  // Show the modal
421
547
  Toolkit.ModalManager.show();
422
-
548
+
423
549
  // Focus on username field when modal is shown
424
550
  const credentialModal = document.getElementById('credentialModal');
425
551
  if (credentialModal && modalUsernameField) {
@@ -428,21 +554,21 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
428
554
  }, { once: true });
429
555
  }
430
556
  },
431
-
557
+
432
558
  /**
433
559
  * Setup modal form handlers
434
560
  */
435
- setupModalForm: function() {
561
+ setupModalForm: function () {
436
562
  const executeCommandBtn = document.getElementById('executeCommandBtn');
437
563
  const modalUsernameField = document.getElementById('modalUsername');
438
564
  const modalPasswordField = document.getElementById('modalPassword');
439
565
  const credentialModal = document.getElementById('credentialModal');
440
-
566
+
441
567
  if (!executeCommandBtn) return;
442
-
568
+
443
569
  // Handle execute button click in modal
444
570
  executeCommandBtn.addEventListener('click', this.executeCommand.bind(this));
445
-
571
+
446
572
  // Handle Enter key in modal form
447
573
  if (modalPasswordField) {
448
574
  modalPasswordField.addEventListener('keypress', (event) => {
@@ -452,7 +578,7 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
452
578
  }
453
579
  });
454
580
  }
455
-
581
+
456
582
  if (modalUsernameField) {
457
583
  modalUsernameField.addEventListener('keypress', (event) => {
458
584
  if (event.key === 'Enter') {
@@ -463,7 +589,7 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
463
589
  }
464
590
  });
465
591
  }
466
-
592
+
467
593
  // Clean up when modal is hidden
468
594
  if (credentialModal) {
469
595
  credentialModal.addEventListener('hidden.bs.modal', () => {
@@ -476,63 +602,63 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
476
602
  });
477
603
  }
478
604
  },
479
-
605
+
480
606
  /**
481
607
  * Execute the selected command
482
608
  */
483
- executeCommand: function() {
609
+ executeCommand: function () {
484
610
  const modalUsernameField = document.getElementById('modalUsername');
485
611
  const modalPasswordField = document.getElementById('modalPassword');
486
-
612
+
487
613
  // Validate credentials
488
614
  if (!modalUsernameField?.value || !modalPasswordField?.value) {
489
615
  alert('Please enter both username and password');
490
616
  return;
491
617
  }
492
-
618
+
493
619
  if (!this.currentCommandData) {
494
620
  alert('No command selected');
495
621
  return;
496
622
  }
497
-
623
+
498
624
  console.log('Executing command:', this.currentCommandData);
499
-
625
+
500
626
  // Set processing flag and update UI
501
627
  this.setCommandProcessing(true);
502
-
628
+
503
629
  // Update output area
504
630
  this.showCommandRunning();
505
-
631
+
506
632
  // Prepare and submit form
507
633
  this.submitCommandForm();
508
634
  },
509
-
635
+
510
636
  /**
511
637
  * Set command processing state
512
638
  */
513
- setCommandProcessing: function(processing) {
639
+ setCommandProcessing: function (processing) {
514
640
  if (!this.currentCommandData) return;
515
-
641
+
516
642
  const button = this.currentCommandData.button;
517
643
  const buttonIcon = button.querySelector('i');
518
-
644
+
519
645
  button.dataset.processing = processing.toString();
520
646
  button.disabled = processing;
521
-
647
+
522
648
  if (processing) {
523
649
  buttonIcon.className = 'mdi mdi-loading mdi-spin';
524
650
  } else {
525
651
  buttonIcon.className = this.currentCommandData.originalIconClass;
526
652
  }
527
653
  },
528
-
654
+
529
655
  /**
530
656
  * Show command running state in output area
531
657
  */
532
- showCommandRunning: function() {
658
+ showCommandRunning: function () {
533
659
  const outputContainer = document.getElementById('commandOutputContainer');
534
660
  const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
535
-
661
+
536
662
  if (outputContainer) {
537
663
  outputContainer.innerHTML = `
538
664
  <div class="alert alert-primary d-flex align-items-center mb-0">
@@ -544,44 +670,44 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
544
670
  </div>
545
671
  `;
546
672
  }
547
-
673
+
548
674
  // Update command header to show executing command
549
675
  if (commandHeader) {
550
676
  commandHeader.textContent = `Executing: ${this.currentCommandData.name}`;
551
677
  }
552
678
  },
553
-
679
+
554
680
  /**
555
681
  * Submit the command execution form
556
682
  */
557
- submitCommandForm: function() {
683
+ submitCommandForm: function () {
558
684
  const commandForm = document.getElementById('commandExecutionForm');
559
685
  const selectedCommandIdField = document.getElementById('selectedCommandId');
560
686
  const formUsernameField = document.getElementById('formUsername');
561
687
  const formPasswordField = document.getElementById('formPassword');
562
688
  const modalUsernameField = document.getElementById('modalUsername');
563
689
  const modalPasswordField = document.getElementById('modalPassword');
564
-
690
+
565
691
  if (!commandForm) {
566
692
  console.error('Command form not found');
567
693
  this.handleSubmissionError('Form not found');
568
694
  return;
569
695
  }
570
-
696
+
571
697
  // Prepare form data
572
698
  if (selectedCommandIdField) selectedCommandIdField.value = this.currentCommandData.id;
573
699
  if (formUsernameField) formUsernameField.value = modalUsernameField.value;
574
700
  if (formPasswordField) formPasswordField.value = modalPasswordField.value;
575
-
701
+
576
702
  console.log('Form data:', {
577
703
  commandId: selectedCommandIdField?.value,
578
704
  username: formUsernameField?.value,
579
705
  hasPassword: !!formPasswordField?.value
580
706
  });
581
-
707
+
582
708
  // Close modal
583
709
  Toolkit.ModalManager.hide();
584
-
710
+
585
711
  try {
586
712
  commandForm.submit();
587
713
  } catch (error) {
@@ -589,20 +715,20 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
589
715
  this.handleSubmissionError('Error submitting form. Please check the console for details.');
590
716
  }
591
717
  },
592
-
718
+
593
719
  /**
594
720
  * Handle form submission errors
595
721
  */
596
- handleSubmissionError: function(message) {
722
+ handleSubmissionError: function (message) {
597
723
  alert(message);
598
-
724
+
599
725
  // Reset processing state
600
726
  this.setCommandProcessing(false);
601
-
727
+
602
728
  // Show error in output container
603
729
  const outputContainer = document.getElementById('commandOutputContainer');
604
730
  const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
605
-
731
+
606
732
  if (outputContainer) {
607
733
  outputContainer.innerHTML = `
608
734
  <div class="alert alert-danger d-flex align-items-start mb-3">
@@ -617,41 +743,41 @@ window.NetBoxToolkit = window.NetBoxToolkit || {};
617
743
  </div>
618
744
  `;
619
745
  }
620
-
746
+
621
747
  // Clear command header
622
748
  if (commandHeader) {
623
749
  commandHeader.textContent = '';
624
750
  }
625
751
  }
626
752
  };
627
-
753
+
628
754
  /**
629
755
  * Main initialization function
630
756
  */
631
- Toolkit.init = function() {
757
+ Toolkit.init = function () {
632
758
  console.log('Initializing NetBox Toolkit JavaScript');
633
-
759
+
634
760
  // Initialize copy functionality (available on all pages)
635
761
  this.CopyManager.init();
636
-
762
+
637
763
  // Initialize command functionality only if elements exist (device toolkit page)
638
764
  const commandForm = document.getElementById('commandExecutionForm');
639
765
  if (commandForm) {
640
766
  this.CommandManager.init();
641
767
  }
642
-
768
+
643
769
  this.initialized = true;
644
770
  console.log('NetBox Toolkit JavaScript initialized successfully');
645
771
  };
646
-
772
+
647
773
  // Auto-initialize when DOM is ready
648
774
  if (document.readyState === 'loading') {
649
- document.addEventListener('DOMContentLoaded', function() {
775
+ document.addEventListener('DOMContentLoaded', function () {
650
776
  Toolkit.init();
651
777
  });
652
778
  } else {
653
779
  // DOM is already ready
654
780
  Toolkit.init();
655
781
  }
656
-
782
+
657
783
  })(window.NetBoxToolkit);