| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533 |
- // SeaweedFS Dashboard JavaScript
- // Global variables
- let bucketToDelete = '';
- // Initialize dashboard when DOM is loaded
- document.addEventListener('DOMContentLoaded', function() {
- initializeDashboard();
- initializeEventHandlers();
- setupFormValidation();
- setupFileManagerEventHandlers();
-
- // Initialize delete button visibility on file browser page
- if (window.location.pathname === '/files') {
- updateDeleteSelectedButton();
- }
- });
- function initializeDashboard() {
- // Set up HTMX event listeners
- setupHTMXListeners();
-
- // Initialize tooltips
- initializeTooltips();
-
- // Set up periodic refresh
- setupAutoRefresh();
-
- // Set active navigation
- setActiveNavigation();
-
- // Set up submenu behavior
- setupSubmenuBehavior();
- }
- // HTMX event listeners
- function setupHTMXListeners() {
- // Show loading indicator on requests
- document.body.addEventListener('htmx:beforeRequest', function(evt) {
- showLoadingIndicator();
- });
-
- // Hide loading indicator on completion
- document.body.addEventListener('htmx:afterRequest', function(evt) {
- hideLoadingIndicator();
- });
-
- // Handle errors
- document.body.addEventListener('htmx:responseError', function(evt) {
- handleHTMXError(evt);
- });
- }
- // Initialize Bootstrap tooltips
- function initializeTooltips() {
- var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
- var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
- return new bootstrap.Tooltip(tooltipTriggerEl);
- });
- }
- // Set up auto-refresh for dashboard data
- function setupAutoRefresh() {
- // Refresh dashboard data every 30 seconds
- setInterval(function() {
- if (window.location.pathname === '/dashboard') {
- htmx.trigger('#dashboard-content', 'refresh');
- }
- }, 30000);
- }
- // Set active navigation item
- function setActiveNavigation() {
- const currentPath = window.location.pathname;
- const navLinks = document.querySelectorAll('.sidebar .nav-link');
-
- navLinks.forEach(function(link) {
- const href = link.getAttribute('href');
- let isActive = false;
-
- if (href === currentPath) {
- isActive = true;
- } else if (currentPath === '/' && href === '/admin') {
- isActive = true;
- } else if (currentPath.startsWith('/s3/') && href === '/s3/buckets') {
- isActive = true;
- }
- // Note: Removed the problematic cluster condition that was highlighting all submenu items
-
- if (isActive) {
- link.classList.add('active');
- } else {
- link.classList.remove('active');
- }
- });
- }
- // Set up submenu behavior
- function setupSubmenuBehavior() {
- const currentPath = window.location.pathname;
-
- // If we're on a cluster page, expand the cluster submenu
- if (currentPath.startsWith('/cluster/')) {
- const clusterSubmenu = document.getElementById('clusterSubmenu');
- if (clusterSubmenu) {
- clusterSubmenu.classList.add('show');
-
- // Update the parent toggle button state
- const toggleButton = document.querySelector('[data-bs-target="#clusterSubmenu"]');
- if (toggleButton) {
- toggleButton.classList.remove('collapsed');
- toggleButton.setAttribute('aria-expanded', 'true');
- }
- }
- }
-
- // If we're on an object store page, expand the object store submenu
- if (currentPath.startsWith('/object-store/')) {
- const objectStoreSubmenu = document.getElementById('objectStoreSubmenu');
- if (objectStoreSubmenu) {
- objectStoreSubmenu.classList.add('show');
-
- // Update the parent toggle button state
- const toggleButton = document.querySelector('[data-bs-target="#objectStoreSubmenu"]');
- if (toggleButton) {
- toggleButton.classList.remove('collapsed');
- toggleButton.setAttribute('aria-expanded', 'true');
- }
- }
- }
-
- // If we're on a maintenance page, expand the maintenance submenu
- if (currentPath.startsWith('/maintenance')) {
- const maintenanceSubmenu = document.getElementById('maintenanceSubmenu');
- if (maintenanceSubmenu) {
- maintenanceSubmenu.classList.add('show');
-
- // Update the parent toggle button state
- const toggleButton = document.querySelector('[data-bs-target="#maintenanceSubmenu"]');
- if (toggleButton) {
- toggleButton.classList.remove('collapsed');
- toggleButton.setAttribute('aria-expanded', 'true');
- }
- }
- }
-
- // Prevent submenu from collapsing when clicking on submenu items
- const clusterSubmenuLinks = document.querySelectorAll('#clusterSubmenu .nav-link');
- clusterSubmenuLinks.forEach(function(link) {
- link.addEventListener('click', function(e) {
- // Don't prevent the navigation, just stop the collapse behavior
- e.stopPropagation();
- });
- });
-
- const objectStoreSubmenuLinks = document.querySelectorAll('#objectStoreSubmenu .nav-link');
- objectStoreSubmenuLinks.forEach(function(link) {
- link.addEventListener('click', function(e) {
- // Don't prevent the navigation, just stop the collapse behavior
- e.stopPropagation();
- });
- });
-
- const maintenanceSubmenuLinks = document.querySelectorAll('#maintenanceSubmenu .nav-link');
- maintenanceSubmenuLinks.forEach(function(link) {
- link.addEventListener('click', function(e) {
- // Don't prevent the navigation, just stop the collapse behavior
- e.stopPropagation();
- });
- });
-
- // Handle the main cluster toggle
- const clusterToggle = document.querySelector('[data-bs-target="#clusterSubmenu"]');
- if (clusterToggle) {
- clusterToggle.addEventListener('click', function(e) {
- e.preventDefault();
-
- const submenu = document.getElementById('clusterSubmenu');
- const isExpanded = submenu.classList.contains('show');
-
- if (isExpanded) {
- // Collapse
- submenu.classList.remove('show');
- this.classList.add('collapsed');
- this.setAttribute('aria-expanded', 'false');
- } else {
- // Expand
- submenu.classList.add('show');
- this.classList.remove('collapsed');
- this.setAttribute('aria-expanded', 'true');
- }
- });
- }
-
- // Handle the main object store toggle
- const objectStoreToggle = document.querySelector('[data-bs-target="#objectStoreSubmenu"]');
- if (objectStoreToggle) {
- objectStoreToggle.addEventListener('click', function(e) {
- e.preventDefault();
-
- const submenu = document.getElementById('objectStoreSubmenu');
- const isExpanded = submenu.classList.contains('show');
-
- if (isExpanded) {
- // Collapse
- submenu.classList.remove('show');
- this.classList.add('collapsed');
- this.setAttribute('aria-expanded', 'false');
- } else {
- // Expand
- submenu.classList.add('show');
- this.classList.remove('collapsed');
- this.setAttribute('aria-expanded', 'true');
- }
- });
- }
-
- // Handle the main maintenance toggle
- const maintenanceToggle = document.querySelector('[data-bs-target="#maintenanceSubmenu"]');
- if (maintenanceToggle) {
- maintenanceToggle.addEventListener('click', function(e) {
- e.preventDefault();
-
- const submenu = document.getElementById('maintenanceSubmenu');
- const isExpanded = submenu.classList.contains('show');
-
- if (isExpanded) {
- // Collapse
- submenu.classList.remove('show');
- this.classList.add('collapsed');
- this.setAttribute('aria-expanded', 'false');
- } else {
- // Expand
- submenu.classList.add('show');
- this.classList.remove('collapsed');
- this.setAttribute('aria-expanded', 'true');
- }
- });
- }
- }
- // Loading indicator functions
- function showLoadingIndicator() {
- const indicator = document.getElementById('loading-indicator');
- if (indicator) {
- indicator.style.display = 'block';
- }
-
- // Add loading class to body
- document.body.classList.add('loading');
- }
- function hideLoadingIndicator() {
- const indicator = document.getElementById('loading-indicator');
- if (indicator) {
- indicator.style.display = 'none';
- }
-
- // Remove loading class from body
- document.body.classList.remove('loading');
- }
- // Handle HTMX errors
- function handleHTMXError(evt) {
- console.error('HTMX Request Error:', evt.detail);
-
- // Show error toast or message
- showErrorMessage('Request failed. Please try again.');
-
- hideLoadingIndicator();
- }
- // Utility functions
- function showErrorMessage(message) {
- // Create toast element
- const toast = document.createElement('div');
- toast.className = 'toast align-items-center text-white bg-danger border-0';
- toast.setAttribute('role', 'alert');
- toast.setAttribute('aria-live', 'assertive');
- toast.setAttribute('aria-atomic', 'true');
-
- toast.innerHTML = `
- <div class="d-flex">
- <div class="toast-body">
- <i class="fas fa-exclamation-triangle me-2"></i>
- ${message}
- </div>
- <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
- </div>
- `;
-
- // Add to toast container or create one
- let toastContainer = document.getElementById('toast-container');
- if (!toastContainer) {
- toastContainer = document.createElement('div');
- toastContainer.id = 'toast-container';
- toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
- toastContainer.style.zIndex = '1055';
- document.body.appendChild(toastContainer);
- }
-
- toastContainer.appendChild(toast);
-
- // Show toast
- const bsToast = new bootstrap.Toast(toast);
- bsToast.show();
-
- // Remove toast element after it's hidden
- toast.addEventListener('hidden.bs.toast', function() {
- toast.remove();
- });
- }
- function showSuccessMessage(message) {
- // Similar to showErrorMessage but with success styling
- const toast = document.createElement('div');
- toast.className = 'toast align-items-center text-white bg-success border-0';
- toast.setAttribute('role', 'alert');
- toast.setAttribute('aria-live', 'assertive');
- toast.setAttribute('aria-atomic', 'true');
-
- toast.innerHTML = `
- <div class="d-flex">
- <div class="toast-body">
- <i class="fas fa-check-circle me-2"></i>
- ${message}
- </div>
- <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
- </div>
- `;
-
- let toastContainer = document.getElementById('toast-container');
- if (!toastContainer) {
- toastContainer = document.createElement('div');
- toastContainer.id = 'toast-container';
- toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
- toastContainer.style.zIndex = '1055';
- document.body.appendChild(toastContainer);
- }
-
- toastContainer.appendChild(toast);
-
- const bsToast = new bootstrap.Toast(toast);
- bsToast.show();
-
- toast.addEventListener('hidden.bs.toast', function() {
- toast.remove();
- });
- }
- // Format bytes for display
- function formatBytes(bytes, decimals = 2) {
- if (bytes === 0) return '0 Bytes';
-
- const k = 1024;
- const dm = decimals < 0 ? 0 : decimals;
- const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
-
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
- }
- // Format numbers with commas
- function formatNumber(num) {
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
- }
- // Helper function to format disk types for CSV export
- function formatDiskTypes(diskTypesText) {
- // Remove any HTML tags and clean up the text
- return diskTypesText.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
- }
- // Confirm action dialogs
- function confirmAction(message, callback) {
- if (confirm(message)) {
- callback();
- }
- }
- // Global error handler
- window.addEventListener('error', function(e) {
- console.error('Global error:', e.error);
- showErrorMessage('An unexpected error occurred.');
- });
- // Export functions for global use
- window.Dashboard = {
- showErrorMessage,
- showSuccessMessage,
- formatBytes,
- formatNumber,
- confirmAction
- };
- // Initialize event handlers
- function initializeEventHandlers() {
- // S3 Bucket Management
- const createBucketForm = document.getElementById('createBucketForm');
- if (createBucketForm) {
- createBucketForm.addEventListener('submit', handleCreateBucket);
- }
- // Delete bucket buttons
- document.addEventListener('click', function(e) {
- if (e.target.closest('.delete-bucket-btn')) {
- const button = e.target.closest('.delete-bucket-btn');
- const bucketName = button.getAttribute('data-bucket-name');
- confirmDeleteBucket(bucketName);
- }
-
- // Quota management buttons
- if (e.target.closest('.quota-btn')) {
- const button = e.target.closest('.quota-btn');
- const bucketName = button.getAttribute('data-bucket-name');
- const currentQuota = parseInt(button.getAttribute('data-current-quota')) || 0;
- const quotaEnabled = button.getAttribute('data-quota-enabled') === 'true';
- showQuotaModal(bucketName, currentQuota, quotaEnabled);
- }
- });
- // Quota form submission
- const quotaForm = document.getElementById('quotaForm');
- if (quotaForm) {
- quotaForm.addEventListener('submit', handleUpdateQuota);
- }
- // Enable quota checkbox for create bucket form
- const enableQuotaCheckbox = document.getElementById('enableQuota');
- if (enableQuotaCheckbox) {
- enableQuotaCheckbox.addEventListener('change', function() {
- const quotaSettings = document.getElementById('quotaSettings');
- if (this.checked) {
- quotaSettings.style.display = 'block';
- } else {
- quotaSettings.style.display = 'none';
- }
- });
- }
- // Enable quota checkbox for quota modal
- const quotaEnabledCheckbox = document.getElementById('quotaEnabled');
- if (quotaEnabledCheckbox) {
- quotaEnabledCheckbox.addEventListener('change', function() {
- const quotaSizeSettings = document.getElementById('quotaSizeSettings');
- if (this.checked) {
- quotaSizeSettings.style.display = 'block';
- } else {
- quotaSizeSettings.style.display = 'none';
- }
- });
- }
- }
- // Setup form validation
- function setupFormValidation() {
- // Bucket name validation
- const bucketNameInput = document.getElementById('bucketName');
- if (bucketNameInput) {
- bucketNameInput.addEventListener('input', validateBucketName);
- }
- }
- // S3 Bucket Management Functions
- // Handle create bucket form submission
- async function handleCreateBucket(event) {
- event.preventDefault();
-
- const form = event.target;
- const formData = new FormData(form);
- const bucketData = {
- name: formData.get('name'),
- region: formData.get('region') || 'us-east-1',
- quota_enabled: formData.get('quota_enabled') === 'on',
- quota_size: parseInt(formData.get('quota_size')) || 0,
- quota_unit: formData.get('quota_unit') || 'MB'
- };
- try {
- const response = await fetch('/api/s3/buckets', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(bucketData)
- });
- const result = await response.json();
- if (response.ok) {
- // Success
- showAlert('success', `Bucket "${bucketData.name}" created successfully!`);
-
- // Close modal
- const modal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal'));
- modal.hide();
-
- // Reset form
- form.reset();
-
- // Refresh the page after a short delay
- setTimeout(() => {
- location.reload();
- }, 1500);
- } else {
- // Error
- showAlert('danger', result.error || 'Failed to create bucket');
- }
- } catch (error) {
- console.error('Error creating bucket:', error);
- showAlert('danger', 'Network error occurred while creating bucket');
- }
- }
- // Validate bucket name input
- function validateBucketName(event) {
- const input = event.target;
- const value = input.value;
- const isValid = /^[a-z0-9.-]+$/.test(value) && value.length >= 3 && value.length <= 63;
-
- if (value.length > 0 && !isValid) {
- input.setCustomValidity('Bucket name must contain only lowercase letters, numbers, dots, and hyphens (3-63 characters)');
- } else {
- input.setCustomValidity('');
- }
- }
- // Confirm bucket deletion
- function confirmDeleteBucket(bucketName) {
- bucketToDelete = bucketName;
- document.getElementById('deleteBucketName').textContent = bucketName;
-
- const modal = new bootstrap.Modal(document.getElementById('deleteBucketModal'));
- modal.show();
- }
- // Delete bucket
- async function deleteBucket() {
- if (!bucketToDelete) {
- return;
- }
- try {
- const response = await fetch(`/api/s3/buckets/${bucketToDelete}`, {
- method: 'DELETE'
- });
- const result = await response.json();
- if (response.ok) {
- // Success
- showAlert('success', `Bucket "${bucketToDelete}" deleted successfully!`);
-
- // Close modal
- const modal = bootstrap.Modal.getInstance(document.getElementById('deleteBucketModal'));
- modal.hide();
-
- // Refresh the page after a short delay
- setTimeout(() => {
- location.reload();
- }, 1500);
- } else {
- // Error
- showAlert('danger', result.error || 'Failed to delete bucket');
- }
- } catch (error) {
- console.error('Error deleting bucket:', error);
- showAlert('danger', 'Network error occurred while deleting bucket');
- }
- bucketToDelete = '';
- }
- // Refresh buckets list
- function refreshBuckets() {
- location.reload();
- }
- // Export bucket list
- function exportBucketList() {
- // Get table data
- const table = document.getElementById('bucketsTable');
- if (!table) return;
- const rows = Array.from(table.querySelectorAll('tbody tr'));
- const data = rows.map(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length < 5) return null; // Skip empty state row
-
- return {
- name: cells[0].textContent.trim(),
- created: cells[1].textContent.trim(),
- objects: cells[2].textContent.trim(),
- size: cells[3].textContent.trim(),
- quota: cells[4].textContent.trim()
- };
- }).filter(item => item !== null);
- // Convert to CSV
- const csv = [
- ['Name', 'Created', 'Objects', 'Size', 'Quota'].join(','),
- ...data.map(row => [
- row.name,
- row.created,
- row.objects,
- row.size,
- row.quota
- ].join(','))
- ].join('\n');
- // Download CSV
- const blob = new Blob([csv], { type: 'text/csv' });
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `seaweedfs-buckets-${new Date().toISOString().split('T')[0]}.csv`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- }
- // Show alert message
- function showAlert(type, message) {
- // Remove existing alerts
- const existingAlerts = document.querySelectorAll('.alert-floating');
- existingAlerts.forEach(alert => alert.remove());
- // Create new alert
- const alert = document.createElement('div');
- alert.className = `alert alert-${type} alert-dismissible fade show alert-floating`;
- alert.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 9999;
- min-width: 300px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- `;
-
- alert.innerHTML = `
- ${message}
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
- `;
- document.body.appendChild(alert);
- // Auto-remove after 5 seconds
- setTimeout(() => {
- if (alert.parentNode) {
- alert.remove();
- }
- }, 5000);
- }
- // Format date for display
- function formatDate(date) {
- return new Date(date).toLocaleString();
- }
- // Copy text to clipboard
- function copyToClipboard(text) {
- navigator.clipboard.writeText(text).then(() => {
- showAlert('success', 'Copied to clipboard!');
- }).catch(err => {
- console.error('Failed to copy text: ', err);
- showAlert('danger', 'Failed to copy to clipboard');
- });
- }
- // Dashboard refresh functionality
- function refreshDashboard() {
- location.reload();
- }
- // Cluster management functions
- // Export volume servers data as CSV
- function exportVolumeServers() {
- const table = document.getElementById('hostsTable');
- if (!table) {
- showErrorMessage('No volume servers data to export');
- return;
- }
-
- let csv = 'Server ID,Address,Data Center,Rack,Volumes,Capacity,Usage\n';
-
- const rows = table.querySelectorAll('tbody tr');
- rows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 7) {
- const rowData = [
- cells[0].textContent.trim(),
- cells[1].textContent.trim(),
- cells[2].textContent.trim(),
- cells[3].textContent.trim(),
- cells[4].textContent.trim(),
- cells[5].textContent.trim(),
- cells[6].textContent.trim()
- ];
- csv += rowData.join(',') + '\n';
- }
- });
-
- downloadCSV(csv, 'seaweedfs-volume-servers.csv');
- }
- // Export volumes data as CSV
- function exportVolumes() {
- const table = document.getElementById('volumesTable');
- if (!table) {
- showErrorMessage('No volumes data to export');
- return;
- }
-
- // Get headers from the table (dynamically handles conditional columns)
- const headerCells = table.querySelectorAll('thead th');
- const headers = [];
- headerCells.forEach((cell, index) => {
- // Skip the Actions column (last column)
- if (index < headerCells.length - 1) {
- headers.push(cell.textContent.trim());
- }
- });
-
- let csv = headers.join(',') + '\n';
-
- const rows = table.querySelectorAll('tbody tr');
- rows.forEach(row => {
- const cells = row.querySelectorAll('td');
- const rowData = [];
- // Export all cells except the Actions column (last column)
- for (let i = 0; i < cells.length - 1; i++) {
- rowData.push(`"${cells[i].textContent.trim().replace(/"/g, '""')}"`);
- }
- csv += rowData.join(',') + '\n';
- });
-
- downloadCSV(csv, 'seaweedfs-volumes.csv');
- }
- // Export collections data as CSV
- function exportCollections() {
- const table = document.getElementById('collectionsTable');
- if (!table) {
- showAlert('error', 'Collections table not found');
- return;
- }
- const headers = ['Collection Name', 'Volumes', 'Files', 'Size', 'Disk Types'];
- const rows = [];
- // Get table rows
- const tableRows = table.querySelectorAll('tbody tr');
- tableRows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 5) {
- rows.push([
- cells[0].textContent.trim(),
- cells[1].textContent.trim(),
- cells[2].textContent.trim(),
- cells[3].textContent.trim(),
- formatDiskTypes(cells[4].textContent.trim())
- ]);
- }
- });
- // Generate CSV
- const csvContent = [headers, ...rows]
- .map(row => row.map(cell => `"${cell}"`).join(','))
- .join('\n');
- // Download
- const filename = `seaweedfs-collections-${new Date().toISOString().split('T')[0]}.csv`;
- downloadCSV(csvContent, filename);
- }
- // Export Masters to CSV
- function exportMasters() {
- const table = document.getElementById('mastersTable');
- if (!table) {
- showAlert('error', 'Masters table not found');
- return;
- }
- const headers = ['Address', 'Role', 'Suffrage'];
- const rows = [];
- // Get table rows
- const tableRows = table.querySelectorAll('tbody tr');
- tableRows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 3) {
- rows.push([
- cells[0].textContent.trim(),
- cells[1].textContent.trim(),
- cells[2].textContent.trim()
- ]);
- }
- });
- // Generate CSV
- const csvContent = [headers, ...rows]
- .map(row => row.map(cell => `"${cell}"`).join(','))
- .join('\n');
- // Download
- const filename = `seaweedfs-masters-${new Date().toISOString().split('T')[0]}.csv`;
- downloadCSV(csvContent, filename);
- }
- // Export Filers to CSV
- function exportFilers() {
- const table = document.getElementById('filersTable');
- if (!table) {
- showAlert('error', 'Filers table not found');
- return;
- }
- const headers = ['Address', 'Version', 'Data Center', 'Rack', 'Created At'];
- const rows = [];
- // Get table rows
- const tableRows = table.querySelectorAll('tbody tr');
- tableRows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 5) {
- rows.push([
- cells[0].textContent.trim(),
- cells[1].textContent.trim(),
- cells[2].textContent.trim(),
- cells[3].textContent.trim(),
- cells[4].textContent.trim()
- ]);
- }
- });
- // Generate CSV
- const csvContent = [headers, ...rows]
- .map(row => row.map(cell => `"${cell}"`).join(','))
- .join('\n');
- // Download
- const filename = `seaweedfs-filers-${new Date().toISOString().split('T')[0]}.csv`;
- downloadCSV(csvContent, filename);
- }
- // Export Users to CSV
- function exportUsers() {
- const table = document.getElementById('usersTable');
- if (!table) {
- showAlert('error', 'Users table not found');
- return;
- }
-
- const rows = table.querySelectorAll('tbody tr');
- if (rows.length === 0) {
- showErrorMessage('No users to export');
- return;
- }
-
- let csvContent = 'Username,Email,Access Key,Status,Created,Last Login\n';
-
- rows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 6) {
- const username = cells[0].textContent.trim();
- const email = cells[1].textContent.trim();
- const accessKey = cells[2].textContent.trim();
- const status = cells[3].textContent.trim();
- const created = cells[4].textContent.trim();
- const lastLogin = cells[5].textContent.trim();
-
- csvContent += `"${username}","${email}","${accessKey}","${status}","${created}","${lastLogin}"\n`;
- }
- });
-
- downloadCSV(csvContent, 'seaweedfs-users.csv');
- }
- // Confirm delete collection
- function confirmDeleteCollection(button) {
- const collectionName = button.getAttribute('data-collection-name');
- document.getElementById('deleteCollectionName').textContent = collectionName;
-
- const modal = new bootstrap.Modal(document.getElementById('deleteCollectionModal'));
- modal.show();
-
- // Set up confirm button
- document.getElementById('confirmDeleteCollection').onclick = function() {
- deleteCollection(collectionName);
- };
- }
- // Delete collection
- async function deleteCollection(collectionName) {
- try {
- const response = await fetch(`/api/collections/${collectionName}`, {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- }
- });
-
- if (response.ok) {
- showSuccessMessage(`Collection "${collectionName}" deleted successfully`);
- // Hide modal
- const modal = bootstrap.Modal.getInstance(document.getElementById('deleteCollectionModal'));
- modal.hide();
- // Refresh page
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- } else {
- const error = await response.json();
- showErrorMessage(`Failed to delete collection: ${error.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error('Error deleting collection:', error);
- showErrorMessage('Failed to delete collection. Please try again.');
- }
- }
- // Download CSV utility function
- function downloadCSV(csvContent, filename) {
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- const link = document.createElement('a');
-
- if (link.download !== undefined) {
- const url = URL.createObjectURL(blob);
- link.setAttribute('href', url);
- link.setAttribute('download', filename);
- link.style.visibility = 'hidden';
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- }
- // File Browser Functions
- // Toggle select all checkboxes
- function toggleSelectAll() {
- const selectAll = document.getElementById('selectAll');
- const checkboxes = document.querySelectorAll('.file-checkbox');
-
- checkboxes.forEach(checkbox => {
- checkbox.checked = selectAll.checked;
- });
-
- updateDeleteSelectedButton();
- }
- // Update visibility of delete selected button based on selection
- function updateDeleteSelectedButton() {
- const checkboxes = document.querySelectorAll('.file-checkbox:checked');
- const deleteBtn = document.getElementById('deleteSelectedBtn');
-
- if (deleteBtn) {
- if (checkboxes.length > 0) {
- deleteBtn.style.display = 'inline-block';
- deleteBtn.innerHTML = `<i class="fas fa-trash me-1"></i>Delete Selected (${checkboxes.length})`;
- } else {
- deleteBtn.style.display = 'none';
- }
- }
- }
- // Update select all checkbox state based on individual selections
- function updateSelectAllCheckbox() {
- const selectAll = document.getElementById('selectAll');
- const allCheckboxes = document.querySelectorAll('.file-checkbox');
- const checkedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
-
- if (selectAll && allCheckboxes.length > 0) {
- if (checkedCheckboxes.length === 0) {
- selectAll.checked = false;
- selectAll.indeterminate = false;
- } else if (checkedCheckboxes.length === allCheckboxes.length) {
- selectAll.checked = true;
- selectAll.indeterminate = false;
- } else {
- selectAll.checked = false;
- selectAll.indeterminate = true;
- }
- }
- }
- // Get selected file paths
- function getSelectedFilePaths() {
- const checkboxes = document.querySelectorAll('.file-checkbox:checked');
- return Array.from(checkboxes).map(cb => cb.value);
- }
- // Confirm delete selected files
- function confirmDeleteSelected() {
- const selectedPaths = getSelectedFilePaths();
-
- if (selectedPaths.length === 0) {
- showAlert('warning', 'No files selected');
- return;
- }
-
- const fileNames = selectedPaths.map(path => path.split('/').pop()).join(', ');
- const message = selectedPaths.length === 1
- ? `Are you sure you want to delete "${fileNames}"?`
- : `Are you sure you want to delete ${selectedPaths.length} selected items?\n\n${fileNames.substring(0, 200)}${fileNames.length > 200 ? '...' : ''}`;
-
- if (confirm(message)) {
- deleteSelectedFiles(selectedPaths);
- }
- }
- // Delete multiple selected files
- async function deleteSelectedFiles(filePaths) {
- if (!filePaths || filePaths.length === 0) {
- showAlert('warning', 'No files selected');
- return;
- }
-
- // Disable the delete button during operation
- const deleteBtn = document.getElementById('deleteSelectedBtn');
- const originalText = deleteBtn.innerHTML;
- deleteBtn.disabled = true;
- deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Deleting...';
-
- try {
- const response = await fetch('/api/files/delete-multiple', {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ paths: filePaths })
- });
- if (response.ok) {
- const result = await response.json();
-
- if (result.deleted > 0) {
- if (result.failed === 0) {
- showAlert('success', `Successfully deleted ${result.deleted} item(s)`);
- } else {
- showAlert('warning', `Deleted ${result.deleted} item(s), failed to delete ${result.failed} item(s)`);
- if (result.errors && result.errors.length > 0) {
- console.warn('Deletion errors:', result.errors);
- }
- }
-
- // Reload the page to update the file list
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- } else {
- let errorMessage = result.message || 'Failed to delete all selected items';
- if (result.errors && result.errors.length > 0) {
- errorMessage += ': ' + result.errors.join(', ');
- }
- showAlert('error', errorMessage);
- }
- } else {
- const error = await response.json();
- showAlert('error', `Failed to delete files: ${error.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error('Delete error:', error);
- showAlert('error', 'Failed to delete files');
- } finally {
- // Re-enable the button
- deleteBtn.disabled = false;
- deleteBtn.innerHTML = originalText;
- }
- }
- // Create new folder
- function createFolder() {
- const modal = new bootstrap.Modal(document.getElementById('createFolderModal'));
- modal.show();
- }
- // Upload file
- function uploadFile() {
- const modal = new bootstrap.Modal(document.getElementById('uploadFileModal'));
- modal.show();
- }
- // Submit create folder form
- async function submitCreateFolder() {
- const folderName = document.getElementById('folderName').value.trim();
- const currentPath = document.getElementById('currentPath').value;
-
- if (!folderName) {
- showErrorMessage('Please enter a folder name');
- return;
- }
-
- // Validate folder name
- if (folderName.includes('/') || folderName.includes('\\')) {
- showErrorMessage('Folder names cannot contain / or \\ characters');
- return;
- }
-
- // Additional validation for reserved names
- const reservedNames = ['.', '..', 'CON', 'PRN', 'AUX', 'NUL'];
- if (reservedNames.includes(folderName.toUpperCase())) {
- showErrorMessage('This folder name is reserved and cannot be used');
- return;
- }
-
- // Disable the button to prevent double submission
- const submitButton = document.querySelector('#createFolderModal .btn-primary');
- const originalText = submitButton.innerHTML;
- submitButton.disabled = true;
- submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Creating...';
-
- try {
- const response = await fetch('/api/files/create-folder', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- path: currentPath,
- folder_name: folderName
- })
- });
- if (response.ok) {
- showSuccessMessage(`Folder "${folderName}" created successfully`);
- // Hide modal
- const modal = bootstrap.Modal.getInstance(document.getElementById('createFolderModal'));
- modal.hide();
- // Clear form
- document.getElementById('folderName').value = '';
- // Refresh page
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- } else {
- const error = await response.json();
- showErrorMessage(`Failed to create folder: ${error.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error('Create folder error:', error);
- showErrorMessage('Failed to create folder. Please try again.');
- } finally {
- // Re-enable the button
- submitButton.disabled = false;
- submitButton.innerHTML = originalText;
- }
- }
- // Submit upload file form
- async function submitUploadFile() {
- const fileInput = document.getElementById('fileInput');
- const currentPath = document.getElementById('uploadPath').value;
-
- if (!fileInput.files || fileInput.files.length === 0) {
- showErrorMessage('Please select at least one file to upload');
- return;
- }
-
- const files = Array.from(fileInput.files);
- const totalSize = files.reduce((sum, file) => sum + file.size, 0);
-
- // Validate total file size (limit to 500MB for admin interface)
- const maxSize = 500 * 1024 * 1024; // 500MB total
- if (totalSize > maxSize) {
- showErrorMessage('Total file size exceeds 500MB limit. Please select fewer or smaller files.');
- return;
- }
-
- // Individual file size validation removed - no limit per file
-
- const formData = new FormData();
- files.forEach(file => {
- formData.append('files', file);
- });
- formData.append('path', currentPath);
-
- // Show progress bar and disable button
- const progressContainer = document.getElementById('uploadProgress');
- const progressBar = progressContainer.querySelector('.progress-bar');
- const uploadStatus = document.getElementById('uploadStatus');
- const submitButton = document.querySelector('#uploadFileModal .btn-primary');
- const originalText = submitButton.innerHTML;
-
- progressContainer.style.display = 'block';
- progressBar.style.width = '0%';
- progressBar.setAttribute('aria-valuenow', '0');
- progressBar.textContent = '0%';
- uploadStatus.textContent = `Uploading ${files.length} file(s)...`;
- submitButton.disabled = true;
- submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
-
- try {
- const xhr = new XMLHttpRequest();
-
- // Handle progress
- xhr.upload.addEventListener('progress', function(e) {
- if (e.lengthComputable) {
- const percentComplete = Math.round((e.loaded / e.total) * 100);
- progressBar.style.width = percentComplete + '%';
- progressBar.setAttribute('aria-valuenow', percentComplete);
- progressBar.textContent = percentComplete + '%';
- uploadStatus.textContent = `Uploading ${files.length} file(s)... ${percentComplete}%`;
- }
- });
-
- // Handle completion
- xhr.addEventListener('load', function() {
- if (xhr.status === 200) {
- try {
- const response = JSON.parse(xhr.responseText);
-
- if (response.uploaded > 0) {
- if (response.failed === 0) {
- showSuccessMessage(`Successfully uploaded ${response.uploaded} file(s)`);
- } else {
- showSuccessMessage(response.message);
- // Show details of failed uploads
- if (response.errors && response.errors.length > 0) {
- console.warn('Upload errors:', response.errors);
- }
- }
-
- // Hide modal and refresh page
- const modal = bootstrap.Modal.getInstance(document.getElementById('uploadFileModal'));
- modal.hide();
- setTimeout(() => {
- window.location.reload();
- }, 1000);
- } else {
- let errorMessage = response.message || 'All file uploads failed';
- if (response.errors && response.errors.length > 0) {
- errorMessage += ': ' + response.errors.join(', ');
- }
- showErrorMessage(errorMessage);
- }
- } catch (e) {
- showErrorMessage('Upload completed but response format was unexpected');
- }
- progressContainer.style.display = 'none';
- } else {
- let errorMessage = 'Unknown error';
- try {
- const error = JSON.parse(xhr.responseText);
- errorMessage = error.error || error.message || errorMessage;
- } catch (e) {
- errorMessage = `Server returned status ${xhr.status}`;
- }
- showErrorMessage(`Failed to upload files: ${errorMessage}`);
- progressContainer.style.display = 'none';
- }
- });
-
- // Handle errors
- xhr.addEventListener('error', function() {
- showErrorMessage('Failed to upload files. Please check your connection and try again.');
- progressContainer.style.display = 'none';
- });
-
- // Handle abort
- xhr.addEventListener('abort', function() {
- showErrorMessage('File upload was cancelled.');
- progressContainer.style.display = 'none';
- });
-
- // Send request
- xhr.open('POST', '/api/files/upload');
- xhr.send(formData);
-
- } catch (error) {
- console.error('Upload error:', error);
- showErrorMessage('Failed to upload files. Please try again.');
- progressContainer.style.display = 'none';
- } finally {
- // Re-enable the button
- submitButton.disabled = false;
- submitButton.innerHTML = originalText;
- }
- }
- // Export file list to CSV
- function exportFileList() {
- const table = document.getElementById('fileTable');
- if (!table) {
- showAlert('error', 'File table not found');
- return;
- }
- const headers = ['Name', 'Size', 'Type', 'Modified', 'Permissions'];
- const rows = [];
- // Get table rows
- const tableRows = table.querySelectorAll('tbody tr');
- tableRows.forEach(row => {
- const cells = row.querySelectorAll('td');
- if (cells.length >= 6) {
- rows.push([
- cells[1].textContent.trim(), // Name
- cells[2].textContent.trim(), // Size
- cells[3].textContent.trim(), // Type
- cells[4].textContent.trim(), // Modified
- cells[5].textContent.trim() // Permissions
- ]);
- }
- });
- // Generate CSV
- const csvContent = [headers, ...rows]
- .map(row => row.map(cell => `"${cell}"`).join(','))
- .join('\n');
- // Download
- const filename = `seaweedfs-files-${new Date().toISOString().split('T')[0]}.csv`;
- downloadCSV(csvContent, filename);
- }
- // Download file
- function downloadFile(filePath) {
- // Create download link using admin API
- const downloadUrl = `/api/files/download?path=${encodeURIComponent(filePath)}`;
- window.open(downloadUrl, '_blank');
- }
- // View file
- async function viewFile(filePath) {
- try {
- const response = await fetch(`/api/files/view?path=${encodeURIComponent(filePath)}`);
-
- if (!response.ok) {
- const error = await response.json();
- showAlert('error', `Failed to view file: ${error.error || 'Unknown error'}`);
- return;
- }
-
- const data = await response.json();
- showFileViewer(data);
-
- } catch (error) {
- console.error('View file error:', error);
- showAlert('error', 'Failed to view file');
- }
- }
- // Show file properties
- async function showProperties(filePath) {
- try {
- const response = await fetch(`/api/files/properties?path=${encodeURIComponent(filePath)}`);
-
- if (!response.ok) {
- const error = await response.json();
- showAlert('error', `Failed to get file properties: ${error.error || 'Unknown error'}`);
- return;
- }
-
- const properties = await response.json();
- showPropertiesModal(properties);
-
- } catch (error) {
- console.error('Properties error:', error);
- showAlert('error', 'Failed to get file properties');
- }
- }
- // Confirm delete file/folder
- function confirmDelete(filePath) {
- if (confirm(`Are you sure you want to delete "${filePath}"?`)) {
- deleteFile(filePath);
- }
- }
- // Delete file/folder
- async function deleteFile(filePath) {
- try {
- const response = await fetch('/api/files/delete', {
- method: 'DELETE',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ path: filePath })
- });
- if (response.ok) {
- showAlert('success', `Successfully deleted "${filePath}"`);
- // Reload the page to update the file list
- window.location.reload();
- } else {
- const error = await response.json();
- showAlert('error', `Failed to delete file: ${error.error || 'Unknown error'}`);
- }
- } catch (error) {
- console.error('Delete error:', error);
- showAlert('error', 'Failed to delete file');
- }
- }
- // Setup file manager specific event handlers
- function setupFileManagerEventHandlers() {
- // Handle Enter key in folder name input
- const folderNameInput = document.getElementById('folderName');
- if (folderNameInput) {
- folderNameInput.addEventListener('keypress', function(e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- submitCreateFolder();
- }
- });
- }
-
- // Handle file selection change to show preview
- const fileInput = document.getElementById('fileInput');
- if (fileInput) {
- fileInput.addEventListener('change', function(e) {
- updateFileListPreview();
- });
- }
-
- // Setup checkbox event listeners for file selection
- const checkboxes = document.querySelectorAll('.file-checkbox');
- checkboxes.forEach(checkbox => {
- checkbox.addEventListener('change', function() {
- updateDeleteSelectedButton();
- updateSelectAllCheckbox();
- });
- });
-
- // Setup drag and drop for file uploads
- setupDragAndDrop();
-
- // Clear form when modals are hidden
- const createFolderModal = document.getElementById('createFolderModal');
- if (createFolderModal) {
- createFolderModal.addEventListener('hidden.bs.modal', function() {
- document.getElementById('folderName').value = '';
- });
- }
-
- const uploadFileModal = document.getElementById('uploadFileModal');
- if (uploadFileModal) {
- uploadFileModal.addEventListener('hidden.bs.modal', function() {
- const fileInput = document.getElementById('fileInput');
- const progressContainer = document.getElementById('uploadProgress');
- const fileListPreview = document.getElementById('fileListPreview');
- fileInput.value = '';
- progressContainer.style.display = 'none';
- fileListPreview.style.display = 'none';
- });
- }
- }
- // Setup drag and drop functionality
- function setupDragAndDrop() {
- const dropZone = document.querySelector('.card-body'); // Main file listing area
- const uploadModal = document.getElementById('uploadFileModal');
-
- if (!dropZone || !uploadModal) return;
-
- // Prevent default drag behaviors
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
- dropZone.addEventListener(eventName, preventDefaults, false);
- document.body.addEventListener(eventName, preventDefaults, false);
- });
-
- // Highlight drop zone when item is dragged over it
- ['dragenter', 'dragover'].forEach(eventName => {
- dropZone.addEventListener(eventName, highlight, false);
- });
-
- ['dragleave', 'drop'].forEach(eventName => {
- dropZone.addEventListener(eventName, unhighlight, false);
- });
-
- // Handle dropped files
- dropZone.addEventListener('drop', handleDrop, false);
-
- function preventDefaults(e) {
- e.preventDefault();
- e.stopPropagation();
- }
-
- function highlight(e) {
- dropZone.classList.add('drag-over');
- // Add some visual feedback
- if (!dropZone.querySelector('.drag-overlay')) {
- const overlay = document.createElement('div');
- overlay.className = 'drag-overlay';
- overlay.innerHTML = `
- <div class="text-center p-5">
- <i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i>
- <h5>Drop files here to upload</h5>
- <p class="text-muted">Release to upload files to this directory</p>
- </div>
- `;
- overlay.style.cssText = `
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.9);
- border: 2px dashed #007bff;
- border-radius: 0.375rem;
- z-index: 1000;
- display: flex;
- align-items: center;
- justify-content: center;
- `;
- dropZone.style.position = 'relative';
- dropZone.appendChild(overlay);
- }
- }
-
- function unhighlight(e) {
- dropZone.classList.remove('drag-over');
- const overlay = dropZone.querySelector('.drag-overlay');
- if (overlay) {
- overlay.remove();
- }
- }
-
- function handleDrop(e) {
- const dt = e.dataTransfer;
- const files = dt.files;
-
- if (files.length > 0) {
- // Open upload modal and set files
- const fileInput = document.getElementById('fileInput');
- if (fileInput) {
- // Create a new FileList-like object
- const fileArray = Array.from(files);
-
- // Set files to input (this is a bit tricky with file inputs)
- const dataTransfer = new DataTransfer();
- fileArray.forEach(file => dataTransfer.items.add(file));
- fileInput.files = dataTransfer.files;
-
- // Update preview and show modal
- updateFileListPreview();
- const modal = new bootstrap.Modal(uploadModal);
- modal.show();
- }
- }
- }
- }
- // Update file list preview when files are selected
- function updateFileListPreview() {
- const fileInput = document.getElementById('fileInput');
- const fileListPreview = document.getElementById('fileListPreview');
- const selectedFilesList = document.getElementById('selectedFilesList');
-
- if (!fileInput.files || fileInput.files.length === 0) {
- fileListPreview.style.display = 'none';
- return;
- }
-
- const files = Array.from(fileInput.files);
- const totalSize = files.reduce((sum, file) => sum + file.size, 0);
-
- let html = `<div class="d-flex justify-content-between align-items-center mb-2">
- <strong>${files.length} file(s) selected</strong>
- <small class="text-muted">Total: ${formatBytes(totalSize)}</small>
- </div>`;
-
- files.forEach((file, index) => {
- const fileIcon = getFileIconByName(file.name);
- html += `<div class="d-flex justify-content-between align-items-center py-1 ${index > 0 ? 'border-top' : ''}">
- <div class="d-flex align-items-center">
- <i class="fas ${fileIcon} me-2 text-muted"></i>
- <span class="text-truncate" style="max-width: 200px;" title="${file.name}">${file.name}</span>
- </div>
- <small class="text-muted">${formatBytes(file.size)}</small>
- </div>`;
- });
-
- selectedFilesList.innerHTML = html;
- fileListPreview.style.display = 'block';
- }
- // Get file icon based on file name/extension
- function getFileIconByName(fileName) {
- const ext = fileName.split('.').pop().toLowerCase();
-
- switch (ext) {
- case 'jpg':
- case 'jpeg':
- case 'png':
- case 'gif':
- case 'bmp':
- case 'svg':
- return 'fa-image';
- case 'mp4':
- case 'avi':
- case 'mov':
- case 'wmv':
- case 'flv':
- return 'fa-video';
- case 'mp3':
- case 'wav':
- case 'flac':
- case 'aac':
- return 'fa-music';
- case 'pdf':
- return 'fa-file-pdf';
- case 'doc':
- case 'docx':
- return 'fa-file-word';
- case 'xls':
- case 'xlsx':
- return 'fa-file-excel';
- case 'ppt':
- case 'pptx':
- return 'fa-file-powerpoint';
- case 'txt':
- case 'md':
- return 'fa-file-text';
- case 'zip':
- case 'rar':
- case '7z':
- case 'tar':
- case 'gz':
- return 'fa-file-archive';
- case 'js':
- case 'ts':
- case 'html':
- case 'css':
- case 'json':
- case 'xml':
- return 'fa-file-code';
- default:
- return 'fa-file';
- }
- }
- // Quota Management Functions
- // Show quota management modal
- function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) {
- document.getElementById('quotaBucketName').value = bucketName;
- document.getElementById('quotaEnabled').checked = quotaEnabled;
-
- // Convert quota to appropriate unit and set values
- const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes
- const { size, unit } = convertBytesToBestUnit(quotaBytes);
-
- document.getElementById('quotaSizeMB').value = size;
- document.getElementById('quotaUnitMB').value = unit;
-
- // Show/hide quota size settings based on enabled state
- const quotaSizeSettings = document.getElementById('quotaSizeSettings');
- if (quotaEnabled) {
- quotaSizeSettings.style.display = 'block';
- } else {
- quotaSizeSettings.style.display = 'none';
- }
-
- const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal'));
- modal.show();
- }
- // Convert bytes to the best unit (TB, GB, or MB)
- function convertBytesToBestUnit(bytes) {
- if (bytes === 0) {
- return { size: 0, unit: 'MB' };
- }
-
- // Check if it's a clean TB value
- if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) {
- return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' };
- }
-
- // Check if it's a clean GB value
- if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) {
- return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' };
- }
-
- // Default to MB
- return { size: bytes / (1024 * 1024), unit: 'MB' };
- }
- // Handle quota update form submission
- async function handleUpdateQuota(event) {
- event.preventDefault();
-
- const form = event.target;
- const formData = new FormData(form);
- const bucketName = document.getElementById('quotaBucketName').value;
-
- const quotaData = {
- quota_enabled: formData.get('quota_enabled') === 'on',
- quota_size: parseInt(formData.get('quota_size')) || 0,
- quota_unit: formData.get('quota_unit') || 'MB'
- };
- try {
- const response = await fetch(`/api/s3/buckets/${bucketName}/quota`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(quotaData)
- });
- const result = await response.json();
- if (response.ok) {
- // Success
- showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`);
-
- // Close modal
- const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal'));
- modal.hide();
-
- // Refresh the page after a short delay
- setTimeout(() => {
- location.reload();
- }, 1500);
- } else {
- // Error
- showAlert('danger', result.error || 'Failed to update bucket quota');
- }
- } catch (error) {
- console.error('Error updating bucket quota:', error);
- showAlert('danger', 'Network error occurred while updating bucket quota');
- }
- }
- // Show file viewer modal
- function showFileViewer(data) {
- const file = data.file;
- const content = data.content || '';
- const viewable = data.viewable !== false;
-
- // Create modal HTML
- const modalHtml = `
- <div class="modal fade" id="fileViewerModal" tabindex="-1" aria-labelledby="fileViewerModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-xl">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="fileViewerModalLabel">
- <i class="fas fa-eye me-2"></i>File Viewer: ${file.name}
- </h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- ${viewable ? createFileViewerContent(file, content) : createNonViewableContent(data.reason || 'File cannot be viewed')}
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-primary" onclick="downloadFile('${file.full_path}')">
- <i class="fas fa-download me-1"></i>Download
- </button>
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
- </div>
- </div>
- </div>
- </div>
- `;
-
- // Remove existing modal if any
- const existingModal = document.getElementById('fileViewerModal');
- if (existingModal) {
- existingModal.remove();
- }
-
- // Add modal to DOM
- document.body.insertAdjacentHTML('beforeend', modalHtml);
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById('fileViewerModal'));
- modal.show();
-
- // Clean up when modal is hidden
- document.getElementById('fileViewerModal').addEventListener('hidden.bs.modal', function () {
- this.remove();
- });
- }
- // Create file viewer content based on file type
- function createFileViewerContent(file, content) {
- if (file.mime.startsWith('image/')) {
- return `
- <div class="text-center">
- <img src="/api/files/download?path=${encodeURIComponent(file.full_path)}"
- class="img-fluid" alt="${file.name}" style="max-height: 500px;">
- </div>
- `;
- } else if (file.mime.startsWith('text/') || file.mime === 'application/json' || file.mime === 'application/javascript') {
- const language = getLanguageFromMime(file.mime, file.name);
- return `
- <div class="mb-3">
- <small class="text-muted">
- <i class="fas fa-info-circle me-1"></i>
- Size: ${formatBytes(file.size)} | Type: ${file.mime}
- </small>
- </div>
- <pre><code class="language-${language}" style="max-height: 400px; overflow-y: auto;">${escapeHtml(content)}</code></pre>
- `;
- } else if (file.mime === 'application/pdf') {
- return `
- <div class="text-center">
- <embed src="/api/files/download?path=${encodeURIComponent(file.full_path)}"
- type="application/pdf" width="100%" height="500px">
- </div>
- `;
- } else {
- return createNonViewableContent('This file type cannot be previewed in the browser.');
- }
- }
- // Create non-viewable content message
- function createNonViewableContent(reason) {
- return `
- <div class="text-center py-5">
- <i class="fas fa-file fa-3x text-muted mb-3"></i>
- <h5 class="text-muted">Cannot preview file</h5>
- <p class="text-muted">${reason}</p>
- </div>
- `;
- }
- // Get language for syntax highlighting
- function getLanguageFromMime(mime, filename) {
- // First check MIME type
- switch (mime) {
- case 'application/json': return 'json';
- case 'application/javascript': return 'javascript';
- case 'text/html': return 'html';
- case 'text/css': return 'css';
- case 'application/xml': return 'xml';
- case 'text/typescript': return 'typescript';
- case 'text/x-python': return 'python';
- case 'text/x-go': return 'go';
- case 'text/x-java': return 'java';
- case 'text/x-c': return 'c';
- case 'text/x-c++': return 'cpp';
- case 'text/x-c-header': return 'c';
- case 'text/x-shellscript': return 'bash';
- case 'text/x-php': return 'php';
- case 'text/x-ruby': return 'ruby';
- case 'text/x-perl': return 'perl';
- case 'text/x-rust': return 'rust';
- case 'text/x-swift': return 'swift';
- case 'text/x-kotlin': return 'kotlin';
- case 'text/x-scala': return 'scala';
- case 'text/x-dockerfile': return 'dockerfile';
- case 'text/yaml': return 'yaml';
- case 'text/csv': return 'csv';
- case 'text/sql': return 'sql';
- case 'text/markdown': return 'markdown';
- }
-
- // Fallback to file extension
- const ext = filename.split('.').pop().toLowerCase();
- switch (ext) {
- case 'js': case 'mjs': return 'javascript';
- case 'ts': return 'typescript';
- case 'py': return 'python';
- case 'go': return 'go';
- case 'java': return 'java';
- case 'cpp': case 'cc': case 'cxx': case 'c++': return 'cpp';
- case 'c': return 'c';
- case 'h': case 'hpp': return 'c';
- case 'sh': case 'bash': case 'zsh': case 'fish': return 'bash';
- case 'php': return 'php';
- case 'rb': return 'ruby';
- case 'pl': return 'perl';
- case 'rs': return 'rust';
- case 'swift': return 'swift';
- case 'kt': return 'kotlin';
- case 'scala': return 'scala';
- case 'yml': case 'yaml': return 'yaml';
- case 'md': case 'markdown': return 'markdown';
- case 'sql': return 'sql';
- case 'csv': return 'csv';
- case 'dockerfile': return 'dockerfile';
- case 'gitignore': case 'gitattributes': return 'text';
- case 'env': return 'bash';
- case 'cfg': case 'conf': case 'ini': case 'properties': return 'ini';
- default: return 'text';
- }
- }
- // Show properties modal
- function showPropertiesModal(properties) {
- // Create modal HTML
- const modalHtml = `
- <div class="modal fade" id="propertiesModal" tabindex="-1" aria-labelledby="propertiesModalLabel" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="propertiesModalLabel">
- <i class="fas fa-info me-2"></i>Properties: ${properties.name}
- </h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- ${createPropertiesContent(properties)}
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
- </div>
- </div>
- </div>
- </div>
- `;
-
- // Remove existing modal if any
- const existingModal = document.getElementById('propertiesModal');
- if (existingModal) {
- existingModal.remove();
- }
-
- // Add modal to DOM
- document.body.insertAdjacentHTML('beforeend', modalHtml);
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById('propertiesModal'));
- modal.show();
-
- // Clean up when modal is hidden
- document.getElementById('propertiesModal').addEventListener('hidden.bs.modal', function () {
- this.remove();
- });
- }
- // Create properties content
- function createPropertiesContent(properties) {
- let html = `
- <div class="row">
- <div class="col-md-6">
- <h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6>
- <table class="table table-sm">
- <tr><td><strong>Name:</strong></td><td>${properties.name}</td></tr>
- <tr><td><strong>Full Path:</strong></td><td><code>${properties.full_path}</code></td></tr>
- <tr><td><strong>Type:</strong></td><td>${properties.is_directory ? 'Directory' : 'File'}</td></tr>
- `;
-
- if (!properties.is_directory) {
- html += `
- <tr><td><strong>Size:</strong></td><td>${properties.size_formatted || formatBytes(properties.size || 0)}</td></tr>
- <tr><td><strong>MIME Type:</strong></td><td>${properties.mime_type || 'Unknown'}</td></tr>
- `;
- }
-
- html += `
- </table>
- </div>
- <div class="col-md-6">
- <h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6>
- <table class="table table-sm">
- `;
-
- if (properties.modified_time) {
- html += `<tr><td><strong>Modified:</strong></td><td>${properties.modified_time}</td></tr>`;
- }
- if (properties.created_time) {
- html += `<tr><td><strong>Created:</strong></td><td>${properties.created_time}</td></tr>`;
- }
-
- html += `
- </table>
-
- <h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6>
- <table class="table table-sm">
- <tr><td><strong>Mode:</strong></td><td><code>${properties.file_mode_formatted || properties.file_mode}</code></td></tr>
- <tr><td><strong>UID:</strong></td><td>${properties.uid || 'N/A'}</td></tr>
- <tr><td><strong>GID:</strong></td><td>${properties.gid || 'N/A'}</td></tr>
- </table>
- </div>
- </div>
- `;
-
- // Add TTL information if available
- if (properties.ttl_seconds && properties.ttl_seconds > 0) {
- html += `
- <div class="row mt-3">
- <div class="col-12">
- <h6 class="text-primary"><i class="fas fa-hourglass-half me-1"></i>TTL (Time To Live)</h6>
- <table class="table table-sm">
- <tr><td><strong>TTL:</strong></td><td>${properties.ttl_formatted || properties.ttl_seconds + ' seconds'}</td></tr>
- </table>
- </div>
- </div>
- `;
- }
-
- // Add chunk information if available
- if (properties.chunks && properties.chunks.length > 0) {
- html += `
- <div class="row mt-3">
- <div class="col-12">
- <h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunks (${properties.chunk_count})</h6>
- <div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
- <table class="table table-sm">
- <thead>
- <tr>
- <th>File ID</th>
- <th>Offset</th>
- <th>Size</th>
- <th>ETag</th>
- </tr>
- </thead>
- <tbody>
- `;
-
- properties.chunks.forEach(chunk => {
- html += `
- <tr>
- <td><code class="small">${chunk.file_id}</code></td>
- <td>${formatBytes(chunk.offset)}</td>
- <td>${formatBytes(chunk.size)}</td>
- <td><code class="small">${chunk.e_tag || 'N/A'}</code></td>
- </tr>
- `;
- });
-
- html += `
- </tbody>
- </table>
- </div>
- </div>
- </div>
- `;
- }
-
- // Add extended attributes if available
- if (properties.extended && Object.keys(properties.extended).length > 0) {
- html += `
- <div class="row mt-3">
- <div class="col-12">
- <h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6>
- <table class="table table-sm">
- `;
-
- Object.entries(properties.extended).forEach(([key, value]) => {
- html += `<tr><td><strong>${key}:</strong></td><td>${value}</td></tr>`;
- });
-
- html += `
- </table>
- </div>
- </div>
- `;
- }
-
- return html;
- }
- // Utility function to escape HTML
- function escapeHtml(text) {
- var map = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- };
- return text.replace(/[&<>"']/g, function(m) { return map[m]; });
- }
- // ============================================================================
- // USER MANAGEMENT FUNCTIONS
- // ============================================================================
- // Global variables for user management
- let currentEditingUser = '';
- let currentAccessKeysUser = '';
- // User Management Functions
- async function handleCreateUser() {
- const form = document.getElementById('createUserForm');
- const formData = new FormData(form);
-
- // Get selected actions
- const actionsSelect = document.getElementById('actions');
- const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
-
- const userData = {
- username: formData.get('username'),
- email: formData.get('email'),
- actions: selectedActions,
- generate_key: formData.get('generateKey') === 'on'
- };
-
- try {
- const response = await fetch('/api/users', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(userData)
- });
-
- if (response.ok) {
- const result = await response.json();
- showSuccessMessage('User created successfully');
-
- // Show the created access key if generated
- if (result.user && result.user.access_key) {
- showNewAccessKeyModal(result.user);
- }
-
- // Close modal and refresh page
- const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
- modal.hide();
- form.reset();
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error creating user:', error);
- showErrorMessage('Failed to create user: ' + error.message);
- }
- }
- async function editUser(username) {
- currentEditingUser = username;
-
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
-
- // Populate edit form
- document.getElementById('editUsername').value = username;
- document.getElementById('editEmail').value = user.email || '';
-
- // Set selected actions
- const actionsSelect = document.getElementById('editActions');
- Array.from(actionsSelect.options).forEach(option => {
- option.selected = user.actions && user.actions.includes(option.value);
- });
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
- modal.show();
- } else {
- showErrorMessage('Failed to load user details');
- }
- } catch (error) {
- console.error('Error loading user:', error);
- showErrorMessage('Failed to load user details');
- }
- }
- async function handleUpdateUser() {
- const form = document.getElementById('editUserForm');
- const formData = new FormData(form);
-
- // Get selected actions
- const actionsSelect = document.getElementById('editActions');
- const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
-
- const userData = {
- email: formData.get('email'),
- actions: selectedActions
- };
-
- try {
- const response = await fetch(`/api/users/${currentEditingUser}`, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(userData)
- });
-
- if (response.ok) {
- showSuccessMessage('User updated successfully');
-
- // Close modal and refresh page
- const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
- modal.hide();
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error updating user:', error);
- showErrorMessage('Failed to update user: ' + error.message);
- }
- }
- function confirmDeleteUser(username) {
- confirmAction(
- `Are you sure you want to delete user "${username}"? This action cannot be undone.`,
- () => deleteUserConfirmed(username)
- );
- }
- function deleteUser(username) {
- confirmDeleteUser(username);
- }
- async function deleteUserConfirmed(username) {
- try {
- const response = await fetch(`/api/users/${username}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- showSuccessMessage('User deleted successfully');
- setTimeout(() => window.location.reload(), 1000);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error deleting user:', error);
- showErrorMessage('Failed to delete user: ' + error.message);
- }
- }
- async function showUserDetails(username) {
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
-
- const content = createUserDetailsContent(user);
- document.getElementById('userDetailsContent').innerHTML = content;
-
- const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
- modal.show();
- } else {
- showErrorMessage('Failed to load user details');
- }
- } catch (error) {
- console.error('Error loading user details:', error);
- showErrorMessage('Failed to load user details');
- }
- }
- function createUserDetailsContent(user) {
- return `
- <div class="row">
- <div class="col-md-6">
- <h6 class="text-muted">Basic Information</h6>
- <table class="table table-sm">
- <tr>
- <td><strong>Username:</strong></td>
- <td>${escapeHtml(user.username)}</td>
- </tr>
- <tr>
- <td><strong>Email:</strong></td>
- <td>${escapeHtml(user.email || 'Not set')}</td>
- </tr>
- </table>
- </div>
- <div class="col-md-6">
- <h6 class="text-muted">Permissions</h6>
- <div class="mb-3">
- ${user.actions && user.actions.length > 0 ?
- user.actions.map(action => `<span class="badge bg-info me-1">${action}</span>`).join('') :
- '<span class="text-muted">No permissions assigned</span>'
- }
- </div>
-
- <h6 class="text-muted">Access Keys</h6>
- ${user.access_keys && user.access_keys.length > 0 ?
- createAccessKeysTable(user.access_keys) :
- '<p class="text-muted">No access keys</p>'
- }
- </div>
- </div>
- `;
- }
- function createAccessKeysTable(accessKeys) {
- return `
- <div class="table-responsive">
- <table class="table table-sm">
- <thead>
- <tr>
- <th>Access Key</th>
- <th>Created</th>
- </tr>
- </thead>
- <tbody>
- ${accessKeys.map(key => `
- <tr>
- <td><code>${key.access_key}</code></td>
- <td>${new Date(key.created_at).toLocaleDateString()}</td>
- </tr>
- `).join('')}
- </tbody>
- </table>
- </div>
- `;
- }
- async function manageAccessKeys(username) {
- currentAccessKeysUser = username;
- document.getElementById('accessKeysUsername').textContent = username;
-
- await loadAccessKeys(username);
-
- const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
- modal.show();
- }
- async function loadAccessKeys(username) {
- try {
- const response = await fetch(`/api/users/${username}`);
- if (response.ok) {
- const user = await response.json();
-
- const content = createAccessKeysManagementContent(user.access_keys || []);
- document.getElementById('accessKeysContent').innerHTML = content;
- } else {
- document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Failed to load access keys</p>';
- }
- } catch (error) {
- console.error('Error loading access keys:', error);
- document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Error loading access keys</p>';
- }
- }
- function createAccessKeysManagementContent(accessKeys) {
- if (accessKeys.length === 0) {
- return '<p class="text-muted">No access keys found. Create one to get started.</p>';
- }
-
- return `
- <div class="table-responsive">
- <table class="table table-hover">
- <thead>
- <tr>
- <th>Access Key</th>
- <th>Secret Key</th>
- <th>Created</th>
- <th>Actions</th>
- </tr>
- </thead>
- <tbody>
- ${accessKeys.map(key => `
- <tr>
- <td>
- <code>${key.access_key}</code>
- <button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('${key.access_key}')">
- <i class="fas fa-copy"></i>
- </button>
- </td>
- <td>
- <code class="text-muted">••••••••••••••••</code>
- <button class="btn btn-sm btn-outline-secondary ms-2" onclick="showSecretKey('${key.access_key}', '${key.secret_key}')">
- <i class="fas fa-eye"></i>
- </button>
- </td>
- <td>${new Date(key.created_at).toLocaleDateString()}</td>
- <td>
- <button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteAccessKey('${key.access_key}')">
- <i class="fas fa-trash"></i>
- </button>
- </td>
- </tr>
- `).join('')}
- </tbody>
- </table>
- </div>
- `;
- }
- async function createAccessKey() {
- if (!currentAccessKeysUser) {
- showErrorMessage('No user selected');
- return;
- }
-
- try {
- const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- }
- });
-
- if (response.ok) {
- const result = await response.json();
- showSuccessMessage('Access key created successfully');
-
- // Show the new access key
- showNewAccessKeyModal(result.access_key);
-
- // Reload access keys
- await loadAccessKeys(currentAccessKeysUser);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error creating access key:', error);
- showErrorMessage('Failed to create access key: ' + error.message);
- }
- }
- function confirmDeleteAccessKey(accessKeyId) {
- confirmAction(
- `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`,
- () => deleteAccessKeyConfirmed(accessKeyId)
- );
- }
- async function deleteAccessKeyConfirmed(accessKeyId) {
- try {
- const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, {
- method: 'DELETE'
- });
-
- if (response.ok) {
- showSuccessMessage('Access key deleted successfully');
-
- // Reload access keys
- await loadAccessKeys(currentAccessKeysUser);
- } else {
- const error = await response.json();
- showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
- }
- } catch (error) {
- console.error('Error deleting access key:', error);
- showErrorMessage('Failed to delete access key: ' + error.message);
- }
- }
- function showSecretKey(accessKey, secretKey) {
- const content = `
- <div class="alert alert-info">
- <i class="fas fa-info-circle me-2"></i>
- <strong>Access Key Details:</strong> These credentials provide access to your object storage. Keep them secure and don't share them.
- </div>
- <div class="mb-3">
- <label class="form-label"><strong>Access Key:</strong></label>
- <div class="input-group">
- <input type="text" class="form-control" value="${accessKey}" readonly>
- <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKey}')">
- <i class="fas fa-copy"></i>
- </button>
- </div>
- </div>
- <div class="mb-3">
- <label class="form-label"><strong>Secret Key:</strong></label>
- <div class="input-group">
- <input type="text" class="form-control" value="${secretKey}" readonly>
- <button class="btn btn-outline-secondary" onclick="copyToClipboard('${secretKey}')">
- <i class="fas fa-copy"></i>
- </button>
- </div>
- </div>
- `;
-
- showModal('Access Key Details', content);
- }
- function showNewAccessKeyModal(accessKeyData) {
- const content = `
- <div class="alert alert-success">
- <i class="fas fa-check-circle me-2"></i>
- <strong>Success!</strong> Your new access key has been created.
- </div>
- <div class="alert alert-info">
- <i class="fas fa-info-circle me-2"></i>
- <strong>Important:</strong> These credentials provide access to your object storage. Keep them secure and don't share them. You can view them again through the user management interface if needed.
- </div>
- <div class="mb-3">
- <label class="form-label"><strong>Access Key:</strong></label>
- <div class="input-group">
- <input type="text" class="form-control" value="${accessKeyData.access_key}" readonly>
- <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.access_key}')">
- <i class="fas fa-copy"></i>
- </button>
- </div>
- </div>
- <div class="mb-3">
- <label class="form-label"><strong>Secret Key:</strong></label>
- <div class="input-group">
- <input type="text" class="form-control" value="${accessKeyData.secret_key}" readonly>
- <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.secret_key}')">
- <i class="fas fa-copy"></i>
- </button>
- </div>
- </div>
- `;
-
- showModal('New Access Key Created', content);
- }
- function showModal(title, content) {
- // Create a dynamic modal
- const modalId = 'dynamicModal_' + Date.now();
- const modalHtml = `
- <div class="modal fade" id="${modalId}" tabindex="-1" role="dialog">
- <div class="modal-dialog" role="document">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">${title}</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
- </div>
- <div class="modal-body">
- ${content}
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
- </div>
- </div>
- </div>
- </div>
- `;
-
- // Add modal to body
- document.body.insertAdjacentHTML('beforeend', modalHtml);
-
- // Show modal
- const modal = new bootstrap.Modal(document.getElementById(modalId));
- modal.show();
-
- // Remove modal from DOM when hidden
- document.getElementById(modalId).addEventListener('hidden.bs.modal', function() {
- this.remove();
- });
- }
-
|