illumio-pylo 0.3.11__py3-none-any.whl → 0.3.13__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 (30) hide show
  1. illumio_pylo/API/APIConnector.py +138 -106
  2. illumio_pylo/API/CredentialsManager.py +168 -3
  3. illumio_pylo/API/Explorer.py +619 -14
  4. illumio_pylo/API/JsonPayloadTypes.py +64 -4
  5. illumio_pylo/FilterQuery.py +892 -0
  6. illumio_pylo/Helpers/exports.py +1 -1
  7. illumio_pylo/LabelCommon.py +13 -3
  8. illumio_pylo/LabelDimension.py +109 -0
  9. illumio_pylo/LabelStore.py +97 -38
  10. illumio_pylo/WorkloadStore.py +58 -0
  11. illumio_pylo/__init__.py +9 -3
  12. illumio_pylo/cli/__init__.py +5 -2
  13. illumio_pylo/cli/commands/__init__.py +1 -0
  14. illumio_pylo/cli/commands/credential_manager.py +555 -4
  15. illumio_pylo/cli/commands/label_delete_unused.py +0 -3
  16. illumio_pylo/cli/commands/traffic_export.py +358 -0
  17. illumio_pylo/cli/commands/ui/credential_manager_ui/app.js +638 -0
  18. illumio_pylo/cli/commands/ui/credential_manager_ui/index.html +217 -0
  19. illumio_pylo/cli/commands/ui/credential_manager_ui/styles.css +581 -0
  20. illumio_pylo/cli/commands/update_pce_objects_cache.py +1 -2
  21. illumio_pylo/cli/commands/ven_duplicate_remover.py +79 -59
  22. illumio_pylo/cli/commands/workload_export.py +29 -0
  23. illumio_pylo/utilities/cli.py +4 -1
  24. illumio_pylo/utilities/health_monitoring.py +5 -1
  25. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/METADATA +2 -1
  26. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/RECORD +29 -24
  27. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/WHEEL +1 -1
  28. illumio_pylo/Query.py +0 -331
  29. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/licenses/LICENSE +0 -0
  30. {illumio_pylo-0.3.11.dist-info → illumio_pylo-0.3.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,638 @@
1
+ // Credential Manager Web Editor - JavaScript Application
2
+
3
+ (function() {
4
+ 'use strict';
5
+
6
+ // DOM Elements
7
+ const elements = {
8
+ // Credentials table
9
+ credentialsTable: document.getElementById('credentials-table'),
10
+ credentialsBody: document.getElementById('credentials-body'),
11
+ credentialsLoading: document.getElementById('credentials-loading'),
12
+ noCredentials: document.getElementById('no-credentials'),
13
+
14
+ // Notification
15
+ notification: document.getElementById('notification'),
16
+
17
+ // Buttons
18
+ btnNewCredential: document.getElementById('btn-new-credential'),
19
+ btnCloseModal: document.getElementById('btn-close-modal'),
20
+ btnCancel: document.getElementById('btn-cancel'),
21
+ btnSubmit: document.getElementById('btn-submit'),
22
+ btnCloseTestModal: document.getElementById('btn-close-test-modal'),
23
+ btnCloseTest: document.getElementById('btn-close-test'),
24
+ themeToggle: document.getElementById('theme-toggle'),
25
+
26
+ // Modal
27
+ credentialModal: document.getElementById('credential-modal'),
28
+ modalTitle: document.getElementById('modal-title'),
29
+ credentialForm: document.getElementById('credential-form'),
30
+
31
+ // Form fields
32
+ formMode: document.getElementById('form-mode'),
33
+ formOriginalName: document.getElementById('form-original-name'),
34
+ formName: document.getElementById('form-name'),
35
+ formFqdn: document.getElementById('form-fqdn'),
36
+ formPort: document.getElementById('form-port'),
37
+ formOrgId: document.getElementById('form-org-id'),
38
+ formApiUser: document.getElementById('form-api-user'),
39
+ formApiKey: document.getElementById('form-api-key'),
40
+ formVerifySsl: document.getElementById('form-verify-ssl'),
41
+ formEncrypt: document.getElementById('form-encrypt'),
42
+ formSshKey: document.getElementById('form-ssh-key'),
43
+ formUseWorkdir: document.getElementById('form-use-workdir'),
44
+
45
+ // Form sections
46
+ encryptionSection: document.getElementById('encryption-section'),
47
+ sshKeysSection: document.getElementById('ssh-keys-section'),
48
+ storageSection: document.getElementById('storage-section'),
49
+ apiKeyRequired: document.getElementById('api-key-required'),
50
+ apiKeyHint: document.getElementById('api-key-hint'),
51
+
52
+ // Test modal
53
+ testModal: document.getElementById('test-modal'),
54
+ testResult: document.getElementById('test-result'),
55
+
56
+ // Delete modal
57
+ deleteModal: document.getElementById('delete-modal'),
58
+ deleteCredentialName: document.getElementById('delete-credential-name'),
59
+ btnCloseDeleteModal: document.getElementById('btn-close-delete-modal'),
60
+ btnCancelDelete: document.getElementById('btn-cancel-delete'),
61
+ btnConfirmDelete: document.getElementById('btn-confirm-delete'),
62
+
63
+ // Encrypt modal
64
+ encryptModal: document.getElementById('encrypt-modal'),
65
+ encryptCredentialName: document.getElementById('encrypt-credential-name'),
66
+ encryptSshKey: document.getElementById('encrypt-ssh-key'),
67
+ btnCloseEncryptModal: document.getElementById('btn-close-encrypt-modal'),
68
+ btnCancelEncrypt: document.getElementById('btn-cancel-encrypt'),
69
+ btnConfirmEncrypt: document.getElementById('btn-confirm-encrypt'),
70
+
71
+ // Shutdown modal
72
+ shutdownModal: document.getElementById('shutdown-modal'),
73
+ btnShutdown: document.getElementById('btn-shutdown'),
74
+ btnCloseShutdownModal: document.getElementById('btn-close-shutdown-modal'),
75
+ btnCancelShutdown: document.getElementById('btn-cancel-shutdown'),
76
+ btnConfirmShutdown: document.getElementById('btn-confirm-shutdown'),
77
+ shutdownMessage: document.getElementById('shutdown-message'),
78
+ shutdownCountdown: document.getElementById('shutdown-countdown'),
79
+ shutdownActions: document.getElementById('shutdown-actions')
80
+ };
81
+
82
+ // State
83
+ let sshKeys = [];
84
+ let encryptionAvailable = false;
85
+ let credentialToDelete = null;
86
+ let credentialToEncrypt = null;
87
+ let shutdownInProgress = false;
88
+
89
+ // Theme Management
90
+ function initTheme() {
91
+ // Check for saved theme preference or default to browser preference
92
+ const savedTheme = localStorage.getItem('theme');
93
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
94
+
95
+ if (savedTheme) {
96
+ setTheme(savedTheme);
97
+ } else if (prefersDark) {
98
+ setTheme('dark');
99
+ } else {
100
+ setTheme('light');
101
+ }
102
+
103
+ // Listen for browser theme changes
104
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
105
+ if (!localStorage.getItem('theme')) {
106
+ setTheme(e.matches ? 'dark' : 'light');
107
+ }
108
+ });
109
+ }
110
+
111
+ function setTheme(theme) {
112
+ document.documentElement.setAttribute('data-theme', theme);
113
+ elements.themeToggle.textContent = theme === 'dark' ? '☀️' : '🌙';
114
+ elements.themeToggle.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
115
+ }
116
+
117
+ function toggleTheme() {
118
+ const currentTheme = document.documentElement.getAttribute('data-theme');
119
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
120
+ setTheme(newTheme);
121
+ localStorage.setItem('theme', newTheme);
122
+ }
123
+
124
+ // Initialize the application
125
+ async function init() {
126
+ initTheme();
127
+ await checkEncryptionStatus();
128
+ await loadCredentials();
129
+ setupEventListeners();
130
+ }
131
+
132
+ // Setup event listeners
133
+ function setupEventListeners() {
134
+ // Theme toggle button
135
+ elements.themeToggle.addEventListener('click', toggleTheme);
136
+
137
+ // New credential button
138
+ elements.btnNewCredential.addEventListener('click', () => openCreateModal());
139
+
140
+ // Modal close buttons
141
+ elements.btnCloseModal.addEventListener('click', closeModal);
142
+ elements.btnCancel.addEventListener('click', closeModal);
143
+
144
+ // Form submission
145
+ elements.credentialForm.addEventListener('submit', handleFormSubmit);
146
+
147
+ // Encryption checkbox
148
+ elements.formEncrypt.addEventListener('change', toggleSshKeySelection);
149
+
150
+ // Test modal close
151
+ elements.btnCloseTestModal.addEventListener('click', closeTestModal);
152
+ elements.btnCloseTest.addEventListener('click', closeTestModal);
153
+
154
+ // Delete modal buttons
155
+ elements.btnCloseDeleteModal.addEventListener('click', closeDeleteModal);
156
+ elements.btnCancelDelete.addEventListener('click', closeDeleteModal);
157
+ elements.btnConfirmDelete.addEventListener('click', confirmDelete);
158
+
159
+ // Encrypt modal buttons
160
+ elements.btnCloseEncryptModal.addEventListener('click', closeEncryptModal);
161
+ elements.btnCancelEncrypt.addEventListener('click', closeEncryptModal);
162
+ elements.btnConfirmEncrypt.addEventListener('click', confirmEncrypt);
163
+
164
+ // Shutdown modal buttons
165
+ elements.btnShutdown.addEventListener('click', openShutdownModal);
166
+ elements.btnConfirmShutdown.addEventListener('click', confirmShutdown);
167
+
168
+ // Close modals on background click
169
+ elements.credentialModal.addEventListener('click', (e) => {
170
+ if (e.target === elements.credentialModal) closeModal();
171
+ });
172
+ elements.testModal.addEventListener('click', (e) => {
173
+ if (e.target === elements.testModal) closeTestModal();
174
+ });
175
+ elements.deleteModal.addEventListener('click', (e) => {
176
+ if (e.target === elements.deleteModal) closeDeleteModal();
177
+ });
178
+ elements.encryptModal.addEventListener('click', (e) => {
179
+ if (e.target === elements.encryptModal) closeEncryptModal();
180
+ });
181
+ }
182
+
183
+ // API Functions
184
+ async function apiCall(url, options = {}) {
185
+ try {
186
+ const response = await fetch(url, {
187
+ headers: {
188
+ 'Content-Type': 'application/json',
189
+ ...options.headers
190
+ },
191
+ ...options
192
+ });
193
+ const data = await response.json();
194
+ if (!response.ok) {
195
+ throw new Error(data.error || 'An error occurred');
196
+ }
197
+ return data;
198
+ } catch (error) {
199
+ throw error;
200
+ }
201
+ }
202
+
203
+ // Load all credentials
204
+ async function loadCredentials() {
205
+ elements.credentialsLoading.classList.remove('hidden');
206
+ elements.credentialsTable.classList.add('hidden');
207
+ elements.noCredentials.classList.add('hidden');
208
+
209
+ try {
210
+ const credentials = await apiCall('/api/credentials');
211
+ renderCredentials(credentials);
212
+ } catch (error) {
213
+ showNotification('Failed to load credentials: ' + error.message, 'error');
214
+ elements.credentialsLoading.classList.add('hidden');
215
+ }
216
+ }
217
+
218
+ // Render credentials table
219
+ function renderCredentials(credentials) {
220
+ elements.credentialsLoading.classList.add('hidden');
221
+
222
+ if (credentials.length === 0) {
223
+ elements.noCredentials.classList.remove('hidden');
224
+ elements.credentialsTable.classList.add('hidden');
225
+ return;
226
+ }
227
+
228
+ elements.credentialsTable.classList.remove('hidden');
229
+ elements.credentialsBody.innerHTML = '';
230
+
231
+ for (const cred of credentials) {
232
+ const row = document.createElement('tr');
233
+ // Show encrypt button only if encryption is available and key is not already encrypted
234
+ const showEncryptBtn = encryptionAvailable && sshKeys.length > 0 && !cred.api_key_encrypted;
235
+ console.log('Rendering credential:', cred.name, 'Show Encrypt Button:', showEncryptBtn, 'Encryption Available:', encryptionAvailable, 'SSH Keys:', sshKeys.length, 'API Key Encrypted:', cred.api_key_encrypted);
236
+ row.innerHTML = `
237
+ <td><strong>${escapeHtml(cred.name)}</strong></td>
238
+ <td>${escapeHtml(cred.fqdn)}</td>
239
+ <td>${cred.port}</td>
240
+ <td>${cred.org_id}</td>
241
+ <td>${escapeHtml(cred.api_user)}</td>
242
+ <td class="${cred.api_key_encrypted ? 'status-yes' : 'status-no'}">${cred.api_key_encrypted ? 'Yes' : 'No'}</td>
243
+ <td class="${cred.verify_ssl ? 'status-yes' : 'status-no'}">${cred.verify_ssl ? 'Yes' : 'No'}</td>
244
+ <td title="${escapeHtml(cred.originating_file)}">${truncatePath(cred.originating_file)}</td>
245
+ <td class="actions-cell">
246
+ <button class="btn btn-small btn-success btn-test" data-name="${escapeHtml(cred.name)}">Test</button>
247
+ <button class="btn btn-small btn-secondary btn-edit" data-name="${escapeHtml(cred.name)}">Edit</button>
248
+ ${showEncryptBtn ? `<button class="btn btn-small btn-warning btn-encrypt" data-name="${escapeHtml(cred.name)}">Encrypt</button>` : ''}
249
+ <button class="btn btn-small btn-danger btn-delete" data-name="${escapeHtml(cred.name)}">Delete</button>
250
+ </td>
251
+ `;
252
+ elements.credentialsBody.appendChild(row);
253
+ }
254
+
255
+ // Add event listeners to buttons
256
+ document.querySelectorAll('.btn-test').forEach(btn => {
257
+ btn.addEventListener('click', () => testCredential(btn.dataset.name));
258
+ });
259
+ document.querySelectorAll('.btn-edit').forEach(btn => {
260
+ btn.addEventListener('click', () => openEditModal(btn.dataset.name));
261
+ });
262
+ document.querySelectorAll('.btn-encrypt').forEach(btn => {
263
+ btn.addEventListener('click', () => openEncryptModal(btn.dataset.name));
264
+ });
265
+ document.querySelectorAll('.btn-delete').forEach(btn => {
266
+ btn.addEventListener('click', () => openDeleteModal(btn.dataset.name));
267
+ });
268
+ }
269
+
270
+ // Check encryption availability and load SSH keys
271
+ async function checkEncryptionStatus() {
272
+ try {
273
+ const status = await apiCall('/api/encryption-status');
274
+ encryptionAvailable = status.available;
275
+
276
+ if (encryptionAvailable) {
277
+ const keysData = await apiCall('/api/ssh-keys');
278
+ sshKeys = keysData.keys || [];
279
+
280
+ if (sshKeys.length > 0) {
281
+ elements.encryptionSection.classList.remove('hidden');
282
+ populateSshKeySelect();
283
+ }
284
+ }
285
+ } catch (error) {
286
+ console.log('Encryption not available:', error.message);
287
+ }
288
+ }
289
+
290
+ // Populate SSH key select dropdown
291
+ function populateSshKeySelect() {
292
+ elements.formSshKey.innerHTML = '';
293
+ for (const key of sshKeys) {
294
+ const option = document.createElement('option');
295
+ option.value = key.index;
296
+ option.textContent = `${key.type} | ${key.fingerprint.substring(0, 16)}... | ${key.comment || 'No comment'}`;
297
+ elements.formSshKey.appendChild(option);
298
+ }
299
+ }
300
+
301
+ // Toggle SSH key selection visibility
302
+ function toggleSshKeySelection() {
303
+ if (elements.formEncrypt.checked && sshKeys.length > 0) {
304
+ elements.sshKeysSection.classList.remove('hidden');
305
+ } else {
306
+ elements.sshKeysSection.classList.add('hidden');
307
+ }
308
+ }
309
+
310
+ // Open modal for creating new credential
311
+ function openCreateModal() {
312
+ resetForm();
313
+ elements.formMode.value = 'create';
314
+ elements.modalTitle.textContent = 'New Credential';
315
+ elements.btnSubmit.textContent = 'Create';
316
+ elements.formName.removeAttribute('readonly');
317
+ elements.formApiKey.setAttribute('required', 'required');
318
+ elements.apiKeyRequired.classList.remove('hidden');
319
+ elements.apiKeyHint.classList.add('hidden');
320
+ elements.storageSection.classList.remove('hidden');
321
+ elements.credentialModal.classList.remove('hidden');
322
+ elements.formName.focus();
323
+ }
324
+
325
+ // Open modal for editing existing credential
326
+ async function openEditModal(name) {
327
+ resetForm();
328
+
329
+ try {
330
+ const credential = await apiCall(`/api/credentials/${encodeURIComponent(name)}`);
331
+
332
+ elements.formMode.value = 'edit';
333
+ elements.formOriginalName.value = credential.name;
334
+ elements.modalTitle.textContent = 'Edit Credential';
335
+ elements.btnSubmit.textContent = 'Update';
336
+
337
+ // Populate form
338
+ elements.formName.value = credential.name;
339
+ elements.formName.setAttribute('readonly', 'readonly');
340
+ elements.formFqdn.value = credential.fqdn;
341
+ elements.formPort.value = credential.port;
342
+ elements.formOrgId.value = credential.org_id;
343
+ elements.formApiUser.value = credential.api_user;
344
+ elements.formVerifySsl.checked = credential.verify_ssl;
345
+
346
+ // API key is optional for updates
347
+ elements.formApiKey.removeAttribute('required');
348
+ elements.apiKeyRequired.classList.add('hidden');
349
+ elements.apiKeyHint.classList.remove('hidden');
350
+
351
+ // Hide storage section for edits
352
+ elements.storageSection.classList.add('hidden');
353
+
354
+ elements.credentialModal.classList.remove('hidden');
355
+ elements.formFqdn.focus();
356
+ } catch (error) {
357
+ showNotification('Failed to load credential: ' + error.message, 'error');
358
+ }
359
+ }
360
+
361
+ // Open modal for deleting credential
362
+ function openDeleteModal(name) {
363
+ credentialToDelete = name;
364
+ elements.deleteCredentialName.textContent = name;
365
+ elements.deleteModal.classList.remove('hidden');
366
+ }
367
+
368
+ // Close modal
369
+ function closeModal() {
370
+ elements.credentialModal.classList.add('hidden');
371
+ resetForm();
372
+ }
373
+
374
+ // Close delete modal
375
+ function closeDeleteModal() {
376
+ elements.deleteModal.classList.add('hidden');
377
+ credentialToDelete = null;
378
+ }
379
+
380
+ // Open modal for encrypting credential
381
+ function openEncryptModal(name) {
382
+ credentialToEncrypt = name;
383
+ elements.encryptCredentialName.textContent = name;
384
+ // Populate SSH key dropdown
385
+ elements.encryptSshKey.innerHTML = '';
386
+ for (const key of sshKeys) {
387
+ const option = document.createElement('option');
388
+ option.value = key.index;
389
+ option.textContent = `${key.type} | ${key.fingerprint.substring(0, 16)}... | ${key.comment || 'No comment'}`;
390
+ elements.encryptSshKey.appendChild(option);
391
+ }
392
+ elements.encryptModal.classList.remove('hidden');
393
+ }
394
+
395
+ // Close encrypt modal
396
+ function closeEncryptModal() {
397
+ elements.encryptModal.classList.add('hidden');
398
+ credentialToEncrypt = null;
399
+ }
400
+
401
+ // Open modal for shutting down the server
402
+ function openShutdownModal() {
403
+ // Reset modal to initial state
404
+ shutdownInProgress = false;
405
+ elements.shutdownMessage.textContent = 'This will stop the Credential Manager server program.';
406
+ elements.shutdownCountdown.classList.add('hidden');
407
+ elements.shutdownCountdown.textContent = '';
408
+ elements.shutdownActions.classList.remove('hidden');
409
+ elements.btnConfirmShutdown.disabled = false;
410
+ elements.btnConfirmShutdown.textContent = 'Stop Server';
411
+ elements.btnCancelShutdown.disabled = true;
412
+ elements.btnCloseShutdownModal.disabled = true;
413
+ elements.shutdownModal.classList.remove('hidden');
414
+ }
415
+
416
+ // Shutdown modal intentionally stays visible once opened.
417
+
418
+ // Confirm shutdown
419
+ async function confirmShutdown() {
420
+ if (shutdownInProgress) return;
421
+
422
+ shutdownInProgress = true;
423
+ elements.btnConfirmShutdown.disabled = true;
424
+ elements.btnConfirmShutdown.textContent = 'Stopping...';
425
+ elements.btnCancelShutdown.disabled = true;
426
+ elements.btnCloseShutdownModal.disabled = true;
427
+
428
+ try {
429
+ await apiCall('/api/shutdown', { method: 'POST' });
430
+
431
+ // Update modal to show success and countdown
432
+ elements.shutdownMessage.textContent = 'Server shutdown acknowledged. Closing window...';
433
+ elements.shutdownActions.classList.add('hidden');
434
+ elements.shutdownCountdown.classList.remove('hidden');
435
+
436
+ // Start countdown
437
+ let countdown = 10;
438
+ elements.shutdownCountdown.textContent = `Window will close in ${countdown} seconds...`;
439
+ elements.shutdownCountdown.className = 'countdown-text';
440
+
441
+ const countdownInterval = setInterval(() => {
442
+ countdown--;
443
+ if (countdown > 0) {
444
+ elements.shutdownCountdown.textContent = `Window will close in ${countdown} seconds...`;
445
+ } else {
446
+ clearInterval(countdownInterval);
447
+ elements.shutdownCountdown.textContent = 'Closing window...';
448
+ // Attempt to close the window/tab
449
+ window.close();
450
+ // If window.close() doesn't work (common in modern browsers), show a message
451
+ setTimeout(() => {
452
+ elements.shutdownCountdown.textContent = 'Please close this browser tab/window manually.';
453
+ }, 500);
454
+ }
455
+ }, 1000);
456
+ } catch (error) {
457
+ shutdownInProgress = false;
458
+ elements.btnConfirmShutdown.disabled = false;
459
+ elements.btnConfirmShutdown.textContent = 'Stop Server';
460
+ showNotification('Failed to stop server: ' + error.message, 'error');
461
+ }
462
+ }
463
+
464
+ // Confirm credential encryption
465
+ async function confirmEncrypt() {
466
+ if (!credentialToEncrypt) return;
467
+
468
+ elements.btnConfirmEncrypt.disabled = true;
469
+ elements.btnConfirmEncrypt.textContent = 'Encrypting...';
470
+
471
+ try {
472
+ await apiCall(`/api/credentials/${encodeURIComponent(credentialToEncrypt)}/encrypt`, {
473
+ method: 'POST',
474
+ body: JSON.stringify({
475
+ ssh_key_index: parseInt(elements.encryptSshKey.value)
476
+ })
477
+ });
478
+ showNotification('API key encrypted successfully!', 'success');
479
+ closeEncryptModal();
480
+ await loadCredentials();
481
+ } catch (error) {
482
+ showNotification('Failed to encrypt API key: ' + error.message, 'error');
483
+ } finally {
484
+ elements.btnConfirmEncrypt.disabled = false;
485
+ elements.btnConfirmEncrypt.textContent = 'Encrypt';
486
+ }
487
+ }
488
+
489
+ // Reset form
490
+ function resetForm() {
491
+ elements.credentialForm.reset();
492
+ elements.formMode.value = 'create';
493
+ elements.formOriginalName.value = '';
494
+ elements.formPort.value = '8443';
495
+ elements.formOrgId.value = '1';
496
+ elements.formVerifySsl.checked = true;
497
+ elements.formEncrypt.checked = false;
498
+ elements.sshKeysSection.classList.add('hidden');
499
+ }
500
+
501
+ // Handle form submission
502
+ async function handleFormSubmit(e) {
503
+ e.preventDefault();
504
+
505
+ const mode = elements.formMode.value;
506
+ const data = {
507
+ name: elements.formName.value.trim(),
508
+ fqdn: elements.formFqdn.value.trim(),
509
+ port: parseInt(elements.formPort.value),
510
+ org_id: parseInt(elements.formOrgId.value),
511
+ api_user: elements.formApiUser.value.trim(),
512
+ verify_ssl: elements.formVerifySsl.checked
513
+ };
514
+
515
+ // Add API key if provided
516
+ const apiKey = elements.formApiKey.value;
517
+ if (apiKey) {
518
+ data.api_key = apiKey;
519
+ } else if (mode === 'create') {
520
+ showNotification('API key is required', 'error');
521
+ return;
522
+ }
523
+
524
+ // Add encryption settings
525
+ if (elements.formEncrypt.checked && sshKeys.length > 0) {
526
+ data.encrypt = true;
527
+ data.ssh_key_index = parseInt(elements.formSshKey.value);
528
+ }
529
+
530
+ // Add storage location for create
531
+ if (mode === 'create') {
532
+ data.use_current_workdir = elements.formUseWorkdir.checked;
533
+ }
534
+
535
+ elements.btnSubmit.disabled = true;
536
+ elements.btnSubmit.textContent = mode === 'create' ? 'Creating...' : 'Updating...';
537
+
538
+ try {
539
+ if (mode === 'create') {
540
+ await apiCall('/api/credentials', {
541
+ method: 'POST',
542
+ body: JSON.stringify(data)
543
+ });
544
+ showNotification('Credential created successfully!', 'success');
545
+ } else {
546
+ const originalName = elements.formOriginalName.value;
547
+ await apiCall(`/api/credentials/${encodeURIComponent(originalName)}`, {
548
+ method: 'PUT',
549
+ body: JSON.stringify(data)
550
+ });
551
+ showNotification('Credential updated successfully!', 'success');
552
+ }
553
+
554
+ closeModal();
555
+ await loadCredentials();
556
+ } catch (error) {
557
+ showNotification('Failed to save credential: ' + error.message, 'error');
558
+ } finally {
559
+ elements.btnSubmit.disabled = false;
560
+ elements.btnSubmit.textContent = mode === 'create' ? 'Create' : 'Update';
561
+ }
562
+ }
563
+
564
+ // Confirm credential deletion
565
+ async function confirmDelete() {
566
+ if (!credentialToDelete) return;
567
+
568
+ elements.btnConfirmDelete.disabled = true;
569
+ elements.btnConfirmDelete.textContent = 'Deleting...';
570
+
571
+ try {
572
+ await apiCall(`/api/credentials/${encodeURIComponent(credentialToDelete)}`, {
573
+ method: 'DELETE'
574
+ });
575
+ showNotification('Credential deleted successfully!', 'success');
576
+ closeDeleteModal();
577
+ await loadCredentials();
578
+ } catch (error) {
579
+ showNotification('Failed to delete credential: ' + error.message, 'error');
580
+ } finally {
581
+ elements.btnConfirmDelete.disabled = false;
582
+ elements.btnConfirmDelete.textContent = 'Delete';
583
+ }
584
+ }
585
+
586
+ // Test credential connection
587
+ async function testCredential(name) {
588
+ elements.testResult.className = 'test-result loading';
589
+ elements.testResult.textContent = 'Testing connection...';
590
+ elements.testModal.classList.remove('hidden');
591
+
592
+ try {
593
+ const result = await apiCall(`/api/credentials/${encodeURIComponent(name)}/test`, {
594
+ method: 'POST'
595
+ });
596
+ elements.testResult.className = 'test-result success';
597
+ elements.testResult.textContent = `Connection to "${name}" successful!`;
598
+ } catch (error) {
599
+ elements.testResult.className = 'test-result error';
600
+ elements.testResult.textContent = `Connection failed: ${error.message}`;
601
+ }
602
+ }
603
+
604
+ // Close test modal
605
+ function closeTestModal() {
606
+ elements.testModal.classList.add('hidden');
607
+ }
608
+
609
+ // Show notification
610
+ function showNotification(message, type = 'info') {
611
+ elements.notification.textContent = message;
612
+ elements.notification.className = `notification ${type}`;
613
+ elements.notification.classList.remove('hidden');
614
+
615
+ // Auto-hide after 5 seconds
616
+ setTimeout(() => {
617
+ elements.notification.classList.add('hidden');
618
+ }, 5000);
619
+ }
620
+
621
+ // Utility: Escape HTML
622
+ function escapeHtml(text) {
623
+ const div = document.createElement('div');
624
+ div.textContent = text;
625
+ return div.innerHTML;
626
+ }
627
+
628
+ // Utility: Truncate file path for display
629
+ function truncatePath(path) {
630
+ if (path.length > 30) {
631
+ return '...' + path.slice(-27);
632
+ }
633
+ return path;
634
+ }
635
+
636
+ // Start the application
637
+ document.addEventListener('DOMContentLoaded', init);
638
+ })();