admin.js 87 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533
  1. // SeaweedFS Dashboard JavaScript
  2. // Global variables
  3. let bucketToDelete = '';
  4. // Initialize dashboard when DOM is loaded
  5. document.addEventListener('DOMContentLoaded', function() {
  6. initializeDashboard();
  7. initializeEventHandlers();
  8. setupFormValidation();
  9. setupFileManagerEventHandlers();
  10. // Initialize delete button visibility on file browser page
  11. if (window.location.pathname === '/files') {
  12. updateDeleteSelectedButton();
  13. }
  14. });
  15. function initializeDashboard() {
  16. // Set up HTMX event listeners
  17. setupHTMXListeners();
  18. // Initialize tooltips
  19. initializeTooltips();
  20. // Set up periodic refresh
  21. setupAutoRefresh();
  22. // Set active navigation
  23. setActiveNavigation();
  24. // Set up submenu behavior
  25. setupSubmenuBehavior();
  26. }
  27. // HTMX event listeners
  28. function setupHTMXListeners() {
  29. // Show loading indicator on requests
  30. document.body.addEventListener('htmx:beforeRequest', function(evt) {
  31. showLoadingIndicator();
  32. });
  33. // Hide loading indicator on completion
  34. document.body.addEventListener('htmx:afterRequest', function(evt) {
  35. hideLoadingIndicator();
  36. });
  37. // Handle errors
  38. document.body.addEventListener('htmx:responseError', function(evt) {
  39. handleHTMXError(evt);
  40. });
  41. }
  42. // Initialize Bootstrap tooltips
  43. function initializeTooltips() {
  44. var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
  45. var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
  46. return new bootstrap.Tooltip(tooltipTriggerEl);
  47. });
  48. }
  49. // Set up auto-refresh for dashboard data
  50. function setupAutoRefresh() {
  51. // Refresh dashboard data every 30 seconds
  52. setInterval(function() {
  53. if (window.location.pathname === '/dashboard') {
  54. htmx.trigger('#dashboard-content', 'refresh');
  55. }
  56. }, 30000);
  57. }
  58. // Set active navigation item
  59. function setActiveNavigation() {
  60. const currentPath = window.location.pathname;
  61. const navLinks = document.querySelectorAll('.sidebar .nav-link');
  62. navLinks.forEach(function(link) {
  63. const href = link.getAttribute('href');
  64. let isActive = false;
  65. if (href === currentPath) {
  66. isActive = true;
  67. } else if (currentPath === '/' && href === '/admin') {
  68. isActive = true;
  69. } else if (currentPath.startsWith('/s3/') && href === '/s3/buckets') {
  70. isActive = true;
  71. }
  72. // Note: Removed the problematic cluster condition that was highlighting all submenu items
  73. if (isActive) {
  74. link.classList.add('active');
  75. } else {
  76. link.classList.remove('active');
  77. }
  78. });
  79. }
  80. // Set up submenu behavior
  81. function setupSubmenuBehavior() {
  82. const currentPath = window.location.pathname;
  83. // If we're on a cluster page, expand the cluster submenu
  84. if (currentPath.startsWith('/cluster/')) {
  85. const clusterSubmenu = document.getElementById('clusterSubmenu');
  86. if (clusterSubmenu) {
  87. clusterSubmenu.classList.add('show');
  88. // Update the parent toggle button state
  89. const toggleButton = document.querySelector('[data-bs-target="#clusterSubmenu"]');
  90. if (toggleButton) {
  91. toggleButton.classList.remove('collapsed');
  92. toggleButton.setAttribute('aria-expanded', 'true');
  93. }
  94. }
  95. }
  96. // If we're on an object store page, expand the object store submenu
  97. if (currentPath.startsWith('/object-store/')) {
  98. const objectStoreSubmenu = document.getElementById('objectStoreSubmenu');
  99. if (objectStoreSubmenu) {
  100. objectStoreSubmenu.classList.add('show');
  101. // Update the parent toggle button state
  102. const toggleButton = document.querySelector('[data-bs-target="#objectStoreSubmenu"]');
  103. if (toggleButton) {
  104. toggleButton.classList.remove('collapsed');
  105. toggleButton.setAttribute('aria-expanded', 'true');
  106. }
  107. }
  108. }
  109. // If we're on a maintenance page, expand the maintenance submenu
  110. if (currentPath.startsWith('/maintenance')) {
  111. const maintenanceSubmenu = document.getElementById('maintenanceSubmenu');
  112. if (maintenanceSubmenu) {
  113. maintenanceSubmenu.classList.add('show');
  114. // Update the parent toggle button state
  115. const toggleButton = document.querySelector('[data-bs-target="#maintenanceSubmenu"]');
  116. if (toggleButton) {
  117. toggleButton.classList.remove('collapsed');
  118. toggleButton.setAttribute('aria-expanded', 'true');
  119. }
  120. }
  121. }
  122. // Prevent submenu from collapsing when clicking on submenu items
  123. const clusterSubmenuLinks = document.querySelectorAll('#clusterSubmenu .nav-link');
  124. clusterSubmenuLinks.forEach(function(link) {
  125. link.addEventListener('click', function(e) {
  126. // Don't prevent the navigation, just stop the collapse behavior
  127. e.stopPropagation();
  128. });
  129. });
  130. const objectStoreSubmenuLinks = document.querySelectorAll('#objectStoreSubmenu .nav-link');
  131. objectStoreSubmenuLinks.forEach(function(link) {
  132. link.addEventListener('click', function(e) {
  133. // Don't prevent the navigation, just stop the collapse behavior
  134. e.stopPropagation();
  135. });
  136. });
  137. const maintenanceSubmenuLinks = document.querySelectorAll('#maintenanceSubmenu .nav-link');
  138. maintenanceSubmenuLinks.forEach(function(link) {
  139. link.addEventListener('click', function(e) {
  140. // Don't prevent the navigation, just stop the collapse behavior
  141. e.stopPropagation();
  142. });
  143. });
  144. // Handle the main cluster toggle
  145. const clusterToggle = document.querySelector('[data-bs-target="#clusterSubmenu"]');
  146. if (clusterToggle) {
  147. clusterToggle.addEventListener('click', function(e) {
  148. e.preventDefault();
  149. const submenu = document.getElementById('clusterSubmenu');
  150. const isExpanded = submenu.classList.contains('show');
  151. if (isExpanded) {
  152. // Collapse
  153. submenu.classList.remove('show');
  154. this.classList.add('collapsed');
  155. this.setAttribute('aria-expanded', 'false');
  156. } else {
  157. // Expand
  158. submenu.classList.add('show');
  159. this.classList.remove('collapsed');
  160. this.setAttribute('aria-expanded', 'true');
  161. }
  162. });
  163. }
  164. // Handle the main object store toggle
  165. const objectStoreToggle = document.querySelector('[data-bs-target="#objectStoreSubmenu"]');
  166. if (objectStoreToggle) {
  167. objectStoreToggle.addEventListener('click', function(e) {
  168. e.preventDefault();
  169. const submenu = document.getElementById('objectStoreSubmenu');
  170. const isExpanded = submenu.classList.contains('show');
  171. if (isExpanded) {
  172. // Collapse
  173. submenu.classList.remove('show');
  174. this.classList.add('collapsed');
  175. this.setAttribute('aria-expanded', 'false');
  176. } else {
  177. // Expand
  178. submenu.classList.add('show');
  179. this.classList.remove('collapsed');
  180. this.setAttribute('aria-expanded', 'true');
  181. }
  182. });
  183. }
  184. // Handle the main maintenance toggle
  185. const maintenanceToggle = document.querySelector('[data-bs-target="#maintenanceSubmenu"]');
  186. if (maintenanceToggle) {
  187. maintenanceToggle.addEventListener('click', function(e) {
  188. e.preventDefault();
  189. const submenu = document.getElementById('maintenanceSubmenu');
  190. const isExpanded = submenu.classList.contains('show');
  191. if (isExpanded) {
  192. // Collapse
  193. submenu.classList.remove('show');
  194. this.classList.add('collapsed');
  195. this.setAttribute('aria-expanded', 'false');
  196. } else {
  197. // Expand
  198. submenu.classList.add('show');
  199. this.classList.remove('collapsed');
  200. this.setAttribute('aria-expanded', 'true');
  201. }
  202. });
  203. }
  204. }
  205. // Loading indicator functions
  206. function showLoadingIndicator() {
  207. const indicator = document.getElementById('loading-indicator');
  208. if (indicator) {
  209. indicator.style.display = 'block';
  210. }
  211. // Add loading class to body
  212. document.body.classList.add('loading');
  213. }
  214. function hideLoadingIndicator() {
  215. const indicator = document.getElementById('loading-indicator');
  216. if (indicator) {
  217. indicator.style.display = 'none';
  218. }
  219. // Remove loading class from body
  220. document.body.classList.remove('loading');
  221. }
  222. // Handle HTMX errors
  223. function handleHTMXError(evt) {
  224. console.error('HTMX Request Error:', evt.detail);
  225. // Show error toast or message
  226. showErrorMessage('Request failed. Please try again.');
  227. hideLoadingIndicator();
  228. }
  229. // Utility functions
  230. function showErrorMessage(message) {
  231. // Create toast element
  232. const toast = document.createElement('div');
  233. toast.className = 'toast align-items-center text-white bg-danger border-0';
  234. toast.setAttribute('role', 'alert');
  235. toast.setAttribute('aria-live', 'assertive');
  236. toast.setAttribute('aria-atomic', 'true');
  237. toast.innerHTML = `
  238. <div class="d-flex">
  239. <div class="toast-body">
  240. <i class="fas fa-exclamation-triangle me-2"></i>
  241. ${message}
  242. </div>
  243. <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
  244. </div>
  245. `;
  246. // Add to toast container or create one
  247. let toastContainer = document.getElementById('toast-container');
  248. if (!toastContainer) {
  249. toastContainer = document.createElement('div');
  250. toastContainer.id = 'toast-container';
  251. toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
  252. toastContainer.style.zIndex = '1055';
  253. document.body.appendChild(toastContainer);
  254. }
  255. toastContainer.appendChild(toast);
  256. // Show toast
  257. const bsToast = new bootstrap.Toast(toast);
  258. bsToast.show();
  259. // Remove toast element after it's hidden
  260. toast.addEventListener('hidden.bs.toast', function() {
  261. toast.remove();
  262. });
  263. }
  264. function showSuccessMessage(message) {
  265. // Similar to showErrorMessage but with success styling
  266. const toast = document.createElement('div');
  267. toast.className = 'toast align-items-center text-white bg-success border-0';
  268. toast.setAttribute('role', 'alert');
  269. toast.setAttribute('aria-live', 'assertive');
  270. toast.setAttribute('aria-atomic', 'true');
  271. toast.innerHTML = `
  272. <div class="d-flex">
  273. <div class="toast-body">
  274. <i class="fas fa-check-circle me-2"></i>
  275. ${message}
  276. </div>
  277. <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
  278. </div>
  279. `;
  280. let toastContainer = document.getElementById('toast-container');
  281. if (!toastContainer) {
  282. toastContainer = document.createElement('div');
  283. toastContainer.id = 'toast-container';
  284. toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
  285. toastContainer.style.zIndex = '1055';
  286. document.body.appendChild(toastContainer);
  287. }
  288. toastContainer.appendChild(toast);
  289. const bsToast = new bootstrap.Toast(toast);
  290. bsToast.show();
  291. toast.addEventListener('hidden.bs.toast', function() {
  292. toast.remove();
  293. });
  294. }
  295. // Format bytes for display
  296. function formatBytes(bytes, decimals = 2) {
  297. if (bytes === 0) return '0 Bytes';
  298. const k = 1024;
  299. const dm = decimals < 0 ? 0 : decimals;
  300. const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
  301. const i = Math.floor(Math.log(bytes) / Math.log(k));
  302. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  303. }
  304. // Format numbers with commas
  305. function formatNumber(num) {
  306. return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
  307. }
  308. // Helper function to format disk types for CSV export
  309. function formatDiskTypes(diskTypesText) {
  310. // Remove any HTML tags and clean up the text
  311. return diskTypesText.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim();
  312. }
  313. // Confirm action dialogs
  314. function confirmAction(message, callback) {
  315. if (confirm(message)) {
  316. callback();
  317. }
  318. }
  319. // Global error handler
  320. window.addEventListener('error', function(e) {
  321. console.error('Global error:', e.error);
  322. showErrorMessage('An unexpected error occurred.');
  323. });
  324. // Export functions for global use
  325. window.Dashboard = {
  326. showErrorMessage,
  327. showSuccessMessage,
  328. formatBytes,
  329. formatNumber,
  330. confirmAction
  331. };
  332. // Initialize event handlers
  333. function initializeEventHandlers() {
  334. // S3 Bucket Management
  335. const createBucketForm = document.getElementById('createBucketForm');
  336. if (createBucketForm) {
  337. createBucketForm.addEventListener('submit', handleCreateBucket);
  338. }
  339. // Delete bucket buttons
  340. document.addEventListener('click', function(e) {
  341. if (e.target.closest('.delete-bucket-btn')) {
  342. const button = e.target.closest('.delete-bucket-btn');
  343. const bucketName = button.getAttribute('data-bucket-name');
  344. confirmDeleteBucket(bucketName);
  345. }
  346. // Quota management buttons
  347. if (e.target.closest('.quota-btn')) {
  348. const button = e.target.closest('.quota-btn');
  349. const bucketName = button.getAttribute('data-bucket-name');
  350. const currentQuota = parseInt(button.getAttribute('data-current-quota')) || 0;
  351. const quotaEnabled = button.getAttribute('data-quota-enabled') === 'true';
  352. showQuotaModal(bucketName, currentQuota, quotaEnabled);
  353. }
  354. });
  355. // Quota form submission
  356. const quotaForm = document.getElementById('quotaForm');
  357. if (quotaForm) {
  358. quotaForm.addEventListener('submit', handleUpdateQuota);
  359. }
  360. // Enable quota checkbox for create bucket form
  361. const enableQuotaCheckbox = document.getElementById('enableQuota');
  362. if (enableQuotaCheckbox) {
  363. enableQuotaCheckbox.addEventListener('change', function() {
  364. const quotaSettings = document.getElementById('quotaSettings');
  365. if (this.checked) {
  366. quotaSettings.style.display = 'block';
  367. } else {
  368. quotaSettings.style.display = 'none';
  369. }
  370. });
  371. }
  372. // Enable quota checkbox for quota modal
  373. const quotaEnabledCheckbox = document.getElementById('quotaEnabled');
  374. if (quotaEnabledCheckbox) {
  375. quotaEnabledCheckbox.addEventListener('change', function() {
  376. const quotaSizeSettings = document.getElementById('quotaSizeSettings');
  377. if (this.checked) {
  378. quotaSizeSettings.style.display = 'block';
  379. } else {
  380. quotaSizeSettings.style.display = 'none';
  381. }
  382. });
  383. }
  384. }
  385. // Setup form validation
  386. function setupFormValidation() {
  387. // Bucket name validation
  388. const bucketNameInput = document.getElementById('bucketName');
  389. if (bucketNameInput) {
  390. bucketNameInput.addEventListener('input', validateBucketName);
  391. }
  392. }
  393. // S3 Bucket Management Functions
  394. // Handle create bucket form submission
  395. async function handleCreateBucket(event) {
  396. event.preventDefault();
  397. const form = event.target;
  398. const formData = new FormData(form);
  399. const bucketData = {
  400. name: formData.get('name'),
  401. region: formData.get('region') || 'us-east-1',
  402. quota_enabled: formData.get('quota_enabled') === 'on',
  403. quota_size: parseInt(formData.get('quota_size')) || 0,
  404. quota_unit: formData.get('quota_unit') || 'MB'
  405. };
  406. try {
  407. const response = await fetch('/api/s3/buckets', {
  408. method: 'POST',
  409. headers: {
  410. 'Content-Type': 'application/json',
  411. },
  412. body: JSON.stringify(bucketData)
  413. });
  414. const result = await response.json();
  415. if (response.ok) {
  416. // Success
  417. showAlert('success', `Bucket "${bucketData.name}" created successfully!`);
  418. // Close modal
  419. const modal = bootstrap.Modal.getInstance(document.getElementById('createBucketModal'));
  420. modal.hide();
  421. // Reset form
  422. form.reset();
  423. // Refresh the page after a short delay
  424. setTimeout(() => {
  425. location.reload();
  426. }, 1500);
  427. } else {
  428. // Error
  429. showAlert('danger', result.error || 'Failed to create bucket');
  430. }
  431. } catch (error) {
  432. console.error('Error creating bucket:', error);
  433. showAlert('danger', 'Network error occurred while creating bucket');
  434. }
  435. }
  436. // Validate bucket name input
  437. function validateBucketName(event) {
  438. const input = event.target;
  439. const value = input.value;
  440. const isValid = /^[a-z0-9.-]+$/.test(value) && value.length >= 3 && value.length <= 63;
  441. if (value.length > 0 && !isValid) {
  442. input.setCustomValidity('Bucket name must contain only lowercase letters, numbers, dots, and hyphens (3-63 characters)');
  443. } else {
  444. input.setCustomValidity('');
  445. }
  446. }
  447. // Confirm bucket deletion
  448. function confirmDeleteBucket(bucketName) {
  449. bucketToDelete = bucketName;
  450. document.getElementById('deleteBucketName').textContent = bucketName;
  451. const modal = new bootstrap.Modal(document.getElementById('deleteBucketModal'));
  452. modal.show();
  453. }
  454. // Delete bucket
  455. async function deleteBucket() {
  456. if (!bucketToDelete) {
  457. return;
  458. }
  459. try {
  460. const response = await fetch(`/api/s3/buckets/${bucketToDelete}`, {
  461. method: 'DELETE'
  462. });
  463. const result = await response.json();
  464. if (response.ok) {
  465. // Success
  466. showAlert('success', `Bucket "${bucketToDelete}" deleted successfully!`);
  467. // Close modal
  468. const modal = bootstrap.Modal.getInstance(document.getElementById('deleteBucketModal'));
  469. modal.hide();
  470. // Refresh the page after a short delay
  471. setTimeout(() => {
  472. location.reload();
  473. }, 1500);
  474. } else {
  475. // Error
  476. showAlert('danger', result.error || 'Failed to delete bucket');
  477. }
  478. } catch (error) {
  479. console.error('Error deleting bucket:', error);
  480. showAlert('danger', 'Network error occurred while deleting bucket');
  481. }
  482. bucketToDelete = '';
  483. }
  484. // Refresh buckets list
  485. function refreshBuckets() {
  486. location.reload();
  487. }
  488. // Export bucket list
  489. function exportBucketList() {
  490. // Get table data
  491. const table = document.getElementById('bucketsTable');
  492. if (!table) return;
  493. const rows = Array.from(table.querySelectorAll('tbody tr'));
  494. const data = rows.map(row => {
  495. const cells = row.querySelectorAll('td');
  496. if (cells.length < 5) return null; // Skip empty state row
  497. return {
  498. name: cells[0].textContent.trim(),
  499. created: cells[1].textContent.trim(),
  500. objects: cells[2].textContent.trim(),
  501. size: cells[3].textContent.trim(),
  502. quota: cells[4].textContent.trim()
  503. };
  504. }).filter(item => item !== null);
  505. // Convert to CSV
  506. const csv = [
  507. ['Name', 'Created', 'Objects', 'Size', 'Quota'].join(','),
  508. ...data.map(row => [
  509. row.name,
  510. row.created,
  511. row.objects,
  512. row.size,
  513. row.quota
  514. ].join(','))
  515. ].join('\n');
  516. // Download CSV
  517. const blob = new Blob([csv], { type: 'text/csv' });
  518. const url = window.URL.createObjectURL(blob);
  519. const a = document.createElement('a');
  520. a.href = url;
  521. a.download = `seaweedfs-buckets-${new Date().toISOString().split('T')[0]}.csv`;
  522. document.body.appendChild(a);
  523. a.click();
  524. document.body.removeChild(a);
  525. window.URL.revokeObjectURL(url);
  526. }
  527. // Show alert message
  528. function showAlert(type, message) {
  529. // Remove existing alerts
  530. const existingAlerts = document.querySelectorAll('.alert-floating');
  531. existingAlerts.forEach(alert => alert.remove());
  532. // Create new alert
  533. const alert = document.createElement('div');
  534. alert.className = `alert alert-${type} alert-dismissible fade show alert-floating`;
  535. alert.style.cssText = `
  536. position: fixed;
  537. top: 20px;
  538. right: 20px;
  539. z-index: 9999;
  540. min-width: 300px;
  541. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  542. `;
  543. alert.innerHTML = `
  544. ${message}
  545. <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
  546. `;
  547. document.body.appendChild(alert);
  548. // Auto-remove after 5 seconds
  549. setTimeout(() => {
  550. if (alert.parentNode) {
  551. alert.remove();
  552. }
  553. }, 5000);
  554. }
  555. // Format date for display
  556. function formatDate(date) {
  557. return new Date(date).toLocaleString();
  558. }
  559. // Copy text to clipboard
  560. function copyToClipboard(text) {
  561. navigator.clipboard.writeText(text).then(() => {
  562. showAlert('success', 'Copied to clipboard!');
  563. }).catch(err => {
  564. console.error('Failed to copy text: ', err);
  565. showAlert('danger', 'Failed to copy to clipboard');
  566. });
  567. }
  568. // Dashboard refresh functionality
  569. function refreshDashboard() {
  570. location.reload();
  571. }
  572. // Cluster management functions
  573. // Export volume servers data as CSV
  574. function exportVolumeServers() {
  575. const table = document.getElementById('hostsTable');
  576. if (!table) {
  577. showErrorMessage('No volume servers data to export');
  578. return;
  579. }
  580. let csv = 'Server ID,Address,Data Center,Rack,Volumes,Capacity,Usage\n';
  581. const rows = table.querySelectorAll('tbody tr');
  582. rows.forEach(row => {
  583. const cells = row.querySelectorAll('td');
  584. if (cells.length >= 7) {
  585. const rowData = [
  586. cells[0].textContent.trim(),
  587. cells[1].textContent.trim(),
  588. cells[2].textContent.trim(),
  589. cells[3].textContent.trim(),
  590. cells[4].textContent.trim(),
  591. cells[5].textContent.trim(),
  592. cells[6].textContent.trim()
  593. ];
  594. csv += rowData.join(',') + '\n';
  595. }
  596. });
  597. downloadCSV(csv, 'seaweedfs-volume-servers.csv');
  598. }
  599. // Export volumes data as CSV
  600. function exportVolumes() {
  601. const table = document.getElementById('volumesTable');
  602. if (!table) {
  603. showErrorMessage('No volumes data to export');
  604. return;
  605. }
  606. // Get headers from the table (dynamically handles conditional columns)
  607. const headerCells = table.querySelectorAll('thead th');
  608. const headers = [];
  609. headerCells.forEach((cell, index) => {
  610. // Skip the Actions column (last column)
  611. if (index < headerCells.length - 1) {
  612. headers.push(cell.textContent.trim());
  613. }
  614. });
  615. let csv = headers.join(',') + '\n';
  616. const rows = table.querySelectorAll('tbody tr');
  617. rows.forEach(row => {
  618. const cells = row.querySelectorAll('td');
  619. const rowData = [];
  620. // Export all cells except the Actions column (last column)
  621. for (let i = 0; i < cells.length - 1; i++) {
  622. rowData.push(`"${cells[i].textContent.trim().replace(/"/g, '""')}"`);
  623. }
  624. csv += rowData.join(',') + '\n';
  625. });
  626. downloadCSV(csv, 'seaweedfs-volumes.csv');
  627. }
  628. // Export collections data as CSV
  629. function exportCollections() {
  630. const table = document.getElementById('collectionsTable');
  631. if (!table) {
  632. showAlert('error', 'Collections table not found');
  633. return;
  634. }
  635. const headers = ['Collection Name', 'Volumes', 'Files', 'Size', 'Disk Types'];
  636. const rows = [];
  637. // Get table rows
  638. const tableRows = table.querySelectorAll('tbody tr');
  639. tableRows.forEach(row => {
  640. const cells = row.querySelectorAll('td');
  641. if (cells.length >= 5) {
  642. rows.push([
  643. cells[0].textContent.trim(),
  644. cells[1].textContent.trim(),
  645. cells[2].textContent.trim(),
  646. cells[3].textContent.trim(),
  647. formatDiskTypes(cells[4].textContent.trim())
  648. ]);
  649. }
  650. });
  651. // Generate CSV
  652. const csvContent = [headers, ...rows]
  653. .map(row => row.map(cell => `"${cell}"`).join(','))
  654. .join('\n');
  655. // Download
  656. const filename = `seaweedfs-collections-${new Date().toISOString().split('T')[0]}.csv`;
  657. downloadCSV(csvContent, filename);
  658. }
  659. // Export Masters to CSV
  660. function exportMasters() {
  661. const table = document.getElementById('mastersTable');
  662. if (!table) {
  663. showAlert('error', 'Masters table not found');
  664. return;
  665. }
  666. const headers = ['Address', 'Role', 'Suffrage'];
  667. const rows = [];
  668. // Get table rows
  669. const tableRows = table.querySelectorAll('tbody tr');
  670. tableRows.forEach(row => {
  671. const cells = row.querySelectorAll('td');
  672. if (cells.length >= 3) {
  673. rows.push([
  674. cells[0].textContent.trim(),
  675. cells[1].textContent.trim(),
  676. cells[2].textContent.trim()
  677. ]);
  678. }
  679. });
  680. // Generate CSV
  681. const csvContent = [headers, ...rows]
  682. .map(row => row.map(cell => `"${cell}"`).join(','))
  683. .join('\n');
  684. // Download
  685. const filename = `seaweedfs-masters-${new Date().toISOString().split('T')[0]}.csv`;
  686. downloadCSV(csvContent, filename);
  687. }
  688. // Export Filers to CSV
  689. function exportFilers() {
  690. const table = document.getElementById('filersTable');
  691. if (!table) {
  692. showAlert('error', 'Filers table not found');
  693. return;
  694. }
  695. const headers = ['Address', 'Version', 'Data Center', 'Rack', 'Created At'];
  696. const rows = [];
  697. // Get table rows
  698. const tableRows = table.querySelectorAll('tbody tr');
  699. tableRows.forEach(row => {
  700. const cells = row.querySelectorAll('td');
  701. if (cells.length >= 5) {
  702. rows.push([
  703. cells[0].textContent.trim(),
  704. cells[1].textContent.trim(),
  705. cells[2].textContent.trim(),
  706. cells[3].textContent.trim(),
  707. cells[4].textContent.trim()
  708. ]);
  709. }
  710. });
  711. // Generate CSV
  712. const csvContent = [headers, ...rows]
  713. .map(row => row.map(cell => `"${cell}"`).join(','))
  714. .join('\n');
  715. // Download
  716. const filename = `seaweedfs-filers-${new Date().toISOString().split('T')[0]}.csv`;
  717. downloadCSV(csvContent, filename);
  718. }
  719. // Export Users to CSV
  720. function exportUsers() {
  721. const table = document.getElementById('usersTable');
  722. if (!table) {
  723. showAlert('error', 'Users table not found');
  724. return;
  725. }
  726. const rows = table.querySelectorAll('tbody tr');
  727. if (rows.length === 0) {
  728. showErrorMessage('No users to export');
  729. return;
  730. }
  731. let csvContent = 'Username,Email,Access Key,Status,Created,Last Login\n';
  732. rows.forEach(row => {
  733. const cells = row.querySelectorAll('td');
  734. if (cells.length >= 6) {
  735. const username = cells[0].textContent.trim();
  736. const email = cells[1].textContent.trim();
  737. const accessKey = cells[2].textContent.trim();
  738. const status = cells[3].textContent.trim();
  739. const created = cells[4].textContent.trim();
  740. const lastLogin = cells[5].textContent.trim();
  741. csvContent += `"${username}","${email}","${accessKey}","${status}","${created}","${lastLogin}"\n`;
  742. }
  743. });
  744. downloadCSV(csvContent, 'seaweedfs-users.csv');
  745. }
  746. // Confirm delete collection
  747. function confirmDeleteCollection(button) {
  748. const collectionName = button.getAttribute('data-collection-name');
  749. document.getElementById('deleteCollectionName').textContent = collectionName;
  750. const modal = new bootstrap.Modal(document.getElementById('deleteCollectionModal'));
  751. modal.show();
  752. // Set up confirm button
  753. document.getElementById('confirmDeleteCollection').onclick = function() {
  754. deleteCollection(collectionName);
  755. };
  756. }
  757. // Delete collection
  758. async function deleteCollection(collectionName) {
  759. try {
  760. const response = await fetch(`/api/collections/${collectionName}`, {
  761. method: 'DELETE',
  762. headers: {
  763. 'Content-Type': 'application/json',
  764. }
  765. });
  766. if (response.ok) {
  767. showSuccessMessage(`Collection "${collectionName}" deleted successfully`);
  768. // Hide modal
  769. const modal = bootstrap.Modal.getInstance(document.getElementById('deleteCollectionModal'));
  770. modal.hide();
  771. // Refresh page
  772. setTimeout(() => {
  773. window.location.reload();
  774. }, 1000);
  775. } else {
  776. const error = await response.json();
  777. showErrorMessage(`Failed to delete collection: ${error.error || 'Unknown error'}`);
  778. }
  779. } catch (error) {
  780. console.error('Error deleting collection:', error);
  781. showErrorMessage('Failed to delete collection. Please try again.');
  782. }
  783. }
  784. // Download CSV utility function
  785. function downloadCSV(csvContent, filename) {
  786. const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  787. const link = document.createElement('a');
  788. if (link.download !== undefined) {
  789. const url = URL.createObjectURL(blob);
  790. link.setAttribute('href', url);
  791. link.setAttribute('download', filename);
  792. link.style.visibility = 'hidden';
  793. document.body.appendChild(link);
  794. link.click();
  795. document.body.removeChild(link);
  796. }
  797. }
  798. // File Browser Functions
  799. // Toggle select all checkboxes
  800. function toggleSelectAll() {
  801. const selectAll = document.getElementById('selectAll');
  802. const checkboxes = document.querySelectorAll('.file-checkbox');
  803. checkboxes.forEach(checkbox => {
  804. checkbox.checked = selectAll.checked;
  805. });
  806. updateDeleteSelectedButton();
  807. }
  808. // Update visibility of delete selected button based on selection
  809. function updateDeleteSelectedButton() {
  810. const checkboxes = document.querySelectorAll('.file-checkbox:checked');
  811. const deleteBtn = document.getElementById('deleteSelectedBtn');
  812. if (deleteBtn) {
  813. if (checkboxes.length > 0) {
  814. deleteBtn.style.display = 'inline-block';
  815. deleteBtn.innerHTML = `<i class="fas fa-trash me-1"></i>Delete Selected (${checkboxes.length})`;
  816. } else {
  817. deleteBtn.style.display = 'none';
  818. }
  819. }
  820. }
  821. // Update select all checkbox state based on individual selections
  822. function updateSelectAllCheckbox() {
  823. const selectAll = document.getElementById('selectAll');
  824. const allCheckboxes = document.querySelectorAll('.file-checkbox');
  825. const checkedCheckboxes = document.querySelectorAll('.file-checkbox:checked');
  826. if (selectAll && allCheckboxes.length > 0) {
  827. if (checkedCheckboxes.length === 0) {
  828. selectAll.checked = false;
  829. selectAll.indeterminate = false;
  830. } else if (checkedCheckboxes.length === allCheckboxes.length) {
  831. selectAll.checked = true;
  832. selectAll.indeterminate = false;
  833. } else {
  834. selectAll.checked = false;
  835. selectAll.indeterminate = true;
  836. }
  837. }
  838. }
  839. // Get selected file paths
  840. function getSelectedFilePaths() {
  841. const checkboxes = document.querySelectorAll('.file-checkbox:checked');
  842. return Array.from(checkboxes).map(cb => cb.value);
  843. }
  844. // Confirm delete selected files
  845. function confirmDeleteSelected() {
  846. const selectedPaths = getSelectedFilePaths();
  847. if (selectedPaths.length === 0) {
  848. showAlert('warning', 'No files selected');
  849. return;
  850. }
  851. const fileNames = selectedPaths.map(path => path.split('/').pop()).join(', ');
  852. const message = selectedPaths.length === 1
  853. ? `Are you sure you want to delete "${fileNames}"?`
  854. : `Are you sure you want to delete ${selectedPaths.length} selected items?\n\n${fileNames.substring(0, 200)}${fileNames.length > 200 ? '...' : ''}`;
  855. if (confirm(message)) {
  856. deleteSelectedFiles(selectedPaths);
  857. }
  858. }
  859. // Delete multiple selected files
  860. async function deleteSelectedFiles(filePaths) {
  861. if (!filePaths || filePaths.length === 0) {
  862. showAlert('warning', 'No files selected');
  863. return;
  864. }
  865. // Disable the delete button during operation
  866. const deleteBtn = document.getElementById('deleteSelectedBtn');
  867. const originalText = deleteBtn.innerHTML;
  868. deleteBtn.disabled = true;
  869. deleteBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Deleting...';
  870. try {
  871. const response = await fetch('/api/files/delete-multiple', {
  872. method: 'DELETE',
  873. headers: {
  874. 'Content-Type': 'application/json',
  875. },
  876. body: JSON.stringify({ paths: filePaths })
  877. });
  878. if (response.ok) {
  879. const result = await response.json();
  880. if (result.deleted > 0) {
  881. if (result.failed === 0) {
  882. showAlert('success', `Successfully deleted ${result.deleted} item(s)`);
  883. } else {
  884. showAlert('warning', `Deleted ${result.deleted} item(s), failed to delete ${result.failed} item(s)`);
  885. if (result.errors && result.errors.length > 0) {
  886. console.warn('Deletion errors:', result.errors);
  887. }
  888. }
  889. // Reload the page to update the file list
  890. setTimeout(() => {
  891. window.location.reload();
  892. }, 1000);
  893. } else {
  894. let errorMessage = result.message || 'Failed to delete all selected items';
  895. if (result.errors && result.errors.length > 0) {
  896. errorMessage += ': ' + result.errors.join(', ');
  897. }
  898. showAlert('error', errorMessage);
  899. }
  900. } else {
  901. const error = await response.json();
  902. showAlert('error', `Failed to delete files: ${error.error || 'Unknown error'}`);
  903. }
  904. } catch (error) {
  905. console.error('Delete error:', error);
  906. showAlert('error', 'Failed to delete files');
  907. } finally {
  908. // Re-enable the button
  909. deleteBtn.disabled = false;
  910. deleteBtn.innerHTML = originalText;
  911. }
  912. }
  913. // Create new folder
  914. function createFolder() {
  915. const modal = new bootstrap.Modal(document.getElementById('createFolderModal'));
  916. modal.show();
  917. }
  918. // Upload file
  919. function uploadFile() {
  920. const modal = new bootstrap.Modal(document.getElementById('uploadFileModal'));
  921. modal.show();
  922. }
  923. // Submit create folder form
  924. async function submitCreateFolder() {
  925. const folderName = document.getElementById('folderName').value.trim();
  926. const currentPath = document.getElementById('currentPath').value;
  927. if (!folderName) {
  928. showErrorMessage('Please enter a folder name');
  929. return;
  930. }
  931. // Validate folder name
  932. if (folderName.includes('/') || folderName.includes('\\')) {
  933. showErrorMessage('Folder names cannot contain / or \\ characters');
  934. return;
  935. }
  936. // Additional validation for reserved names
  937. const reservedNames = ['.', '..', 'CON', 'PRN', 'AUX', 'NUL'];
  938. if (reservedNames.includes(folderName.toUpperCase())) {
  939. showErrorMessage('This folder name is reserved and cannot be used');
  940. return;
  941. }
  942. // Disable the button to prevent double submission
  943. const submitButton = document.querySelector('#createFolderModal .btn-primary');
  944. const originalText = submitButton.innerHTML;
  945. submitButton.disabled = true;
  946. submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Creating...';
  947. try {
  948. const response = await fetch('/api/files/create-folder', {
  949. method: 'POST',
  950. headers: {
  951. 'Content-Type': 'application/json',
  952. },
  953. body: JSON.stringify({
  954. path: currentPath,
  955. folder_name: folderName
  956. })
  957. });
  958. if (response.ok) {
  959. showSuccessMessage(`Folder "${folderName}" created successfully`);
  960. // Hide modal
  961. const modal = bootstrap.Modal.getInstance(document.getElementById('createFolderModal'));
  962. modal.hide();
  963. // Clear form
  964. document.getElementById('folderName').value = '';
  965. // Refresh page
  966. setTimeout(() => {
  967. window.location.reload();
  968. }, 1000);
  969. } else {
  970. const error = await response.json();
  971. showErrorMessage(`Failed to create folder: ${error.error || 'Unknown error'}`);
  972. }
  973. } catch (error) {
  974. console.error('Create folder error:', error);
  975. showErrorMessage('Failed to create folder. Please try again.');
  976. } finally {
  977. // Re-enable the button
  978. submitButton.disabled = false;
  979. submitButton.innerHTML = originalText;
  980. }
  981. }
  982. // Submit upload file form
  983. async function submitUploadFile() {
  984. const fileInput = document.getElementById('fileInput');
  985. const currentPath = document.getElementById('uploadPath').value;
  986. if (!fileInput.files || fileInput.files.length === 0) {
  987. showErrorMessage('Please select at least one file to upload');
  988. return;
  989. }
  990. const files = Array.from(fileInput.files);
  991. const totalSize = files.reduce((sum, file) => sum + file.size, 0);
  992. // Validate total file size (limit to 500MB for admin interface)
  993. const maxSize = 500 * 1024 * 1024; // 500MB total
  994. if (totalSize > maxSize) {
  995. showErrorMessage('Total file size exceeds 500MB limit. Please select fewer or smaller files.');
  996. return;
  997. }
  998. // Individual file size validation removed - no limit per file
  999. const formData = new FormData();
  1000. files.forEach(file => {
  1001. formData.append('files', file);
  1002. });
  1003. formData.append('path', currentPath);
  1004. // Show progress bar and disable button
  1005. const progressContainer = document.getElementById('uploadProgress');
  1006. const progressBar = progressContainer.querySelector('.progress-bar');
  1007. const uploadStatus = document.getElementById('uploadStatus');
  1008. const submitButton = document.querySelector('#uploadFileModal .btn-primary');
  1009. const originalText = submitButton.innerHTML;
  1010. progressContainer.style.display = 'block';
  1011. progressBar.style.width = '0%';
  1012. progressBar.setAttribute('aria-valuenow', '0');
  1013. progressBar.textContent = '0%';
  1014. uploadStatus.textContent = `Uploading ${files.length} file(s)...`;
  1015. submitButton.disabled = true;
  1016. submitButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
  1017. try {
  1018. const xhr = new XMLHttpRequest();
  1019. // Handle progress
  1020. xhr.upload.addEventListener('progress', function(e) {
  1021. if (e.lengthComputable) {
  1022. const percentComplete = Math.round((e.loaded / e.total) * 100);
  1023. progressBar.style.width = percentComplete + '%';
  1024. progressBar.setAttribute('aria-valuenow', percentComplete);
  1025. progressBar.textContent = percentComplete + '%';
  1026. uploadStatus.textContent = `Uploading ${files.length} file(s)... ${percentComplete}%`;
  1027. }
  1028. });
  1029. // Handle completion
  1030. xhr.addEventListener('load', function() {
  1031. if (xhr.status === 200) {
  1032. try {
  1033. const response = JSON.parse(xhr.responseText);
  1034. if (response.uploaded > 0) {
  1035. if (response.failed === 0) {
  1036. showSuccessMessage(`Successfully uploaded ${response.uploaded} file(s)`);
  1037. } else {
  1038. showSuccessMessage(response.message);
  1039. // Show details of failed uploads
  1040. if (response.errors && response.errors.length > 0) {
  1041. console.warn('Upload errors:', response.errors);
  1042. }
  1043. }
  1044. // Hide modal and refresh page
  1045. const modal = bootstrap.Modal.getInstance(document.getElementById('uploadFileModal'));
  1046. modal.hide();
  1047. setTimeout(() => {
  1048. window.location.reload();
  1049. }, 1000);
  1050. } else {
  1051. let errorMessage = response.message || 'All file uploads failed';
  1052. if (response.errors && response.errors.length > 0) {
  1053. errorMessage += ': ' + response.errors.join(', ');
  1054. }
  1055. showErrorMessage(errorMessage);
  1056. }
  1057. } catch (e) {
  1058. showErrorMessage('Upload completed but response format was unexpected');
  1059. }
  1060. progressContainer.style.display = 'none';
  1061. } else {
  1062. let errorMessage = 'Unknown error';
  1063. try {
  1064. const error = JSON.parse(xhr.responseText);
  1065. errorMessage = error.error || error.message || errorMessage;
  1066. } catch (e) {
  1067. errorMessage = `Server returned status ${xhr.status}`;
  1068. }
  1069. showErrorMessage(`Failed to upload files: ${errorMessage}`);
  1070. progressContainer.style.display = 'none';
  1071. }
  1072. });
  1073. // Handle errors
  1074. xhr.addEventListener('error', function() {
  1075. showErrorMessage('Failed to upload files. Please check your connection and try again.');
  1076. progressContainer.style.display = 'none';
  1077. });
  1078. // Handle abort
  1079. xhr.addEventListener('abort', function() {
  1080. showErrorMessage('File upload was cancelled.');
  1081. progressContainer.style.display = 'none';
  1082. });
  1083. // Send request
  1084. xhr.open('POST', '/api/files/upload');
  1085. xhr.send(formData);
  1086. } catch (error) {
  1087. console.error('Upload error:', error);
  1088. showErrorMessage('Failed to upload files. Please try again.');
  1089. progressContainer.style.display = 'none';
  1090. } finally {
  1091. // Re-enable the button
  1092. submitButton.disabled = false;
  1093. submitButton.innerHTML = originalText;
  1094. }
  1095. }
  1096. // Export file list to CSV
  1097. function exportFileList() {
  1098. const table = document.getElementById('fileTable');
  1099. if (!table) {
  1100. showAlert('error', 'File table not found');
  1101. return;
  1102. }
  1103. const headers = ['Name', 'Size', 'Type', 'Modified', 'Permissions'];
  1104. const rows = [];
  1105. // Get table rows
  1106. const tableRows = table.querySelectorAll('tbody tr');
  1107. tableRows.forEach(row => {
  1108. const cells = row.querySelectorAll('td');
  1109. if (cells.length >= 6) {
  1110. rows.push([
  1111. cells[1].textContent.trim(), // Name
  1112. cells[2].textContent.trim(), // Size
  1113. cells[3].textContent.trim(), // Type
  1114. cells[4].textContent.trim(), // Modified
  1115. cells[5].textContent.trim() // Permissions
  1116. ]);
  1117. }
  1118. });
  1119. // Generate CSV
  1120. const csvContent = [headers, ...rows]
  1121. .map(row => row.map(cell => `"${cell}"`).join(','))
  1122. .join('\n');
  1123. // Download
  1124. const filename = `seaweedfs-files-${new Date().toISOString().split('T')[0]}.csv`;
  1125. downloadCSV(csvContent, filename);
  1126. }
  1127. // Download file
  1128. function downloadFile(filePath) {
  1129. // Create download link using admin API
  1130. const downloadUrl = `/api/files/download?path=${encodeURIComponent(filePath)}`;
  1131. window.open(downloadUrl, '_blank');
  1132. }
  1133. // View file
  1134. async function viewFile(filePath) {
  1135. try {
  1136. const response = await fetch(`/api/files/view?path=${encodeURIComponent(filePath)}`);
  1137. if (!response.ok) {
  1138. const error = await response.json();
  1139. showAlert('error', `Failed to view file: ${error.error || 'Unknown error'}`);
  1140. return;
  1141. }
  1142. const data = await response.json();
  1143. showFileViewer(data);
  1144. } catch (error) {
  1145. console.error('View file error:', error);
  1146. showAlert('error', 'Failed to view file');
  1147. }
  1148. }
  1149. // Show file properties
  1150. async function showProperties(filePath) {
  1151. try {
  1152. const response = await fetch(`/api/files/properties?path=${encodeURIComponent(filePath)}`);
  1153. if (!response.ok) {
  1154. const error = await response.json();
  1155. showAlert('error', `Failed to get file properties: ${error.error || 'Unknown error'}`);
  1156. return;
  1157. }
  1158. const properties = await response.json();
  1159. showPropertiesModal(properties);
  1160. } catch (error) {
  1161. console.error('Properties error:', error);
  1162. showAlert('error', 'Failed to get file properties');
  1163. }
  1164. }
  1165. // Confirm delete file/folder
  1166. function confirmDelete(filePath) {
  1167. if (confirm(`Are you sure you want to delete "${filePath}"?`)) {
  1168. deleteFile(filePath);
  1169. }
  1170. }
  1171. // Delete file/folder
  1172. async function deleteFile(filePath) {
  1173. try {
  1174. const response = await fetch('/api/files/delete', {
  1175. method: 'DELETE',
  1176. headers: {
  1177. 'Content-Type': 'application/json',
  1178. },
  1179. body: JSON.stringify({ path: filePath })
  1180. });
  1181. if (response.ok) {
  1182. showAlert('success', `Successfully deleted "${filePath}"`);
  1183. // Reload the page to update the file list
  1184. window.location.reload();
  1185. } else {
  1186. const error = await response.json();
  1187. showAlert('error', `Failed to delete file: ${error.error || 'Unknown error'}`);
  1188. }
  1189. } catch (error) {
  1190. console.error('Delete error:', error);
  1191. showAlert('error', 'Failed to delete file');
  1192. }
  1193. }
  1194. // Setup file manager specific event handlers
  1195. function setupFileManagerEventHandlers() {
  1196. // Handle Enter key in folder name input
  1197. const folderNameInput = document.getElementById('folderName');
  1198. if (folderNameInput) {
  1199. folderNameInput.addEventListener('keypress', function(e) {
  1200. if (e.key === 'Enter') {
  1201. e.preventDefault();
  1202. submitCreateFolder();
  1203. }
  1204. });
  1205. }
  1206. // Handle file selection change to show preview
  1207. const fileInput = document.getElementById('fileInput');
  1208. if (fileInput) {
  1209. fileInput.addEventListener('change', function(e) {
  1210. updateFileListPreview();
  1211. });
  1212. }
  1213. // Setup checkbox event listeners for file selection
  1214. const checkboxes = document.querySelectorAll('.file-checkbox');
  1215. checkboxes.forEach(checkbox => {
  1216. checkbox.addEventListener('change', function() {
  1217. updateDeleteSelectedButton();
  1218. updateSelectAllCheckbox();
  1219. });
  1220. });
  1221. // Setup drag and drop for file uploads
  1222. setupDragAndDrop();
  1223. // Clear form when modals are hidden
  1224. const createFolderModal = document.getElementById('createFolderModal');
  1225. if (createFolderModal) {
  1226. createFolderModal.addEventListener('hidden.bs.modal', function() {
  1227. document.getElementById('folderName').value = '';
  1228. });
  1229. }
  1230. const uploadFileModal = document.getElementById('uploadFileModal');
  1231. if (uploadFileModal) {
  1232. uploadFileModal.addEventListener('hidden.bs.modal', function() {
  1233. const fileInput = document.getElementById('fileInput');
  1234. const progressContainer = document.getElementById('uploadProgress');
  1235. const fileListPreview = document.getElementById('fileListPreview');
  1236. fileInput.value = '';
  1237. progressContainer.style.display = 'none';
  1238. fileListPreview.style.display = 'none';
  1239. });
  1240. }
  1241. }
  1242. // Setup drag and drop functionality
  1243. function setupDragAndDrop() {
  1244. const dropZone = document.querySelector('.card-body'); // Main file listing area
  1245. const uploadModal = document.getElementById('uploadFileModal');
  1246. if (!dropZone || !uploadModal) return;
  1247. // Prevent default drag behaviors
  1248. ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  1249. dropZone.addEventListener(eventName, preventDefaults, false);
  1250. document.body.addEventListener(eventName, preventDefaults, false);
  1251. });
  1252. // Highlight drop zone when item is dragged over it
  1253. ['dragenter', 'dragover'].forEach(eventName => {
  1254. dropZone.addEventListener(eventName, highlight, false);
  1255. });
  1256. ['dragleave', 'drop'].forEach(eventName => {
  1257. dropZone.addEventListener(eventName, unhighlight, false);
  1258. });
  1259. // Handle dropped files
  1260. dropZone.addEventListener('drop', handleDrop, false);
  1261. function preventDefaults(e) {
  1262. e.preventDefault();
  1263. e.stopPropagation();
  1264. }
  1265. function highlight(e) {
  1266. dropZone.classList.add('drag-over');
  1267. // Add some visual feedback
  1268. if (!dropZone.querySelector('.drag-overlay')) {
  1269. const overlay = document.createElement('div');
  1270. overlay.className = 'drag-overlay';
  1271. overlay.innerHTML = `
  1272. <div class="text-center p-5">
  1273. <i class="fas fa-cloud-upload-alt fa-3x text-primary mb-3"></i>
  1274. <h5>Drop files here to upload</h5>
  1275. <p class="text-muted">Release to upload files to this directory</p>
  1276. </div>
  1277. `;
  1278. overlay.style.cssText = `
  1279. position: absolute;
  1280. top: 0;
  1281. left: 0;
  1282. right: 0;
  1283. bottom: 0;
  1284. background: rgba(255, 255, 255, 0.9);
  1285. border: 2px dashed #007bff;
  1286. border-radius: 0.375rem;
  1287. z-index: 1000;
  1288. display: flex;
  1289. align-items: center;
  1290. justify-content: center;
  1291. `;
  1292. dropZone.style.position = 'relative';
  1293. dropZone.appendChild(overlay);
  1294. }
  1295. }
  1296. function unhighlight(e) {
  1297. dropZone.classList.remove('drag-over');
  1298. const overlay = dropZone.querySelector('.drag-overlay');
  1299. if (overlay) {
  1300. overlay.remove();
  1301. }
  1302. }
  1303. function handleDrop(e) {
  1304. const dt = e.dataTransfer;
  1305. const files = dt.files;
  1306. if (files.length > 0) {
  1307. // Open upload modal and set files
  1308. const fileInput = document.getElementById('fileInput');
  1309. if (fileInput) {
  1310. // Create a new FileList-like object
  1311. const fileArray = Array.from(files);
  1312. // Set files to input (this is a bit tricky with file inputs)
  1313. const dataTransfer = new DataTransfer();
  1314. fileArray.forEach(file => dataTransfer.items.add(file));
  1315. fileInput.files = dataTransfer.files;
  1316. // Update preview and show modal
  1317. updateFileListPreview();
  1318. const modal = new bootstrap.Modal(uploadModal);
  1319. modal.show();
  1320. }
  1321. }
  1322. }
  1323. }
  1324. // Update file list preview when files are selected
  1325. function updateFileListPreview() {
  1326. const fileInput = document.getElementById('fileInput');
  1327. const fileListPreview = document.getElementById('fileListPreview');
  1328. const selectedFilesList = document.getElementById('selectedFilesList');
  1329. if (!fileInput.files || fileInput.files.length === 0) {
  1330. fileListPreview.style.display = 'none';
  1331. return;
  1332. }
  1333. const files = Array.from(fileInput.files);
  1334. const totalSize = files.reduce((sum, file) => sum + file.size, 0);
  1335. let html = `<div class="d-flex justify-content-between align-items-center mb-2">
  1336. <strong>${files.length} file(s) selected</strong>
  1337. <small class="text-muted">Total: ${formatBytes(totalSize)}</small>
  1338. </div>`;
  1339. files.forEach((file, index) => {
  1340. const fileIcon = getFileIconByName(file.name);
  1341. html += `<div class="d-flex justify-content-between align-items-center py-1 ${index > 0 ? 'border-top' : ''}">
  1342. <div class="d-flex align-items-center">
  1343. <i class="fas ${fileIcon} me-2 text-muted"></i>
  1344. <span class="text-truncate" style="max-width: 200px;" title="${file.name}">${file.name}</span>
  1345. </div>
  1346. <small class="text-muted">${formatBytes(file.size)}</small>
  1347. </div>`;
  1348. });
  1349. selectedFilesList.innerHTML = html;
  1350. fileListPreview.style.display = 'block';
  1351. }
  1352. // Get file icon based on file name/extension
  1353. function getFileIconByName(fileName) {
  1354. const ext = fileName.split('.').pop().toLowerCase();
  1355. switch (ext) {
  1356. case 'jpg':
  1357. case 'jpeg':
  1358. case 'png':
  1359. case 'gif':
  1360. case 'bmp':
  1361. case 'svg':
  1362. return 'fa-image';
  1363. case 'mp4':
  1364. case 'avi':
  1365. case 'mov':
  1366. case 'wmv':
  1367. case 'flv':
  1368. return 'fa-video';
  1369. case 'mp3':
  1370. case 'wav':
  1371. case 'flac':
  1372. case 'aac':
  1373. return 'fa-music';
  1374. case 'pdf':
  1375. return 'fa-file-pdf';
  1376. case 'doc':
  1377. case 'docx':
  1378. return 'fa-file-word';
  1379. case 'xls':
  1380. case 'xlsx':
  1381. return 'fa-file-excel';
  1382. case 'ppt':
  1383. case 'pptx':
  1384. return 'fa-file-powerpoint';
  1385. case 'txt':
  1386. case 'md':
  1387. return 'fa-file-text';
  1388. case 'zip':
  1389. case 'rar':
  1390. case '7z':
  1391. case 'tar':
  1392. case 'gz':
  1393. return 'fa-file-archive';
  1394. case 'js':
  1395. case 'ts':
  1396. case 'html':
  1397. case 'css':
  1398. case 'json':
  1399. case 'xml':
  1400. return 'fa-file-code';
  1401. default:
  1402. return 'fa-file';
  1403. }
  1404. }
  1405. // Quota Management Functions
  1406. // Show quota management modal
  1407. function showQuotaModal(bucketName, currentQuotaMB, quotaEnabled) {
  1408. document.getElementById('quotaBucketName').value = bucketName;
  1409. document.getElementById('quotaEnabled').checked = quotaEnabled;
  1410. // Convert quota to appropriate unit and set values
  1411. const quotaBytes = currentQuotaMB * 1024 * 1024; // Convert MB to bytes
  1412. const { size, unit } = convertBytesToBestUnit(quotaBytes);
  1413. document.getElementById('quotaSizeMB').value = size;
  1414. document.getElementById('quotaUnitMB').value = unit;
  1415. // Show/hide quota size settings based on enabled state
  1416. const quotaSizeSettings = document.getElementById('quotaSizeSettings');
  1417. if (quotaEnabled) {
  1418. quotaSizeSettings.style.display = 'block';
  1419. } else {
  1420. quotaSizeSettings.style.display = 'none';
  1421. }
  1422. const modal = new bootstrap.Modal(document.getElementById('manageQuotaModal'));
  1423. modal.show();
  1424. }
  1425. // Convert bytes to the best unit (TB, GB, or MB)
  1426. function convertBytesToBestUnit(bytes) {
  1427. if (bytes === 0) {
  1428. return { size: 0, unit: 'MB' };
  1429. }
  1430. // Check if it's a clean TB value
  1431. if (bytes >= 1024 * 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024 * 1024) === 0) {
  1432. return { size: bytes / (1024 * 1024 * 1024 * 1024), unit: 'TB' };
  1433. }
  1434. // Check if it's a clean GB value
  1435. if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) {
  1436. return { size: bytes / (1024 * 1024 * 1024), unit: 'GB' };
  1437. }
  1438. // Default to MB
  1439. return { size: bytes / (1024 * 1024), unit: 'MB' };
  1440. }
  1441. // Handle quota update form submission
  1442. async function handleUpdateQuota(event) {
  1443. event.preventDefault();
  1444. const form = event.target;
  1445. const formData = new FormData(form);
  1446. const bucketName = document.getElementById('quotaBucketName').value;
  1447. const quotaData = {
  1448. quota_enabled: formData.get('quota_enabled') === 'on',
  1449. quota_size: parseInt(formData.get('quota_size')) || 0,
  1450. quota_unit: formData.get('quota_unit') || 'MB'
  1451. };
  1452. try {
  1453. const response = await fetch(`/api/s3/buckets/${bucketName}/quota`, {
  1454. method: 'PUT',
  1455. headers: {
  1456. 'Content-Type': 'application/json',
  1457. },
  1458. body: JSON.stringify(quotaData)
  1459. });
  1460. const result = await response.json();
  1461. if (response.ok) {
  1462. // Success
  1463. showAlert('success', `Quota for bucket "${bucketName}" updated successfully!`);
  1464. // Close modal
  1465. const modal = bootstrap.Modal.getInstance(document.getElementById('manageQuotaModal'));
  1466. modal.hide();
  1467. // Refresh the page after a short delay
  1468. setTimeout(() => {
  1469. location.reload();
  1470. }, 1500);
  1471. } else {
  1472. // Error
  1473. showAlert('danger', result.error || 'Failed to update bucket quota');
  1474. }
  1475. } catch (error) {
  1476. console.error('Error updating bucket quota:', error);
  1477. showAlert('danger', 'Network error occurred while updating bucket quota');
  1478. }
  1479. }
  1480. // Show file viewer modal
  1481. function showFileViewer(data) {
  1482. const file = data.file;
  1483. const content = data.content || '';
  1484. const viewable = data.viewable !== false;
  1485. // Create modal HTML
  1486. const modalHtml = `
  1487. <div class="modal fade" id="fileViewerModal" tabindex="-1" aria-labelledby="fileViewerModalLabel" aria-hidden="true">
  1488. <div class="modal-dialog modal-xl">
  1489. <div class="modal-content">
  1490. <div class="modal-header">
  1491. <h5 class="modal-title" id="fileViewerModalLabel">
  1492. <i class="fas fa-eye me-2"></i>File Viewer: ${file.name}
  1493. </h5>
  1494. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  1495. </div>
  1496. <div class="modal-body">
  1497. ${viewable ? createFileViewerContent(file, content) : createNonViewableContent(data.reason || 'File cannot be viewed')}
  1498. </div>
  1499. <div class="modal-footer">
  1500. <button type="button" class="btn btn-primary" onclick="downloadFile('${file.full_path}')">
  1501. <i class="fas fa-download me-1"></i>Download
  1502. </button>
  1503. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  1504. </div>
  1505. </div>
  1506. </div>
  1507. </div>
  1508. `;
  1509. // Remove existing modal if any
  1510. const existingModal = document.getElementById('fileViewerModal');
  1511. if (existingModal) {
  1512. existingModal.remove();
  1513. }
  1514. // Add modal to DOM
  1515. document.body.insertAdjacentHTML('beforeend', modalHtml);
  1516. // Show modal
  1517. const modal = new bootstrap.Modal(document.getElementById('fileViewerModal'));
  1518. modal.show();
  1519. // Clean up when modal is hidden
  1520. document.getElementById('fileViewerModal').addEventListener('hidden.bs.modal', function () {
  1521. this.remove();
  1522. });
  1523. }
  1524. // Create file viewer content based on file type
  1525. function createFileViewerContent(file, content) {
  1526. if (file.mime.startsWith('image/')) {
  1527. return `
  1528. <div class="text-center">
  1529. <img src="/api/files/download?path=${encodeURIComponent(file.full_path)}"
  1530. class="img-fluid" alt="${file.name}" style="max-height: 500px;">
  1531. </div>
  1532. `;
  1533. } else if (file.mime.startsWith('text/') || file.mime === 'application/json' || file.mime === 'application/javascript') {
  1534. const language = getLanguageFromMime(file.mime, file.name);
  1535. return `
  1536. <div class="mb-3">
  1537. <small class="text-muted">
  1538. <i class="fas fa-info-circle me-1"></i>
  1539. Size: ${formatBytes(file.size)} | Type: ${file.mime}
  1540. </small>
  1541. </div>
  1542. <pre><code class="language-${language}" style="max-height: 400px; overflow-y: auto;">${escapeHtml(content)}</code></pre>
  1543. `;
  1544. } else if (file.mime === 'application/pdf') {
  1545. return `
  1546. <div class="text-center">
  1547. <embed src="/api/files/download?path=${encodeURIComponent(file.full_path)}"
  1548. type="application/pdf" width="100%" height="500px">
  1549. </div>
  1550. `;
  1551. } else {
  1552. return createNonViewableContent('This file type cannot be previewed in the browser.');
  1553. }
  1554. }
  1555. // Create non-viewable content message
  1556. function createNonViewableContent(reason) {
  1557. return `
  1558. <div class="text-center py-5">
  1559. <i class="fas fa-file fa-3x text-muted mb-3"></i>
  1560. <h5 class="text-muted">Cannot preview file</h5>
  1561. <p class="text-muted">${reason}</p>
  1562. </div>
  1563. `;
  1564. }
  1565. // Get language for syntax highlighting
  1566. function getLanguageFromMime(mime, filename) {
  1567. // First check MIME type
  1568. switch (mime) {
  1569. case 'application/json': return 'json';
  1570. case 'application/javascript': return 'javascript';
  1571. case 'text/html': return 'html';
  1572. case 'text/css': return 'css';
  1573. case 'application/xml': return 'xml';
  1574. case 'text/typescript': return 'typescript';
  1575. case 'text/x-python': return 'python';
  1576. case 'text/x-go': return 'go';
  1577. case 'text/x-java': return 'java';
  1578. case 'text/x-c': return 'c';
  1579. case 'text/x-c++': return 'cpp';
  1580. case 'text/x-c-header': return 'c';
  1581. case 'text/x-shellscript': return 'bash';
  1582. case 'text/x-php': return 'php';
  1583. case 'text/x-ruby': return 'ruby';
  1584. case 'text/x-perl': return 'perl';
  1585. case 'text/x-rust': return 'rust';
  1586. case 'text/x-swift': return 'swift';
  1587. case 'text/x-kotlin': return 'kotlin';
  1588. case 'text/x-scala': return 'scala';
  1589. case 'text/x-dockerfile': return 'dockerfile';
  1590. case 'text/yaml': return 'yaml';
  1591. case 'text/csv': return 'csv';
  1592. case 'text/sql': return 'sql';
  1593. case 'text/markdown': return 'markdown';
  1594. }
  1595. // Fallback to file extension
  1596. const ext = filename.split('.').pop().toLowerCase();
  1597. switch (ext) {
  1598. case 'js': case 'mjs': return 'javascript';
  1599. case 'ts': return 'typescript';
  1600. case 'py': return 'python';
  1601. case 'go': return 'go';
  1602. case 'java': return 'java';
  1603. case 'cpp': case 'cc': case 'cxx': case 'c++': return 'cpp';
  1604. case 'c': return 'c';
  1605. case 'h': case 'hpp': return 'c';
  1606. case 'sh': case 'bash': case 'zsh': case 'fish': return 'bash';
  1607. case 'php': return 'php';
  1608. case 'rb': return 'ruby';
  1609. case 'pl': return 'perl';
  1610. case 'rs': return 'rust';
  1611. case 'swift': return 'swift';
  1612. case 'kt': return 'kotlin';
  1613. case 'scala': return 'scala';
  1614. case 'yml': case 'yaml': return 'yaml';
  1615. case 'md': case 'markdown': return 'markdown';
  1616. case 'sql': return 'sql';
  1617. case 'csv': return 'csv';
  1618. case 'dockerfile': return 'dockerfile';
  1619. case 'gitignore': case 'gitattributes': return 'text';
  1620. case 'env': return 'bash';
  1621. case 'cfg': case 'conf': case 'ini': case 'properties': return 'ini';
  1622. default: return 'text';
  1623. }
  1624. }
  1625. // Show properties modal
  1626. function showPropertiesModal(properties) {
  1627. // Create modal HTML
  1628. const modalHtml = `
  1629. <div class="modal fade" id="propertiesModal" tabindex="-1" aria-labelledby="propertiesModalLabel" aria-hidden="true">
  1630. <div class="modal-dialog modal-lg">
  1631. <div class="modal-content">
  1632. <div class="modal-header">
  1633. <h5 class="modal-title" id="propertiesModalLabel">
  1634. <i class="fas fa-info me-2"></i>Properties: ${properties.name}
  1635. </h5>
  1636. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  1637. </div>
  1638. <div class="modal-body">
  1639. ${createPropertiesContent(properties)}
  1640. </div>
  1641. <div class="modal-footer">
  1642. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  1643. </div>
  1644. </div>
  1645. </div>
  1646. </div>
  1647. `;
  1648. // Remove existing modal if any
  1649. const existingModal = document.getElementById('propertiesModal');
  1650. if (existingModal) {
  1651. existingModal.remove();
  1652. }
  1653. // Add modal to DOM
  1654. document.body.insertAdjacentHTML('beforeend', modalHtml);
  1655. // Show modal
  1656. const modal = new bootstrap.Modal(document.getElementById('propertiesModal'));
  1657. modal.show();
  1658. // Clean up when modal is hidden
  1659. document.getElementById('propertiesModal').addEventListener('hidden.bs.modal', function () {
  1660. this.remove();
  1661. });
  1662. }
  1663. // Create properties content
  1664. function createPropertiesContent(properties) {
  1665. let html = `
  1666. <div class="row">
  1667. <div class="col-md-6">
  1668. <h6 class="text-primary"><i class="fas fa-file me-1"></i>Basic Information</h6>
  1669. <table class="table table-sm">
  1670. <tr><td><strong>Name:</strong></td><td>${properties.name}</td></tr>
  1671. <tr><td><strong>Full Path:</strong></td><td><code>${properties.full_path}</code></td></tr>
  1672. <tr><td><strong>Type:</strong></td><td>${properties.is_directory ? 'Directory' : 'File'}</td></tr>
  1673. `;
  1674. if (!properties.is_directory) {
  1675. html += `
  1676. <tr><td><strong>Size:</strong></td><td>${properties.size_formatted || formatBytes(properties.size || 0)}</td></tr>
  1677. <tr><td><strong>MIME Type:</strong></td><td>${properties.mime_type || 'Unknown'}</td></tr>
  1678. `;
  1679. }
  1680. html += `
  1681. </table>
  1682. </div>
  1683. <div class="col-md-6">
  1684. <h6 class="text-primary"><i class="fas fa-clock me-1"></i>Timestamps</h6>
  1685. <table class="table table-sm">
  1686. `;
  1687. if (properties.modified_time) {
  1688. html += `<tr><td><strong>Modified:</strong></td><td>${properties.modified_time}</td></tr>`;
  1689. }
  1690. if (properties.created_time) {
  1691. html += `<tr><td><strong>Created:</strong></td><td>${properties.created_time}</td></tr>`;
  1692. }
  1693. html += `
  1694. </table>
  1695. <h6 class="text-primary"><i class="fas fa-shield-alt me-1"></i>Permissions</h6>
  1696. <table class="table table-sm">
  1697. <tr><td><strong>Mode:</strong></td><td><code>${properties.file_mode_formatted || properties.file_mode}</code></td></tr>
  1698. <tr><td><strong>UID:</strong></td><td>${properties.uid || 'N/A'}</td></tr>
  1699. <tr><td><strong>GID:</strong></td><td>${properties.gid || 'N/A'}</td></tr>
  1700. </table>
  1701. </div>
  1702. </div>
  1703. `;
  1704. // Add TTL information if available
  1705. if (properties.ttl_seconds && properties.ttl_seconds > 0) {
  1706. html += `
  1707. <div class="row mt-3">
  1708. <div class="col-12">
  1709. <h6 class="text-primary"><i class="fas fa-hourglass-half me-1"></i>TTL (Time To Live)</h6>
  1710. <table class="table table-sm">
  1711. <tr><td><strong>TTL:</strong></td><td>${properties.ttl_formatted || properties.ttl_seconds + ' seconds'}</td></tr>
  1712. </table>
  1713. </div>
  1714. </div>
  1715. `;
  1716. }
  1717. // Add chunk information if available
  1718. if (properties.chunks && properties.chunks.length > 0) {
  1719. html += `
  1720. <div class="row mt-3">
  1721. <div class="col-12">
  1722. <h6 class="text-primary"><i class="fas fa-puzzle-piece me-1"></i>Chunks (${properties.chunk_count})</h6>
  1723. <div class="table-responsive" style="max-height: 200px; overflow-y: auto;">
  1724. <table class="table table-sm">
  1725. <thead>
  1726. <tr>
  1727. <th>File ID</th>
  1728. <th>Offset</th>
  1729. <th>Size</th>
  1730. <th>ETag</th>
  1731. </tr>
  1732. </thead>
  1733. <tbody>
  1734. `;
  1735. properties.chunks.forEach(chunk => {
  1736. html += `
  1737. <tr>
  1738. <td><code class="small">${chunk.file_id}</code></td>
  1739. <td>${formatBytes(chunk.offset)}</td>
  1740. <td>${formatBytes(chunk.size)}</td>
  1741. <td><code class="small">${chunk.e_tag || 'N/A'}</code></td>
  1742. </tr>
  1743. `;
  1744. });
  1745. html += `
  1746. </tbody>
  1747. </table>
  1748. </div>
  1749. </div>
  1750. </div>
  1751. `;
  1752. }
  1753. // Add extended attributes if available
  1754. if (properties.extended && Object.keys(properties.extended).length > 0) {
  1755. html += `
  1756. <div class="row mt-3">
  1757. <div class="col-12">
  1758. <h6 class="text-primary"><i class="fas fa-tags me-1"></i>Extended Attributes</h6>
  1759. <table class="table table-sm">
  1760. `;
  1761. Object.entries(properties.extended).forEach(([key, value]) => {
  1762. html += `<tr><td><strong>${key}:</strong></td><td>${value}</td></tr>`;
  1763. });
  1764. html += `
  1765. </table>
  1766. </div>
  1767. </div>
  1768. `;
  1769. }
  1770. return html;
  1771. }
  1772. // Utility function to escape HTML
  1773. function escapeHtml(text) {
  1774. var map = {
  1775. '&': '&amp;',
  1776. '<': '&lt;',
  1777. '>': '&gt;',
  1778. '"': '&quot;',
  1779. "'": '&#039;'
  1780. };
  1781. return text.replace(/[&<>"']/g, function(m) { return map[m]; });
  1782. }
  1783. // ============================================================================
  1784. // USER MANAGEMENT FUNCTIONS
  1785. // ============================================================================
  1786. // Global variables for user management
  1787. let currentEditingUser = '';
  1788. let currentAccessKeysUser = '';
  1789. // User Management Functions
  1790. async function handleCreateUser() {
  1791. const form = document.getElementById('createUserForm');
  1792. const formData = new FormData(form);
  1793. // Get selected actions
  1794. const actionsSelect = document.getElementById('actions');
  1795. const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
  1796. const userData = {
  1797. username: formData.get('username'),
  1798. email: formData.get('email'),
  1799. actions: selectedActions,
  1800. generate_key: formData.get('generateKey') === 'on'
  1801. };
  1802. try {
  1803. const response = await fetch('/api/users', {
  1804. method: 'POST',
  1805. headers: {
  1806. 'Content-Type': 'application/json',
  1807. },
  1808. body: JSON.stringify(userData)
  1809. });
  1810. if (response.ok) {
  1811. const result = await response.json();
  1812. showSuccessMessage('User created successfully');
  1813. // Show the created access key if generated
  1814. if (result.user && result.user.access_key) {
  1815. showNewAccessKeyModal(result.user);
  1816. }
  1817. // Close modal and refresh page
  1818. const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
  1819. modal.hide();
  1820. form.reset();
  1821. setTimeout(() => window.location.reload(), 1000);
  1822. } else {
  1823. const error = await response.json();
  1824. showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
  1825. }
  1826. } catch (error) {
  1827. console.error('Error creating user:', error);
  1828. showErrorMessage('Failed to create user: ' + error.message);
  1829. }
  1830. }
  1831. async function editUser(username) {
  1832. currentEditingUser = username;
  1833. try {
  1834. const response = await fetch(`/api/users/${username}`);
  1835. if (response.ok) {
  1836. const user = await response.json();
  1837. // Populate edit form
  1838. document.getElementById('editUsername').value = username;
  1839. document.getElementById('editEmail').value = user.email || '';
  1840. // Set selected actions
  1841. const actionsSelect = document.getElementById('editActions');
  1842. Array.from(actionsSelect.options).forEach(option => {
  1843. option.selected = user.actions && user.actions.includes(option.value);
  1844. });
  1845. // Show modal
  1846. const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
  1847. modal.show();
  1848. } else {
  1849. showErrorMessage('Failed to load user details');
  1850. }
  1851. } catch (error) {
  1852. console.error('Error loading user:', error);
  1853. showErrorMessage('Failed to load user details');
  1854. }
  1855. }
  1856. async function handleUpdateUser() {
  1857. const form = document.getElementById('editUserForm');
  1858. const formData = new FormData(form);
  1859. // Get selected actions
  1860. const actionsSelect = document.getElementById('editActions');
  1861. const selectedActions = Array.from(actionsSelect.selectedOptions).map(option => option.value);
  1862. const userData = {
  1863. email: formData.get('email'),
  1864. actions: selectedActions
  1865. };
  1866. try {
  1867. const response = await fetch(`/api/users/${currentEditingUser}`, {
  1868. method: 'PUT',
  1869. headers: {
  1870. 'Content-Type': 'application/json',
  1871. },
  1872. body: JSON.stringify(userData)
  1873. });
  1874. if (response.ok) {
  1875. showSuccessMessage('User updated successfully');
  1876. // Close modal and refresh page
  1877. const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
  1878. modal.hide();
  1879. setTimeout(() => window.location.reload(), 1000);
  1880. } else {
  1881. const error = await response.json();
  1882. showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
  1883. }
  1884. } catch (error) {
  1885. console.error('Error updating user:', error);
  1886. showErrorMessage('Failed to update user: ' + error.message);
  1887. }
  1888. }
  1889. function confirmDeleteUser(username) {
  1890. confirmAction(
  1891. `Are you sure you want to delete user "${username}"? This action cannot be undone.`,
  1892. () => deleteUserConfirmed(username)
  1893. );
  1894. }
  1895. function deleteUser(username) {
  1896. confirmDeleteUser(username);
  1897. }
  1898. async function deleteUserConfirmed(username) {
  1899. try {
  1900. const response = await fetch(`/api/users/${username}`, {
  1901. method: 'DELETE'
  1902. });
  1903. if (response.ok) {
  1904. showSuccessMessage('User deleted successfully');
  1905. setTimeout(() => window.location.reload(), 1000);
  1906. } else {
  1907. const error = await response.json();
  1908. showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
  1909. }
  1910. } catch (error) {
  1911. console.error('Error deleting user:', error);
  1912. showErrorMessage('Failed to delete user: ' + error.message);
  1913. }
  1914. }
  1915. async function showUserDetails(username) {
  1916. try {
  1917. const response = await fetch(`/api/users/${username}`);
  1918. if (response.ok) {
  1919. const user = await response.json();
  1920. const content = createUserDetailsContent(user);
  1921. document.getElementById('userDetailsContent').innerHTML = content;
  1922. const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
  1923. modal.show();
  1924. } else {
  1925. showErrorMessage('Failed to load user details');
  1926. }
  1927. } catch (error) {
  1928. console.error('Error loading user details:', error);
  1929. showErrorMessage('Failed to load user details');
  1930. }
  1931. }
  1932. function createUserDetailsContent(user) {
  1933. return `
  1934. <div class="row">
  1935. <div class="col-md-6">
  1936. <h6 class="text-muted">Basic Information</h6>
  1937. <table class="table table-sm">
  1938. <tr>
  1939. <td><strong>Username:</strong></td>
  1940. <td>${escapeHtml(user.username)}</td>
  1941. </tr>
  1942. <tr>
  1943. <td><strong>Email:</strong></td>
  1944. <td>${escapeHtml(user.email || 'Not set')}</td>
  1945. </tr>
  1946. </table>
  1947. </div>
  1948. <div class="col-md-6">
  1949. <h6 class="text-muted">Permissions</h6>
  1950. <div class="mb-3">
  1951. ${user.actions && user.actions.length > 0 ?
  1952. user.actions.map(action => `<span class="badge bg-info me-1">${action}</span>`).join('') :
  1953. '<span class="text-muted">No permissions assigned</span>'
  1954. }
  1955. </div>
  1956. <h6 class="text-muted">Access Keys</h6>
  1957. ${user.access_keys && user.access_keys.length > 0 ?
  1958. createAccessKeysTable(user.access_keys) :
  1959. '<p class="text-muted">No access keys</p>'
  1960. }
  1961. </div>
  1962. </div>
  1963. `;
  1964. }
  1965. function createAccessKeysTable(accessKeys) {
  1966. return `
  1967. <div class="table-responsive">
  1968. <table class="table table-sm">
  1969. <thead>
  1970. <tr>
  1971. <th>Access Key</th>
  1972. <th>Created</th>
  1973. </tr>
  1974. </thead>
  1975. <tbody>
  1976. ${accessKeys.map(key => `
  1977. <tr>
  1978. <td><code>${key.access_key}</code></td>
  1979. <td>${new Date(key.created_at).toLocaleDateString()}</td>
  1980. </tr>
  1981. `).join('')}
  1982. </tbody>
  1983. </table>
  1984. </div>
  1985. `;
  1986. }
  1987. async function manageAccessKeys(username) {
  1988. currentAccessKeysUser = username;
  1989. document.getElementById('accessKeysUsername').textContent = username;
  1990. await loadAccessKeys(username);
  1991. const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
  1992. modal.show();
  1993. }
  1994. async function loadAccessKeys(username) {
  1995. try {
  1996. const response = await fetch(`/api/users/${username}`);
  1997. if (response.ok) {
  1998. const user = await response.json();
  1999. const content = createAccessKeysManagementContent(user.access_keys || []);
  2000. document.getElementById('accessKeysContent').innerHTML = content;
  2001. } else {
  2002. document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Failed to load access keys</p>';
  2003. }
  2004. } catch (error) {
  2005. console.error('Error loading access keys:', error);
  2006. document.getElementById('accessKeysContent').innerHTML = '<p class="text-muted">Error loading access keys</p>';
  2007. }
  2008. }
  2009. function createAccessKeysManagementContent(accessKeys) {
  2010. if (accessKeys.length === 0) {
  2011. return '<p class="text-muted">No access keys found. Create one to get started.</p>';
  2012. }
  2013. return `
  2014. <div class="table-responsive">
  2015. <table class="table table-hover">
  2016. <thead>
  2017. <tr>
  2018. <th>Access Key</th>
  2019. <th>Secret Key</th>
  2020. <th>Created</th>
  2021. <th>Actions</th>
  2022. </tr>
  2023. </thead>
  2024. <tbody>
  2025. ${accessKeys.map(key => `
  2026. <tr>
  2027. <td>
  2028. <code>${key.access_key}</code>
  2029. <button class="btn btn-sm btn-outline-secondary ms-2" onclick="copyToClipboard('${key.access_key}')">
  2030. <i class="fas fa-copy"></i>
  2031. </button>
  2032. </td>
  2033. <td>
  2034. <code class="text-muted">••••••••••••••••</code>
  2035. <button class="btn btn-sm btn-outline-secondary ms-2" onclick="showSecretKey('${key.access_key}', '${key.secret_key}')">
  2036. <i class="fas fa-eye"></i>
  2037. </button>
  2038. </td>
  2039. <td>${new Date(key.created_at).toLocaleDateString()}</td>
  2040. <td>
  2041. <button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteAccessKey('${key.access_key}')">
  2042. <i class="fas fa-trash"></i>
  2043. </button>
  2044. </td>
  2045. </tr>
  2046. `).join('')}
  2047. </tbody>
  2048. </table>
  2049. </div>
  2050. `;
  2051. }
  2052. async function createAccessKey() {
  2053. if (!currentAccessKeysUser) {
  2054. showErrorMessage('No user selected');
  2055. return;
  2056. }
  2057. try {
  2058. const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys`, {
  2059. method: 'POST',
  2060. headers: {
  2061. 'Content-Type': 'application/json',
  2062. }
  2063. });
  2064. if (response.ok) {
  2065. const result = await response.json();
  2066. showSuccessMessage('Access key created successfully');
  2067. // Show the new access key
  2068. showNewAccessKeyModal(result.access_key);
  2069. // Reload access keys
  2070. await loadAccessKeys(currentAccessKeysUser);
  2071. } else {
  2072. const error = await response.json();
  2073. showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
  2074. }
  2075. } catch (error) {
  2076. console.error('Error creating access key:', error);
  2077. showErrorMessage('Failed to create access key: ' + error.message);
  2078. }
  2079. }
  2080. function confirmDeleteAccessKey(accessKeyId) {
  2081. confirmAction(
  2082. `Are you sure you want to delete access key "${accessKeyId}"? This action cannot be undone.`,
  2083. () => deleteAccessKeyConfirmed(accessKeyId)
  2084. );
  2085. }
  2086. async function deleteAccessKeyConfirmed(accessKeyId) {
  2087. try {
  2088. const response = await fetch(`/api/users/${currentAccessKeysUser}/access-keys/${accessKeyId}`, {
  2089. method: 'DELETE'
  2090. });
  2091. if (response.ok) {
  2092. showSuccessMessage('Access key deleted successfully');
  2093. // Reload access keys
  2094. await loadAccessKeys(currentAccessKeysUser);
  2095. } else {
  2096. const error = await response.json();
  2097. showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
  2098. }
  2099. } catch (error) {
  2100. console.error('Error deleting access key:', error);
  2101. showErrorMessage('Failed to delete access key: ' + error.message);
  2102. }
  2103. }
  2104. function showSecretKey(accessKey, secretKey) {
  2105. const content = `
  2106. <div class="alert alert-info">
  2107. <i class="fas fa-info-circle me-2"></i>
  2108. <strong>Access Key Details:</strong> These credentials provide access to your object storage. Keep them secure and don't share them.
  2109. </div>
  2110. <div class="mb-3">
  2111. <label class="form-label"><strong>Access Key:</strong></label>
  2112. <div class="input-group">
  2113. <input type="text" class="form-control" value="${accessKey}" readonly>
  2114. <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKey}')">
  2115. <i class="fas fa-copy"></i>
  2116. </button>
  2117. </div>
  2118. </div>
  2119. <div class="mb-3">
  2120. <label class="form-label"><strong>Secret Key:</strong></label>
  2121. <div class="input-group">
  2122. <input type="text" class="form-control" value="${secretKey}" readonly>
  2123. <button class="btn btn-outline-secondary" onclick="copyToClipboard('${secretKey}')">
  2124. <i class="fas fa-copy"></i>
  2125. </button>
  2126. </div>
  2127. </div>
  2128. `;
  2129. showModal('Access Key Details', content);
  2130. }
  2131. function showNewAccessKeyModal(accessKeyData) {
  2132. const content = `
  2133. <div class="alert alert-success">
  2134. <i class="fas fa-check-circle me-2"></i>
  2135. <strong>Success!</strong> Your new access key has been created.
  2136. </div>
  2137. <div class="alert alert-info">
  2138. <i class="fas fa-info-circle me-2"></i>
  2139. <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.
  2140. </div>
  2141. <div class="mb-3">
  2142. <label class="form-label"><strong>Access Key:</strong></label>
  2143. <div class="input-group">
  2144. <input type="text" class="form-control" value="${accessKeyData.access_key}" readonly>
  2145. <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.access_key}')">
  2146. <i class="fas fa-copy"></i>
  2147. </button>
  2148. </div>
  2149. </div>
  2150. <div class="mb-3">
  2151. <label class="form-label"><strong>Secret Key:</strong></label>
  2152. <div class="input-group">
  2153. <input type="text" class="form-control" value="${accessKeyData.secret_key}" readonly>
  2154. <button class="btn btn-outline-secondary" onclick="copyToClipboard('${accessKeyData.secret_key}')">
  2155. <i class="fas fa-copy"></i>
  2156. </button>
  2157. </div>
  2158. </div>
  2159. `;
  2160. showModal('New Access Key Created', content);
  2161. }
  2162. function showModal(title, content) {
  2163. // Create a dynamic modal
  2164. const modalId = 'dynamicModal_' + Date.now();
  2165. const modalHtml = `
  2166. <div class="modal fade" id="${modalId}" tabindex="-1" role="dialog">
  2167. <div class="modal-dialog" role="document">
  2168. <div class="modal-content">
  2169. <div class="modal-header">
  2170. <h5 class="modal-title">${title}</h5>
  2171. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  2172. </div>
  2173. <div class="modal-body">
  2174. ${content}
  2175. </div>
  2176. <div class="modal-footer">
  2177. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  2178. </div>
  2179. </div>
  2180. </div>
  2181. </div>
  2182. `;
  2183. // Add modal to body
  2184. document.body.insertAdjacentHTML('beforeend', modalHtml);
  2185. // Show modal
  2186. const modal = new bootstrap.Modal(document.getElementById(modalId));
  2187. modal.show();
  2188. // Remove modal from DOM when hidden
  2189. document.getElementById(modalId).addEventListener('hidden.bs.modal', function() {
  2190. this.remove();
  2191. });
  2192. }