netbox-toolkit-plugin 0.1.0__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 (56) hide show
  1. netbox_toolkit/__init__.py +30 -0
  2. netbox_toolkit/admin.py +16 -0
  3. netbox_toolkit/api/__init__.py +0 -0
  4. netbox_toolkit/api/mixins.py +54 -0
  5. netbox_toolkit/api/schemas.py +234 -0
  6. netbox_toolkit/api/serializers.py +158 -0
  7. netbox_toolkit/api/urls.py +10 -0
  8. netbox_toolkit/api/views/__init__.py +10 -0
  9. netbox_toolkit/api/views/command_logs.py +170 -0
  10. netbox_toolkit/api/views/commands.py +267 -0
  11. netbox_toolkit/config.py +159 -0
  12. netbox_toolkit/connectors/__init__.py +15 -0
  13. netbox_toolkit/connectors/base.py +97 -0
  14. netbox_toolkit/connectors/factory.py +301 -0
  15. netbox_toolkit/connectors/netmiko_connector.py +443 -0
  16. netbox_toolkit/connectors/scrapli_connector.py +486 -0
  17. netbox_toolkit/exceptions.py +36 -0
  18. netbox_toolkit/filtersets.py +85 -0
  19. netbox_toolkit/forms.py +31 -0
  20. netbox_toolkit/migrations/0001_initial.py +54 -0
  21. netbox_toolkit/migrations/0002_alter_command_options_alter_command_unique_together_and_more.py +66 -0
  22. netbox_toolkit/migrations/0003_permission_system_update.py +48 -0
  23. netbox_toolkit/migrations/0004_remove_django_permissions.py +77 -0
  24. netbox_toolkit/migrations/0005_alter_command_options_and_more.py +25 -0
  25. netbox_toolkit/migrations/0006_commandlog_parsed_data_commandlog_parsing_success_and_more.py +28 -0
  26. netbox_toolkit/migrations/0007_alter_commandlog_parsing_template.py +18 -0
  27. netbox_toolkit/migrations/__init__.py +0 -0
  28. netbox_toolkit/models.py +89 -0
  29. netbox_toolkit/navigation.py +30 -0
  30. netbox_toolkit/search.py +21 -0
  31. netbox_toolkit/services/__init__.py +7 -0
  32. netbox_toolkit/services/command_service.py +357 -0
  33. netbox_toolkit/services/device_service.py +87 -0
  34. netbox_toolkit/services/rate_limiting_service.py +228 -0
  35. netbox_toolkit/static/netbox_toolkit/css/toolkit.css +143 -0
  36. netbox_toolkit/static/netbox_toolkit/js/toolkit.js +657 -0
  37. netbox_toolkit/tables.py +37 -0
  38. netbox_toolkit/templates/netbox_toolkit/command.html +108 -0
  39. netbox_toolkit/templates/netbox_toolkit/command_edit.html +10 -0
  40. netbox_toolkit/templates/netbox_toolkit/command_list.html +12 -0
  41. netbox_toolkit/templates/netbox_toolkit/commandlog.html +170 -0
  42. netbox_toolkit/templates/netbox_toolkit/commandlog_list.html +4 -0
  43. netbox_toolkit/templates/netbox_toolkit/device_toolkit.html +536 -0
  44. netbox_toolkit/urls.py +22 -0
  45. netbox_toolkit/utils/__init__.py +1 -0
  46. netbox_toolkit/utils/connection.py +125 -0
  47. netbox_toolkit/utils/error_parser.py +428 -0
  48. netbox_toolkit/utils/logging.py +58 -0
  49. netbox_toolkit/utils/network.py +157 -0
  50. netbox_toolkit/views.py +385 -0
  51. netbox_toolkit_plugin-0.1.0.dist-info/METADATA +76 -0
  52. netbox_toolkit_plugin-0.1.0.dist-info/RECORD +56 -0
  53. netbox_toolkit_plugin-0.1.0.dist-info/WHEEL +5 -0
  54. netbox_toolkit_plugin-0.1.0.dist-info/entry_points.txt +2 -0
  55. netbox_toolkit_plugin-0.1.0.dist-info/licenses/LICENSE +200 -0
  56. netbox_toolkit_plugin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,657 @@
1
+ /**
2
+ * NetBox Toolkit Plugin - Consolidated JavaScript
3
+ *
4
+ * This file contains all the JavaScript functionality for the NetBox Toolkit plugin
5
+ * to avoid duplication across templates and improve maintainability.
6
+ */
7
+
8
+ // Namespace for the toolkit functionality
9
+ window.NetBoxToolkit = window.NetBoxToolkit || {};
10
+
11
+ (function(Toolkit) {
12
+ 'use strict';
13
+
14
+ // Prevent multiple initialization
15
+ if (Toolkit.initialized) {
16
+ console.log('NetBox Toolkit already initialized, skipping');
17
+ return;
18
+ }
19
+
20
+ /**
21
+ * Utility functions
22
+ */
23
+ Toolkit.Utils = {
24
+ /**
25
+ * Show success state on a button temporarily
26
+ */
27
+ showButtonSuccess: function(btn, successText = '<i class="mdi mdi-check me-1"></i>Copied!', duration = 2000) {
28
+ const originalText = btn.innerHTML;
29
+ const originalClass = btn.className;
30
+
31
+ btn.classList.add('copied');
32
+ btn.innerHTML = successText;
33
+ btn.style.backgroundColor = 'var(--tblr-success)';
34
+ btn.style.borderColor = 'var(--tblr-success)';
35
+ btn.style.color = 'white';
36
+
37
+ setTimeout(() => {
38
+ btn.className = originalClass;
39
+ btn.innerHTML = originalText;
40
+ btn.style.backgroundColor = '';
41
+ btn.style.borderColor = '';
42
+ btn.style.color = '';
43
+ }, duration);
44
+ },
45
+
46
+ /**
47
+ * Fallback text copy using document.execCommand (legacy browsers)
48
+ */
49
+ fallbackCopyText: function(text, btn) {
50
+ const textArea = document.createElement('textarea');
51
+ textArea.value = text;
52
+ textArea.style.position = 'fixed';
53
+ textArea.style.left = '-999999px';
54
+ textArea.style.top = '-999999px';
55
+ document.body.appendChild(textArea);
56
+
57
+ try {
58
+ textArea.focus();
59
+ textArea.select();
60
+ const successful = document.execCommand('copy');
61
+ if (successful) {
62
+ this.showButtonSuccess(btn);
63
+ } else {
64
+ console.error('Fallback copy command failed');
65
+ alert('Failed to copy to clipboard');
66
+ }
67
+ } catch (err) {
68
+ console.error('Fallback copy failed:', err);
69
+ alert('Failed to copy to clipboard');
70
+ } finally {
71
+ document.body.removeChild(textArea);
72
+ }
73
+ }
74
+ };
75
+
76
+ /**
77
+ * Copy functionality for both parsed data and raw output
78
+ * Works with multiple element ID patterns for flexibility
79
+ */
80
+ Toolkit.CopyManager = {
81
+ /**
82
+ * Initialize copy functionality for both parsed data and raw output buttons
83
+ */
84
+ init: function() {
85
+ // Initialize parsed data copy buttons
86
+ const copyParsedBtns = document.querySelectorAll('.copy-parsed-btn');
87
+ copyParsedBtns.forEach(btn => {
88
+ btn.addEventListener('click', this.handleCopyParsedData.bind(this));
89
+ });
90
+
91
+ // Initialize raw output copy buttons
92
+ const copyOutputBtns = document.querySelectorAll('.copy-output-btn');
93
+ copyOutputBtns.forEach(btn => {
94
+ btn.addEventListener('click', this.handleCopyRawOutput.bind(this));
95
+ });
96
+ },
97
+
98
+ /**
99
+ * Handle copying raw command output from pre elements
100
+ */
101
+ handleCopyRawOutput: function(event) {
102
+ const btn = event.target.closest('.copy-output-btn');
103
+ if (!btn) return;
104
+
105
+ // Find the command output element
106
+ // Look for .command-output within the same tab pane or nearby
107
+ const tabPane = btn.closest('.tab-pane') || btn.closest('.card-body') || document;
108
+ const outputElement = tabPane.querySelector('.command-output');
109
+
110
+ if (!outputElement) {
111
+ console.error('No command output element found');
112
+ alert('No command output found to copy');
113
+ return;
114
+ }
115
+
116
+ const outputText = outputElement.textContent || outputElement.innerText;
117
+ if (!outputText || !outputText.trim()) {
118
+ console.error('No command output text found');
119
+ alert('No command output available to copy');
120
+ return;
121
+ }
122
+
123
+ // Use modern Clipboard API if available
124
+ if (navigator.clipboard && window.isSecureContext) {
125
+ navigator.clipboard.writeText(outputText.trim()).then(() => {
126
+ Toolkit.Utils.showButtonSuccess(btn);
127
+ }).catch(err => {
128
+ console.error('Failed to copy using Clipboard API:', err);
129
+ Toolkit.Utils.fallbackCopyText(outputText.trim(), btn);
130
+ });
131
+ } else {
132
+ // Fallback for older browsers or non-secure contexts
133
+ Toolkit.Utils.fallbackCopyText(outputText.trim(), btn);
134
+ }
135
+ },
136
+
137
+ /**
138
+ * Handle copying parsed data from JSON script elements
139
+ */
140
+ handleCopyParsedData: function(event) {
141
+ const btn = event.target.closest('.copy-parsed-btn');
142
+ if (!btn) return;
143
+
144
+ // Try multiple possible element IDs for parsed data
145
+ const possibleIds = [
146
+ 'parsed-data-json', // device_toolkit.html
147
+ 'commandlog-parsed-data-json' // commandlog.html
148
+ ];
149
+
150
+ let parsedDataElement = null;
151
+ for (const id of possibleIds) {
152
+ parsedDataElement = document.getElementById(id);
153
+ if (parsedDataElement) break;
154
+ }
155
+
156
+ if (!parsedDataElement) {
157
+ console.error('No parsed data script element found with IDs:', possibleIds);
158
+ alert('No parsed data found to copy');
159
+ return;
160
+ }
161
+
162
+ const parsedDataStr = parsedDataElement.textContent;
163
+ if (!parsedDataStr) {
164
+ console.error('No parsed data found to copy');
165
+ alert('No parsed data available');
166
+ return;
167
+ }
168
+
169
+ try {
170
+ // Parse and re-stringify for clean formatting
171
+ const parsedData = JSON.parse(parsedDataStr);
172
+ const formattedJson = JSON.stringify(parsedData, null, 2);
173
+
174
+ // Use modern Clipboard API if available
175
+ if (navigator.clipboard && window.isSecureContext) {
176
+ navigator.clipboard.writeText(formattedJson).then(() => {
177
+ Toolkit.Utils.showButtonSuccess(btn);
178
+ }).catch(err => {
179
+ console.error('Failed to copy using Clipboard API:', err);
180
+ Toolkit.Utils.fallbackCopyText(formattedJson, btn);
181
+ });
182
+ } else {
183
+ // Fallback for older browsers or non-secure contexts
184
+ Toolkit.Utils.fallbackCopyText(formattedJson, btn);
185
+ }
186
+ } catch (err) {
187
+ console.error('Error processing parsed data:', err);
188
+ alert('Failed to process parsed data for copying: ' + err.message);
189
+ }
190
+ }
191
+ };
192
+
193
+ /**
194
+ * Modal management for device toolkit
195
+ */
196
+ Toolkit.ModalManager = {
197
+ instance: null,
198
+
199
+ /**
200
+ * Initialize modal functionality
201
+ */
202
+ init: function() {
203
+ const credentialModal = document.getElementById('credentialModal');
204
+ if (!credentialModal) return;
205
+
206
+ // Try Bootstrap modal first, fallback to manual control
207
+ try {
208
+ if (typeof bootstrap !== 'undefined' && bootstrap.Modal) {
209
+ this.instance = new bootstrap.Modal(credentialModal);
210
+ console.log('Bootstrap modal initialized successfully');
211
+ } else {
212
+ console.log('Bootstrap not available, using manual modal control');
213
+ this.instance = this.createManualModal(credentialModal);
214
+ }
215
+ } catch (error) {
216
+ console.error('Bootstrap modal initialization failed:', error);
217
+ this.instance = this.createManualModal(credentialModal);
218
+ }
219
+
220
+ this.setupModalEvents(credentialModal);
221
+ },
222
+
223
+ /**
224
+ * Create manual modal controls for fallback
225
+ */
226
+ createManualModal: function(modalElement) {
227
+ return {
228
+ show: function() {
229
+ modalElement.style.display = 'block';
230
+ modalElement.classList.add('show');
231
+ document.body.classList.add('modal-open');
232
+
233
+ // Create backdrop
234
+ const backdrop = document.createElement('div');
235
+ backdrop.className = 'modal-backdrop fade show';
236
+ backdrop.id = 'credentialModalBackdrop';
237
+ document.body.appendChild(backdrop);
238
+
239
+ // Trigger shown event
240
+ const shownEvent = new Event('shown.bs.modal');
241
+ modalElement.dispatchEvent(shownEvent);
242
+ },
243
+ hide: function() {
244
+ modalElement.style.display = 'none';
245
+ modalElement.classList.remove('show');
246
+ document.body.classList.remove('modal-open');
247
+
248
+ // Remove backdrop
249
+ const backdrop = document.getElementById('credentialModalBackdrop');
250
+ if (backdrop) {
251
+ backdrop.remove();
252
+ }
253
+
254
+ // Trigger hidden event
255
+ const hiddenEvent = new Event('hidden.bs.modal');
256
+ modalElement.dispatchEvent(hiddenEvent);
257
+ }
258
+ };
259
+ },
260
+
261
+ /**
262
+ * Setup modal event handlers
263
+ */
264
+ setupModalEvents: function(credentialModal) {
265
+ // Handle close button clicks
266
+ const modalCloseButton = credentialModal.querySelector('.btn-close');
267
+ const modalCancelButton = credentialModal.querySelector('.btn-secondary');
268
+
269
+ if (modalCloseButton) {
270
+ modalCloseButton.addEventListener('click', (event) => {
271
+ event.preventDefault();
272
+ if (this.instance) {
273
+ this.instance.hide();
274
+ }
275
+ });
276
+ }
277
+
278
+ if (modalCancelButton) {
279
+ modalCancelButton.addEventListener('click', (event) => {
280
+ event.preventDefault();
281
+ if (this.instance) {
282
+ this.instance.hide();
283
+ }
284
+ });
285
+ }
286
+
287
+ // Handle backdrop clicks for manual modal
288
+ credentialModal.addEventListener('click', (event) => {
289
+ if (event.target === credentialModal && this.instance) {
290
+ this.instance.hide();
291
+ }
292
+ });
293
+ },
294
+
295
+ /**
296
+ * Show the modal
297
+ */
298
+ show: function() {
299
+ if (this.instance) {
300
+ this.instance.show();
301
+ }
302
+ },
303
+
304
+ /**
305
+ * Hide the modal
306
+ */
307
+ hide: function() {
308
+ if (this.instance) {
309
+ this.instance.hide();
310
+ }
311
+ }
312
+ };
313
+
314
+ /**
315
+ * Command execution functionality for device toolkit
316
+ */
317
+ Toolkit.CommandManager = {
318
+ currentCommandData: null,
319
+
320
+ /**
321
+ * Initialize command execution functionality
322
+ */
323
+ init: function() {
324
+ // Initialize modal first
325
+ Toolkit.ModalManager.init();
326
+
327
+ // Setup collapse toggle for connection info
328
+ this.setupConnectionInfoToggle();
329
+
330
+ // Setup command execution
331
+ this.setupCommandExecution();
332
+
333
+ // Setup modal form handlers
334
+ this.setupModalForm();
335
+ },
336
+
337
+ /**
338
+ * Setup connection info collapse toggle
339
+ */
340
+ setupConnectionInfoToggle: function() {
341
+ const connectionInfoCollapse = document.getElementById('connectionInfoCollapse');
342
+ const connectionInfoToggleButton = document.querySelector('[data-bs-target="#connectionInfoCollapse"]');
343
+
344
+ if (connectionInfoCollapse && connectionInfoToggleButton) {
345
+ connectionInfoCollapse.addEventListener('hidden.bs.collapse', function () {
346
+ connectionInfoToggleButton.classList.add('collapsed');
347
+ });
348
+
349
+ connectionInfoCollapse.addEventListener('shown.bs.collapse', function () {
350
+ connectionInfoToggleButton.classList.remove('collapsed');
351
+ });
352
+ }
353
+ },
354
+
355
+ /**
356
+ * Setup command execution event handlers
357
+ */
358
+ setupCommandExecution: function() {
359
+ const commandContainer = document.querySelector('.card-commands');
360
+ if (!commandContainer) {
361
+ console.error('Command container not found');
362
+ return;
363
+ }
364
+
365
+ // Use event delegation to avoid duplicate listeners
366
+ commandContainer.removeEventListener('click', this.handleRunButtonClick.bind(this));
367
+ commandContainer.addEventListener('click', this.handleRunButtonClick.bind(this));
368
+ },
369
+
370
+ /**
371
+ * Handle run button clicks
372
+ */
373
+ handleRunButtonClick: function(event) {
374
+ // Only handle clicks on run buttons
375
+ const runButton = event.target.closest('.command-run-btn');
376
+ if (!runButton) return;
377
+
378
+ // Prevent any other event handlers from running
379
+ event.preventDefault();
380
+ event.stopImmediatePropagation();
381
+
382
+ console.log('Run button clicked:', runButton);
383
+
384
+ // Prevent double-clicks by checking if already processing
385
+ if (runButton.dataset.processing === 'true') {
386
+ console.log('Command already processing, ignoring click');
387
+ return;
388
+ }
389
+
390
+ // Get the command item and set active state
391
+ const commandItem = runButton.closest('.command-item');
392
+ const allCommandItems = document.querySelectorAll('.command-item');
393
+ allCommandItems.forEach(ci => ci.classList.remove('active'));
394
+ commandItem.classList.add('active');
395
+
396
+ // Store command data for modal
397
+ const commandId = runButton.getAttribute('data-command-id');
398
+ const commandName = runButton.getAttribute('data-command-name');
399
+
400
+ this.currentCommandData = {
401
+ id: commandId,
402
+ name: commandName,
403
+ element: commandItem,
404
+ button: runButton,
405
+ originalIconClass: runButton.querySelector('i').className
406
+ };
407
+
408
+ // Update modal content
409
+ const commandToExecuteElement = document.getElementById('commandToExecute');
410
+ if (commandToExecuteElement) {
411
+ commandToExecuteElement.textContent = commandName;
412
+ }
413
+
414
+ // Clear previous credentials and show modal
415
+ const modalUsernameField = document.getElementById('modalUsername');
416
+ const modalPasswordField = document.getElementById('modalPassword');
417
+ if (modalUsernameField) modalUsernameField.value = '';
418
+ if (modalPasswordField) modalPasswordField.value = '';
419
+
420
+ // Show the modal
421
+ Toolkit.ModalManager.show();
422
+
423
+ // Focus on username field when modal is shown
424
+ const credentialModal = document.getElementById('credentialModal');
425
+ if (credentialModal && modalUsernameField) {
426
+ credentialModal.addEventListener('shown.bs.modal', function () {
427
+ modalUsernameField.focus();
428
+ }, { once: true });
429
+ }
430
+ },
431
+
432
+ /**
433
+ * Setup modal form handlers
434
+ */
435
+ setupModalForm: function() {
436
+ const executeCommandBtn = document.getElementById('executeCommandBtn');
437
+ const modalUsernameField = document.getElementById('modalUsername');
438
+ const modalPasswordField = document.getElementById('modalPassword');
439
+ const credentialModal = document.getElementById('credentialModal');
440
+
441
+ if (!executeCommandBtn) return;
442
+
443
+ // Handle execute button click in modal
444
+ executeCommandBtn.addEventListener('click', this.executeCommand.bind(this));
445
+
446
+ // Handle Enter key in modal form
447
+ if (modalPasswordField) {
448
+ modalPasswordField.addEventListener('keypress', (event) => {
449
+ if (event.key === 'Enter') {
450
+ event.preventDefault();
451
+ executeCommandBtn.click();
452
+ }
453
+ });
454
+ }
455
+
456
+ if (modalUsernameField) {
457
+ modalUsernameField.addEventListener('keypress', (event) => {
458
+ if (event.key === 'Enter') {
459
+ event.preventDefault();
460
+ if (modalPasswordField) {
461
+ modalPasswordField.focus();
462
+ }
463
+ }
464
+ });
465
+ }
466
+
467
+ // Clean up when modal is hidden
468
+ if (credentialModal) {
469
+ credentialModal.addEventListener('hidden.bs.modal', () => {
470
+ if (this.currentCommandData && this.currentCommandData.button.dataset.processing !== 'true') {
471
+ this.currentCommandData.element.classList.remove('active');
472
+ }
473
+ this.currentCommandData = null;
474
+ if (modalUsernameField) modalUsernameField.value = '';
475
+ if (modalPasswordField) modalPasswordField.value = '';
476
+ });
477
+ }
478
+ },
479
+
480
+ /**
481
+ * Execute the selected command
482
+ */
483
+ executeCommand: function() {
484
+ const modalUsernameField = document.getElementById('modalUsername');
485
+ const modalPasswordField = document.getElementById('modalPassword');
486
+
487
+ // Validate credentials
488
+ if (!modalUsernameField?.value || !modalPasswordField?.value) {
489
+ alert('Please enter both username and password');
490
+ return;
491
+ }
492
+
493
+ if (!this.currentCommandData) {
494
+ alert('No command selected');
495
+ return;
496
+ }
497
+
498
+ console.log('Executing command:', this.currentCommandData);
499
+
500
+ // Set processing flag and update UI
501
+ this.setCommandProcessing(true);
502
+
503
+ // Update output area
504
+ this.showCommandRunning();
505
+
506
+ // Prepare and submit form
507
+ this.submitCommandForm();
508
+ },
509
+
510
+ /**
511
+ * Set command processing state
512
+ */
513
+ setCommandProcessing: function(processing) {
514
+ if (!this.currentCommandData) return;
515
+
516
+ const button = this.currentCommandData.button;
517
+ const buttonIcon = button.querySelector('i');
518
+
519
+ button.dataset.processing = processing.toString();
520
+ button.disabled = processing;
521
+
522
+ if (processing) {
523
+ buttonIcon.className = 'mdi mdi-loading mdi-spin';
524
+ } else {
525
+ buttonIcon.className = this.currentCommandData.originalIconClass;
526
+ }
527
+ },
528
+
529
+ /**
530
+ * Show command running state in output area
531
+ */
532
+ showCommandRunning: function() {
533
+ const outputContainer = document.getElementById('commandOutputContainer');
534
+ const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
535
+
536
+ if (outputContainer) {
537
+ outputContainer.innerHTML = `
538
+ <div class="alert alert-primary d-flex align-items-center mb-0">
539
+ <div class="spinner-border spinner-border-sm me-3" role="status" aria-hidden="true" style="width: 1rem; height: 1rem; border-width: 0.125em;"></div>
540
+ <div>
541
+ <strong>Running command:</strong> ${this.currentCommandData.name}<br>
542
+ <small class="text-muted">Please wait while the command executes...</small>
543
+ </div>
544
+ </div>
545
+ `;
546
+ }
547
+
548
+ // Update command header to show executing command
549
+ if (commandHeader) {
550
+ commandHeader.textContent = `Executing: ${this.currentCommandData.name}`;
551
+ }
552
+ },
553
+
554
+ /**
555
+ * Submit the command execution form
556
+ */
557
+ submitCommandForm: function() {
558
+ const commandForm = document.getElementById('commandExecutionForm');
559
+ const selectedCommandIdField = document.getElementById('selectedCommandId');
560
+ const formUsernameField = document.getElementById('formUsername');
561
+ const formPasswordField = document.getElementById('formPassword');
562
+ const modalUsernameField = document.getElementById('modalUsername');
563
+ const modalPasswordField = document.getElementById('modalPassword');
564
+
565
+ if (!commandForm) {
566
+ console.error('Command form not found');
567
+ this.handleSubmissionError('Form not found');
568
+ return;
569
+ }
570
+
571
+ // Prepare form data
572
+ if (selectedCommandIdField) selectedCommandIdField.value = this.currentCommandData.id;
573
+ if (formUsernameField) formUsernameField.value = modalUsernameField.value;
574
+ if (formPasswordField) formPasswordField.value = modalPasswordField.value;
575
+
576
+ console.log('Form data:', {
577
+ commandId: selectedCommandIdField?.value,
578
+ username: formUsernameField?.value,
579
+ hasPassword: !!formPasswordField?.value
580
+ });
581
+
582
+ // Close modal
583
+ Toolkit.ModalManager.hide();
584
+
585
+ try {
586
+ commandForm.submit();
587
+ } catch (error) {
588
+ console.error('Error submitting form:', error);
589
+ this.handleSubmissionError('Error submitting form. Please check the console for details.');
590
+ }
591
+ },
592
+
593
+ /**
594
+ * Handle form submission errors
595
+ */
596
+ handleSubmissionError: function(message) {
597
+ alert(message);
598
+
599
+ // Reset processing state
600
+ this.setCommandProcessing(false);
601
+
602
+ // Show error in output container
603
+ const outputContainer = document.getElementById('commandOutputContainer');
604
+ const commandHeader = document.querySelector('.output-card .card-header span.text-muted');
605
+
606
+ if (outputContainer) {
607
+ outputContainer.innerHTML = `
608
+ <div class="alert alert-danger d-flex align-items-start mb-3">
609
+ <i class="mdi mdi-alert-circle me-2 mt-1"></i>
610
+ <div>
611
+ <strong>Form submission error</strong>
612
+ <br><small class="text-muted">${message}</small>
613
+ </div>
614
+ </div>
615
+ <div class="alert alert-info" id="defaultMessage">
616
+ Select a command to execute from the list on the left.
617
+ </div>
618
+ `;
619
+ }
620
+
621
+ // Clear command header
622
+ if (commandHeader) {
623
+ commandHeader.textContent = '';
624
+ }
625
+ }
626
+ };
627
+
628
+ /**
629
+ * Main initialization function
630
+ */
631
+ Toolkit.init = function() {
632
+ console.log('Initializing NetBox Toolkit JavaScript');
633
+
634
+ // Initialize copy functionality (available on all pages)
635
+ this.CopyManager.init();
636
+
637
+ // Initialize command functionality only if elements exist (device toolkit page)
638
+ const commandForm = document.getElementById('commandExecutionForm');
639
+ if (commandForm) {
640
+ this.CommandManager.init();
641
+ }
642
+
643
+ this.initialized = true;
644
+ console.log('NetBox Toolkit JavaScript initialized successfully');
645
+ };
646
+
647
+ // Auto-initialize when DOM is ready
648
+ if (document.readyState === 'loading') {
649
+ document.addEventListener('DOMContentLoaded', function() {
650
+ Toolkit.init();
651
+ });
652
+ } else {
653
+ // DOM is already ready
654
+ Toolkit.init();
655
+ }
656
+
657
+ })(window.NetBoxToolkit);
@@ -0,0 +1,37 @@
1
+ import django_tables2 as tables
2
+ from netbox.tables import NetBoxTable, columns
3
+ from .models import Command, CommandLog
4
+
5
+ class CommandTable(NetBoxTable):
6
+ name = tables.Column(
7
+ linkify=('plugins:netbox_toolkit:command_detail', [tables.A('pk')])
8
+ )
9
+ platform = tables.Column(
10
+ linkify=True
11
+ )
12
+ command_type = tables.Column()
13
+
14
+ class Meta(NetBoxTable.Meta):
15
+ model = Command
16
+ fields = ('pk', 'id', 'name', 'platform', 'command_type', 'description')
17
+ default_columns = ('pk', 'name', 'platform', 'command_type', 'description')
18
+
19
+ class CommandLogTable(NetBoxTable):
20
+ command = tables.Column(
21
+ linkify=('plugins:netbox_toolkit:command_detail', [tables.A('command.pk')])
22
+ )
23
+ device = tables.Column(
24
+ linkify=True
25
+ )
26
+ success = tables.BooleanColumn(
27
+ verbose_name='Status',
28
+ yesno=('Success', 'Failed')
29
+ )
30
+
31
+ # Remove actions column entirely
32
+ actions = False
33
+
34
+ class Meta(NetBoxTable.Meta):
35
+ model = CommandLog
36
+ fields = ('pk', 'id', 'command', 'device', 'username', 'execution_time', 'success', 'execution_duration')
37
+ default_columns = ('pk', 'command', 'device', 'username', 'execution_time', 'success')