213 lines
6.9 KiB
Svelte
213 lines
6.9 KiB
Svelte
<!-- [DEF:StoragePage:Component] -->
|
|
<!--
|
|
@TIER: STANDARD
|
|
@SEMANTICS: storage, files, management
|
|
@PURPOSE: Main page for file storage management.
|
|
@LAYER: UI
|
|
@RELATION: DEPENDS_ON -> storageService
|
|
@RELATION: CONTAINS -> FileList
|
|
@RELATION: CONTAINS -> FileUpload
|
|
|
|
@INVARIANT: Always displays tabs for Backups and Repositories.
|
|
-->
|
|
|
|
<script lang="ts">
|
|
// [SECTION: IMPORTS]
|
|
import { onMount } from 'svelte';
|
|
import { page } from '$app/stores';
|
|
import { listFiles, deleteFile } from '../../../services/storageService';
|
|
import { addToast } from '../../../lib/toasts';
|
|
import { t } from '../../../lib/i18n';
|
|
import FileList from '../../../components/storage/FileList.svelte';
|
|
import FileUpload from '../../../components/storage/FileUpload.svelte';
|
|
// [/SECTION: IMPORTS]
|
|
|
|
// [DEF:loadFiles:Function]
|
|
/**
|
|
* @purpose Fetches the list of files from the server.
|
|
* @post Updates the `files` array with the latest data.
|
|
*/
|
|
let files = [];
|
|
let isLoading = false;
|
|
let activeTab = 'backups';
|
|
let currentPath = 'backups'; // Relative to storage root
|
|
|
|
async function loadFiles() {
|
|
isLoading = true;
|
|
try {
|
|
const category = activeTab;
|
|
|
|
// If we have a currentPath, we use it.
|
|
// But if user switched tabs, we should reset currentPath to category root
|
|
let effectivePath = currentPath;
|
|
if (category && !currentPath.startsWith(category)) {
|
|
effectivePath = category;
|
|
currentPath = category;
|
|
}
|
|
|
|
// API expects path relative to category root if category is provided
|
|
const subpath = (category && effectivePath.startsWith(category))
|
|
? effectivePath.substring(category.length).replace(/^\/+/, '')
|
|
: effectivePath;
|
|
|
|
files = await listFiles(category, subpath);
|
|
} catch (error) {
|
|
addToast($t.storage.messages.load_failed.replace('{error}', error.message), 'error');
|
|
} finally {
|
|
isLoading = false;
|
|
}
|
|
}
|
|
// [/DEF:loadFiles:Function]
|
|
|
|
// [DEF:handleDelete:Function]
|
|
/**
|
|
* @purpose Handles the file deletion process.
|
|
* @param {CustomEvent} event - The delete event containing category and path.
|
|
*/
|
|
async function handleDelete(event) {
|
|
const { category, path, name } = event.detail;
|
|
if (!confirm($t.storage.messages.delete_confirm.replace('{name}', name))) return;
|
|
|
|
try {
|
|
await deleteFile(category, path);
|
|
addToast($t.storage.messages.delete_success.replace('{name}', name), 'success');
|
|
await loadFiles();
|
|
} catch (error) {
|
|
addToast($t.storage.messages.delete_failed.replace('{error}', error.message), 'error');
|
|
}
|
|
}
|
|
// [/DEF:handleDelete:Function]
|
|
|
|
// [DEF:handleNavigate:Function]
|
|
/**
|
|
* @purpose Updates the current path and reloads files when navigating into a directory.
|
|
* @param {CustomEvent} event - The navigation event containing the new path.
|
|
*/
|
|
function handleNavigate(event) {
|
|
currentPath = event.detail;
|
|
loadFiles();
|
|
}
|
|
// [/DEF:handleNavigate:Function]
|
|
|
|
// [DEF:navigateUp:Function]
|
|
/**
|
|
* @purpose Navigates one level up in the directory structure.
|
|
* @pre currentPath is set and deeper than activeTab root.
|
|
* @post currentPath is moved up one directory level.
|
|
*/
|
|
function navigateUp() {
|
|
if (!currentPath || currentPath === activeTab) return;
|
|
const parts = currentPath.split('/');
|
|
parts.pop();
|
|
currentPath = parts.join('/') || '';
|
|
loadFiles();
|
|
}
|
|
// [/DEF:navigateUp:Function]
|
|
|
|
onMount(() => {
|
|
const pathParam = $page.url.searchParams.get('path');
|
|
if (pathParam) {
|
|
currentPath = pathParam;
|
|
if (pathParam.startsWith('repositorys')) {
|
|
activeTab = 'repositorys';
|
|
} else {
|
|
activeTab = 'backups';
|
|
}
|
|
}
|
|
loadFiles();
|
|
});
|
|
|
|
$: if (activeTab) {
|
|
// Reset path when switching tabs
|
|
if (!currentPath.startsWith(activeTab)) {
|
|
currentPath = activeTab;
|
|
}
|
|
loadFiles();
|
|
}
|
|
</script>
|
|
|
|
<!-- [SECTION: TEMPLATE] -->
|
|
<div class="container mx-auto p-4 max-w-6xl">
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-900">{$t.storage.management}</h1>
|
|
{#if currentPath}
|
|
<div class="flex items-center mt-2 text-sm text-gray-500">
|
|
<button on:click={() => { currentPath = activeTab; loadFiles(); }} class="hover:text-indigo-600">{$t.storage.root}</button>
|
|
{#each currentPath.split('/').slice(1) as part, i}
|
|
<span class="mx-2">/</span>
|
|
<button
|
|
on:click={() => { currentPath = currentPath.split('/').slice(0, i + 1).join('/'); loadFiles(); }}
|
|
class="hover:text-indigo-600 capitalize"
|
|
>
|
|
{part}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex justify-end mb-4">
|
|
<button
|
|
on:click={loadFiles}
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
|
|
disabled={isLoading}
|
|
>
|
|
{isLoading ? $t.storage.refreshing : $t.storage.refresh}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Main Content: File List -->
|
|
<div class="lg:col-span-2 space-y-4">
|
|
<!-- Tabs -->
|
|
<div class="border-b border-gray-200">
|
|
<nav class="-mb-px flex space-x-8">
|
|
<button
|
|
on:click={() => activeTab = 'backups'}
|
|
class="py-4 px-1 border-b-2 font-medium text-sm {activeTab === 'backups' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}"
|
|
>
|
|
{$t.storage.backups}
|
|
</button>
|
|
<button
|
|
on:click={() => activeTab = 'repositorys'}
|
|
class="py-4 px-1 border-b-2 font-medium text-sm {activeTab === 'repositorys' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}"
|
|
>
|
|
{$t.storage.repositories}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="flex items-center mb-2">
|
|
{#if currentPath && currentPath !== activeTab}
|
|
<button
|
|
on:click={navigateUp}
|
|
class="mr-4 inline-flex items-center px-3 py-1 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50"
|
|
>
|
|
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
</svg>
|
|
Back
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<FileList {files} on:delete={handleDelete} on:navigate={handleNavigate} />
|
|
</div>
|
|
|
|
<!-- Sidebar: Upload -->
|
|
<div class="lg:col-span-1">
|
|
<FileUpload
|
|
category={activeTab}
|
|
path={currentPath}
|
|
on:uploaded={loadFiles}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- [/SECTION: TEMPLATE] -->
|
|
|
|
<style>
|
|
/* ... */
|
|
</style>
|
|
|
|
<!-- [/DEF:StoragePage:Component] --> |