more design tweaks
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { X, Activity, Search, Play, RotateCw, Clock, CheckCircle2, AlertCircle, FileText, Database, HardDrive, MapPin, ExternalLink, ArrowRight } from 'lucide-svelte';
|
||||
import { Button } from './ui/button';
|
||||
import { Card } from './ui/card';
|
||||
import Dialog from './ui/Dialog.svelte';
|
||||
import { getJobDetailSystemJobsJobIdGet, type JobSchema } from '$lib/api';
|
||||
import { cn, formatLocalTime, formatLocalDateTime, parseUTCDate } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -44,8 +45,8 @@
|
||||
onMount(loadJob);
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0 z-[1200] bg-black/90 backdrop-blur-md flex items-center justify-center p-6 animate-in fade-in duration-300" onmousedown={onClear}>
|
||||
<Card class="w-[800px] max-h-[90vh] bg-bg-secondary border-border-color shadow-2xl overflow-hidden flex flex-col animate-in zoom-in-95 duration-300" onmousedown={(e) => e.stopPropagation()}>
|
||||
<Dialog show={true} onClose={onClear} ariaLabelledBy="modal-title">
|
||||
<Card class="w-[800px] max-h-[90vh] overflow-hidden flex flex-col shadow-2xl">
|
||||
{#if loading}
|
||||
<div class="flex-1 flex flex-col items-center justify-center gap-4 py-24">
|
||||
<RotateCw size={48} class="animate-spin text-blue-500" />
|
||||
@@ -63,7 +64,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<h2 class="text-xl font-bold text-text-primary">{job.job_type.charAt(0) + job.job_type.slice(1).toLowerCase()} job #{job.id}</h2>
|
||||
<h2 id="modal-title" class="text-xl font-bold text-text-primary">{job.job_type.charAt(0) + job.job_type.slice(1).toLowerCase()} job #{job.id}</h2>
|
||||
<span class={cn(
|
||||
"px-2.5 py-0.5 rounded-full border text-[10px] font-medium uppercase tracking-wider",
|
||||
job.status === 'COMPLETED' ? 'text-success-color border-success-color/20 bg-success-color/5' :
|
||||
@@ -149,4 +150,4 @@
|
||||
</footer>
|
||||
{/if}
|
||||
</Card>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ContextMenu from './ui/ContextMenu.svelte';
|
||||
import EmptyState from './ui/EmptyState.svelte';
|
||||
import { FolderSearch, ExternalLink, ChevronLeft } from 'lucide-svelte';
|
||||
import type { TreemapItem } from '$lib/types';
|
||||
|
||||
@@ -135,9 +136,10 @@
|
||||
{/each}
|
||||
|
||||
{#if currentItems.length === 0}
|
||||
<div class="absolute inset-0 flex items-center justify-center opacity-20">
|
||||
<span class="text-sm font-medium">No nested data</span>
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No nested data"
|
||||
class="absolute inset-0 p-0"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -326,10 +326,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeyDown} />
|
||||
|
||||
<div
|
||||
class="file-browser flex h-full flex-col overflow-hidden rounded-lg border border-border-color bg-bg-secondary shadow-2xl min-w-0"
|
||||
onkeydown={handleKeyDown}
|
||||
tabindex="0"
|
||||
role="application"
|
||||
aria-label="File Browser"
|
||||
>
|
||||
@@ -378,8 +378,9 @@
|
||||
<div
|
||||
class="flex-1 flex items-center bg-bg-primary border border-border-color/40 rounded-md px-3 h-9 shadow-inner overflow-hidden max-w-3xl group transition-all focus-within:border-action-color/50 min-w-0"
|
||||
onclick={handleAddressClick}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleAddressClick()}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
tabindex="0"
|
||||
>
|
||||
<Folder size={16} class="text-yellow-500/80 mr-2 shrink-0"></Folder>
|
||||
|
||||
@@ -389,7 +390,6 @@
|
||||
class="flex-1 bg-transparent border-none outline-none text-[13px] text-text-primary mono"
|
||||
bind:value={pathInputValue}
|
||||
onblur={() => setTimeout(() => isEditingPath = false, 100)}
|
||||
autoFocus
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-1 flex items-center overflow-x-auto scrollbar-hide">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
children: Snippet;
|
||||
}>();
|
||||
|
||||
let menuElement: HTMLElement;
|
||||
let menuElement = $state<HTMLElement | null>(null);
|
||||
|
||||
// Adjust position to stay within viewport
|
||||
let adjustedX = $state(0);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { type Snippet } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
children: Snippet;
|
||||
class?: string;
|
||||
ariaLabelledBy?: string;
|
||||
}
|
||||
|
||||
let { show, onClose, children, class: className, ariaLabelledBy }: Props = $props();
|
||||
|
||||
function handleMousedown(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div
|
||||
class="fixed inset-0 z-[1200] bg-black/90 backdrop-blur-md flex items-center justify-center p-6"
|
||||
onmousedown={handleMousedown}
|
||||
transition:fade={{ duration: 200 }}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class={cn("relative animate-in zoom-in-95 duration-300", className)}
|
||||
onmousedown={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
tabindex="-1"
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { cn } from '$lib/utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
icon?: any;
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: Snippet;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
let { icon: Icon, title, description, action, class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn("flex w-full flex-col items-center justify-center p-12 text-center animate-in fade-in zoom-in duration-500", className)}>
|
||||
<div class="flex w-full flex-col items-center">
|
||||
{#if Icon}
|
||||
<div class="w-20 h-20 bg-bg-tertiary rounded-full flex items-center justify-center mb-6 border-2 border-dashed border-border-color opacity-50">
|
||||
<Icon size={40} class="text-text-secondary" strokeWidth={1} />
|
||||
</div>
|
||||
{/if}
|
||||
<h2 class="text-lg font-bold text-text-primary">{title}</h2>
|
||||
{#if description}
|
||||
<p class="text-sm mt-3 text-text-secondary leading-relaxed opacity-60">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
{#if action}
|
||||
<div class="mt-8">
|
||||
{@render action()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import { Button } from './button';
|
||||
import { cn } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
icon: any;
|
||||
iconSize?: number;
|
||||
onclick?: (e: MouseEvent) => void;
|
||||
variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
class?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
icon: Icon,
|
||||
iconSize,
|
||||
onclick,
|
||||
variant = 'ghost',
|
||||
size = 'md',
|
||||
class: className,
|
||||
title,
|
||||
disabled = false,
|
||||
href
|
||||
}: Props = $props();
|
||||
|
||||
const sizes = {
|
||||
sm: { btn: 'h-7 w-7', icon: 12 },
|
||||
md: { btn: 'h-9 w-9', icon: 16 },
|
||||
lg: { btn: 'h-11 w-11', icon: 20 }
|
||||
};
|
||||
|
||||
const currentSize = $derived(sizes[size]);
|
||||
const currentIconSize = $derived(iconSize || currentSize.icon);
|
||||
</script>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
{variant}
|
||||
{onclick}
|
||||
{href}
|
||||
class={cn(currentSize.btn, className)}
|
||||
{title}
|
||||
{disabled}
|
||||
>
|
||||
<Icon size={currentIconSize} />
|
||||
</Button>
|
||||
@@ -3,7 +3,7 @@ import type { Button as ButtonPrimitive } from "bits-ui";
|
||||
import Root from "./button.svelte";
|
||||
|
||||
const buttonVariants = tv({
|
||||
base: "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-40 active:translate-y-[1px] border select-none relative overflow-hidden",
|
||||
base: "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-40 active:translate-y-[1px] border select-none relative overflow-hidden [&_svg]:shrink-0",
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-blue-600 text-white border-blue-700 shadow-lg shadow-blue-500/10 hover:bg-blue-700",
|
||||
|
||||
@@ -159,10 +159,10 @@
|
||||
<!-- Dynamic Scan Status Overlay (Global) -->
|
||||
<ScanStatusOverlay />
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-8 lg:p-10 relative">
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden p-8 lg:p-10 relative">
|
||||
<!-- Animated Background Glow -->
|
||||
<div class="absolute -top-24 -right-24 w-96 h-96 bg-blue-600/5 rounded-full blur-[120px] pointer-events-none"></div>
|
||||
<div class="absolute -bottom-24 -left-24 w-96 h-96 bg-blue-600/5 rounded-full blur-[120px] pointer-events-none"></div>
|
||||
<div class="absolute -top-24 -right-24 w-96 h-96 bg-blue-600/5 rounded-full blur-[120px] pointer-events-none overflow-hidden"></div>
|
||||
<div class="absolute -bottom-24 -left-24 w-96 h-96 bg-blue-600/5 rounded-full blur-[120px] pointer-events-none overflow-hidden"></div>
|
||||
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
import SectionHeader from '$lib/components/ui/SectionHeader.svelte';
|
||||
import StatusBadge from '$lib/components/ui/StatusBadge.svelte';
|
||||
import ProgressBar from '$lib/components/ui/ProgressBar.svelte';
|
||||
import Dialog from '$lib/components/ui/Dialog.svelte';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { cn } from '$lib/utils';
|
||||
@@ -459,8 +460,8 @@
|
||||
<Button variant="default" size="sm" class="h-7 text-[10px] bg-blue-600 hover:bg-blue-500" onclick={() => handleStartBackup(media.id, media.identifier)}>Archive</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-text-secondary hover:text-text-primary hover:bg-white/5" onclick={() => openEdit(media)}><Edit3 size={12} /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-text-secondary hover:text-error-color hover:bg-error-color/10" onclick={() => handleDelete(media.id)}><Trash2 size={12} /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-text-secondary hover:text-text-primary hover:bg-white/5" onclick={() => openEdit(media)}><Edit3 size={14} /></Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-text-secondary hover:text-error-color hover:bg-error-color/10" onclick={() => handleDelete(media.id)}><Trash2 size={16} /></Button>
|
||||
</div>
|
||||
</td>
|
||||
{/snippet}
|
||||
@@ -828,89 +829,87 @@
|
||||
</div>
|
||||
|
||||
<!-- Registration Dialog -->
|
||||
{#if showRegisterDialog}
|
||||
<div class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-6" onmousedown={() => showRegisterDialog = false}>
|
||||
<Card class="w-[700px] max-h-[90vh] overflow-y-auto bg-bg-secondary border-border-color shadow-2xl p-8 flex flex-col gap-6 animate-in zoom-in-95 duration-300" onmousedown={(e) => e.stopPropagation()}>
|
||||
<header class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-text-primary">Add new media</h2>
|
||||
<p class="text-sm text-text-secondary mt-1 opacity-60">Add physical storage locations for your backups.</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="hover:bg-white/5" onclick={() => showRegisterDialog = false}><X size={20} /></Button>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
{#each providersList as provider}
|
||||
<button class={cn("flex flex-col items-center gap-3 p-4 rounded-xl border-2 transition-all", newMedia.media_type === provider.provider_id ? "bg-blue-500/10 border-blue-500 text-blue-400 shadow-lg shadow-blue-500/10" : "bg-bg-primary/50 border-border-color text-text-secondary hover:border-text-secondary/30")}
|
||||
onclick={() => {
|
||||
newMedia.media_type = provider.provider_id;
|
||||
if (provider.provider_id === 'lto_tape') newMedia.location = 'Storage Shelf';
|
||||
else if (provider.provider_id === 'local_hdd') newMedia.location = 'Offsite Safe';
|
||||
else newMedia.location = 'Cloud';
|
||||
}}
|
||||
>
|
||||
{@render ConfigIcon(provider.provider_id)}
|
||||
<span class="text-xs font-semibold">{provider.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
<Dialog show={showRegisterDialog} onClose={() => showRegisterDialog = false} ariaLabelledBy="register-title">
|
||||
<Card class="w-[700px] max-h-[90vh] overflow-y-auto p-8 flex flex-col gap-6 shadow-2xl">
|
||||
<header class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 id="register-title" class="text-xl font-bold text-text-primary">Add new media</h2>
|
||||
<p class="text-sm text-text-secondary mt-1 opacity-60">Add physical storage locations for your backups.</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="hover:bg-white/5" onclick={() => showRegisterDialog = false}><X size={20} /></Button>
|
||||
</header>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="identifier">Identifier (Barcode/SN)</label>
|
||||
<Input id="identifier" bind:value={newMedia.identifier} placeholder="BUP-00001" class="h-10 bg-bg-primary/50 border-border-color font-mono text-sm" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="capacity">Capacity (GB)</label>
|
||||
<Input id="capacity" type="number" bind:value={newMedia.capacity_gb} class="h-10 bg-bg-primary/50 border-border-color font-mono" />
|
||||
<p class="text-[10px] text-text-secondary leading-tight opacity-60">Auto-detected when possible. You can manually reduce this to reserve space.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
{#each providersList as provider}
|
||||
<button class={cn("flex flex-col items-center gap-3 p-4 rounded-xl border-2 transition-all", newMedia.media_type === provider.provider_id ? "bg-blue-500/10 border-blue-500 text-blue-400 shadow-lg shadow-blue-500/10" : "bg-bg-primary/50 border-border-color text-text-secondary hover:border-text-secondary/30")}
|
||||
onclick={() => {
|
||||
newMedia.media_type = provider.provider_id;
|
||||
if (provider.provider_id === 'lto_tape') newMedia.location = 'Storage Shelf';
|
||||
else if (provider.provider_id === 'local_hdd') newMedia.location = 'Offsite Safe';
|
||||
else newMedia.location = 'Cloud';
|
||||
}}
|
||||
>
|
||||
{@render ConfigIcon(provider.provider_id)}
|
||||
<span class="text-xs font-semibold">{provider.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="location">Physical location</label>
|
||||
<div class="relative">
|
||||
<MapPin size={16} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input id="location" bind:value={newMedia.location} placeholder="Cabinet A, Shelf 2" class="h-10 bg-bg-primary/50 pl-12 border-border-color font-mono text-sm" />
|
||||
</div>
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="identifier">Identifier (Barcode/SN)</label>
|
||||
<Input id="identifier" bind:value={newMedia.identifier} placeholder="BUP-00001" class="h-10 bg-bg-primary/50 border-border-color font-mono text-sm" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="capacity">Capacity (GB)</label>
|
||||
<Input id="capacity" type="number" bind:value={newMedia.capacity_gb} class="h-10 bg-bg-primary/50 border-border-color font-mono" />
|
||||
<p class="text-[10px] text-text-secondary leading-tight opacity-60">Auto-detected when possible. You can manually reduce this to reserve space.</p>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Provider Config Fields -->
|
||||
{#if activeProvider}
|
||||
<div class="grid grid-cols-2 gap-4 animate-in slide-in-from-top-2 duration-300">
|
||||
{#each Object.entries(activeProvider.config_schema) as [key, schema]}
|
||||
{@const field = schema as any}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="config-{key}">{field.title || key}</label>
|
||||
<Input
|
||||
id="config-{key}"
|
||||
bind:value={dynamicConfig[key]}
|
||||
placeholder={field.description || ""}
|
||||
type={key.includes("key") || key.includes("passphrase") ? "password" : "text"}
|
||||
class="h-10 bg-bg-primary/50 border-border-color font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-3 pt-4 border-t border-border-color">
|
||||
<Button variant="outline" class="flex-1 h-10" onclick={() => showRegisterDialog = false}>Cancel</Button>
|
||||
<Button variant="default" class="flex-[2] h-10" onclick={handleRegister}>Register media</Button>
|
||||
</footer>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="location">Physical location</label>
|
||||
<div class="relative">
|
||||
<MapPin size={16} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input id="location" bind:value={newMedia.location} placeholder="Cabinet A, Shelf 2" class="h-10 bg-bg-primary/50 pl-12 border-border-color font-mono text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Provider Config Fields -->
|
||||
{#if activeProvider}
|
||||
<div class="grid grid-cols-2 gap-4 animate-in slide-in-from-top-2 duration-300">
|
||||
{#each Object.entries(activeProvider.config_schema) as [key, schema]}
|
||||
{@const field = schema as any}
|
||||
<div class="space-y-2">
|
||||
<label class="text-xs font-medium text-text-secondary ml-1" for="config-{key}">{field.title || key}</label>
|
||||
<Input
|
||||
id="config-{key}"
|
||||
bind:value={dynamicConfig[key]}
|
||||
placeholder={field.description || ""}
|
||||
type={key.includes("key") || key.includes("passphrase") ? "password" : "text"}
|
||||
class="h-10 bg-bg-primary/50 border-border-color font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<footer class="flex gap-3 pt-4 border-t border-border-color">
|
||||
<Button variant="outline" class="flex-1 h-10" onclick={() => showRegisterDialog = false}>Cancel</Button>
|
||||
<Button variant="default" class="flex-[2] h-10" onclick={handleRegister}>Register media</Button>
|
||||
</footer>
|
||||
</Card>
|
||||
</Dialog>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
{#if editingMedia}
|
||||
<div class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[100] flex items-center justify-center p-6" onmousedown={() => editingMedia = null}>
|
||||
<Card class="w-[600px] max-h-[90vh] overflow-y-auto bg-bg-secondary border-border-color shadow-2xl p-8 flex flex-col gap-6 animate-in zoom-in-95 duration-300" onmousedown={(e) => e.stopPropagation()}>
|
||||
<Dialog show={!!editingMedia} onClose={() => editingMedia = null} ariaLabelledBy="edit-title">
|
||||
{#if editingMedia}
|
||||
<Card class="w-[600px] max-h-[90vh] overflow-y-auto p-8 flex flex-col gap-6 shadow-2xl">
|
||||
<header class="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-text-primary flex items-center gap-3">
|
||||
<h2 id="edit-title" class="text-xl font-bold text-text-primary flex items-center gap-3">
|
||||
<Edit3 size={20} class="text-blue-500" />
|
||||
Edit media config
|
||||
</h2>
|
||||
@@ -975,6 +974,6 @@
|
||||
</Button>
|
||||
</footer>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import SectionHeader from '$lib/components/ui/SectionHeader.svelte';
|
||||
import StatusBadge from '$lib/components/ui/StatusBadge.svelte';
|
||||
import ProgressBar from '$lib/components/ui/ProgressBar.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import JobDetailModal from '$lib/components/JobDetailModal.svelte';
|
||||
import {
|
||||
listJobsSystemJobsGet,
|
||||
@@ -214,10 +215,11 @@
|
||||
</div>
|
||||
</Card>
|
||||
{:else}
|
||||
<div class="py-12 border-2 border-dashed border-border-color rounded-2xl flex flex-col items-center justify-center opacity-20">
|
||||
<Activity size={40} class="mb-2 text-blue-500" />
|
||||
<p class="text-xs font-medium">No active operations</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Activity}
|
||||
title="No active operations"
|
||||
description="There are currently no tasks running on this station."
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||
import SectionHeader from '$lib/components/ui/SectionHeader.svelte';
|
||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { ScrollArea } from '$lib/components/ui/scroll-area';
|
||||
import FileBrowser from '$lib/components/file-browser/FileBrowser.svelte';
|
||||
@@ -189,20 +190,17 @@
|
||||
</PageHeader>
|
||||
|
||||
{#if (manifest?.total_files || 0) === 0 && !loading}
|
||||
<div class="flex-1 flex flex-col items-center justify-center p-12 text-center animate-in fade-in zoom-in duration-500">
|
||||
<div class="max-w-2xl flex flex-col items-center">
|
||||
<div class="w-24 h-24 bg-bg-tertiary rounded-full flex items-center justify-center mb-8 border-2 border-dashed border-border-color opacity-50">
|
||||
<History size={48} class="text-text-secondary" strokeWidth={1} />
|
||||
</div>
|
||||
<h2 class="text-xl font-bold text-text-primary">Recovery queue is empty</h2>
|
||||
<p class="text-sm mt-4 text-text-secondary leading-relaxed opacity-60">
|
||||
You haven't selected any files for restoration yet. Use the Index Browser to find and queue the items you need to recover from your archives.
|
||||
</p>
|
||||
<Button variant="default" class="mt-8 px-8 shadow-lg shadow-blue-500/20" href="/index-browser">
|
||||
<EmptyState
|
||||
icon={History}
|
||||
title="Recovery queue is empty"
|
||||
description="You haven't selected any files for restoration yet. Use the Index Browser to find and queue the items you need to recover from your archives."
|
||||
>
|
||||
{#snippet action()}
|
||||
<Button variant="default" class="px-8 shadow-lg shadow-blue-500/20" href="/index-browser">
|
||||
Browse virtual index <ArrowRight size={14} class="ml-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</EmptyState>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8 flex-1 min-h-0">
|
||||
<!-- RECOVERY STRUCTURE TREE -->
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
} from "lucide-svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import PageHeader from "$lib/components/ui/PageHeader.svelte";
|
||||
import SectionHeader from "$lib/components/ui/SectionHeader.svelte";
|
||||
import { Card } from "$lib/components/ui/card";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import {
|
||||
@@ -245,7 +246,7 @@
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class={cn(
|
||||
"flex items-center gap-3 px-4 py-3 rounded-xl text-3xs font-black uppercase tracking-widest transition-all whitespace-nowrap",
|
||||
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all whitespace-nowrap cursor-pointer",
|
||||
activeTab === tab.id
|
||||
? "bg-blue-500/10 text-blue-500 border border-blue-500/20 shadow-[0_0_15px_rgba(59,130,246,0.1)]"
|
||||
: "text-text-secondary hover:bg-white/5 border border-transparent"
|
||||
@@ -265,14 +266,8 @@
|
||||
{:else}
|
||||
{#if activeTab === 'hardware'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500">
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20"><Monitor size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">LTO Hardware</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Define tape drive device nodes</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="LTO hardware" icon={Monitor} class="mb-6 px-0" />
|
||||
<div class="space-y-3">
|
||||
{#each tapeDrives as drive, i}
|
||||
<div class="flex gap-2 animate-in slide-in-from-left-4 duration-300" style="animation-delay: {i * 50}ms">
|
||||
@@ -280,11 +275,11 @@
|
||||
<Terminal size={14} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input bind:value={tapeDrives[i]} placeholder="/dev/nst0" class="h-10 bg-bg-primary/50 pl-10 border-border-color font-mono text-xs" />
|
||||
</div>
|
||||
<Button variant="ghost" class="h-10 w-10 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeDrive(i)}><Trash2 size={16} /></Button>
|
||||
<Button variant="ghost" class="h-10 w-10 shrink-0 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeDrive(i)}><Trash2 size={18} /></Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button variant="outline" class="w-full h-12 border-dashed border-2 border-border-color hover:border-blue-500/50 hover:bg-blue-500/5 font-black uppercase tracking-widest text-3xs mt-2" onclick={addDrive}>
|
||||
<Plus size={16} class="mr-2" /> Add Tape Drive
|
||||
<Button variant="outline" class="w-full h-11 border-dashed border-2 font-medium text-sm mt-2" onclick={addDrive}>
|
||||
<Plus size={20} class="mr-2" /> Add tape drive
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -292,55 +287,37 @@
|
||||
|
||||
{:else if activeTab === 'paths'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500 space-y-6">
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20"><HardDrive size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Source Roots</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Directories available for archival</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Source roots" icon={HardDrive} class="mb-6 px-0" />
|
||||
<div class="space-y-3">
|
||||
{#each sourceRoots as root, i}
|
||||
<div class="flex gap-2">
|
||||
<Input bind:value={sourceRoots[i]} placeholder="/mnt/data" class="h-10 bg-bg-primary/50 border-border-color font-mono text-xs" />
|
||||
<Button variant="ghost" class="h-10 w-10 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeSource(i)}><Trash2 size={16} /></Button>
|
||||
<Button variant="ghost" class="h-10 w-10 shrink-0 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeSource(i)}><Trash2 size={18} /></Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button variant="outline" class="w-full h-12 border-dashed border-2 border-border-color font-black uppercase tracking-widest text-3xs" onclick={addSource}><Plus size={16} class="mr-2" /> Add Source Root</Button>
|
||||
<Button variant="outline" class="w-full h-11 border-dashed border-2 font-medium text-sm" onclick={addSource}><Plus size={20} class="mr-2" /> Add source root</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-green-500/10 rounded-xl text-green-500 border border-green-500/20"><ArrowRight size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Restore Targets</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Permitted recovery destinations</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Restore targets" icon={ArrowRight} iconColor="text-success-color" class="mb-6 px-0" />
|
||||
<div class="space-y-3">
|
||||
{#each restoreDestinations as dest, i}
|
||||
<div class="flex gap-2">
|
||||
<Input bind:value={restoreDestinations[i]} placeholder="/restores" class="h-10 bg-bg-primary/50 border-border-color font-mono text-xs" />
|
||||
<Button variant="ghost" class="h-10 w-10 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeDest(i)}><Trash2 size={16} /></Button>
|
||||
<Button variant="ghost" class="h-10 w-10 shrink-0 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeDest(i)}><Trash2 size={18} /></Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button variant="outline" class="w-full h-12 border-dashed border-2 border-border-color font-black uppercase tracking-widest text-3xs" onclick={addDest}><Plus size={16} class="mr-2" /> Add Restore Path</Button>
|
||||
<Button variant="outline" class="w-full h-11 border-dashed border-2 font-medium text-sm" onclick={addDest}><Plus size={20} class="mr-2" /> Add restore path</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'exclusions'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500">
|
||||
<Card class="p-6 shadow-xl border-border-color/60 bg-bg-secondary">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-orange-500/10 rounded-xl text-orange-500 border border-orange-500/20"><ListX size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Exclusion Policy</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Git-style ignore patterns for all scans.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Exclusion policy" icon={ListX} iconColor="text-orange-500" class="mb-6 px-0" />
|
||||
<div class="space-y-5">
|
||||
<textarea
|
||||
bind:value={globalExclusions}
|
||||
@@ -349,14 +326,14 @@
|
||||
></textarea>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h4 class="text-5xs font-black uppercase tracking-widest text-text-secondary opacity-40">Common Patterns</h4>
|
||||
<h4 class="text-[10px] font-semibold uppercase tracking-wider text-text-secondary opacity-40">Common patterns</h4>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
{#each commonExclusions as item}
|
||||
<button
|
||||
class="flex items-center justify-between px-3 py-1.5 bg-bg-primary/40 border border-border-color/60 rounded-lg hover:border-orange-500/40 hover:bg-orange-500/5 transition-all group"
|
||||
onclick={() => addCommonExclusion(item.pattern)}
|
||||
>
|
||||
<span class="text-5xs font-bold text-text-secondary group-hover:text-text-primary">{item.label}</span>
|
||||
<span class="text-[10px] font-medium text-text-secondary group-hover:text-text-primary">{item.label}</span>
|
||||
<Plus size={10} class="text-text-secondary opacity-20 group-hover:opacity-100" />
|
||||
</button>
|
||||
{/each}
|
||||
@@ -366,8 +343,8 @@
|
||||
<div class="p-4 bg-orange-500/5 border border-dashed border-orange-500/30 rounded-xl flex gap-4 items-start">
|
||||
<ShieldAlert size={20} class="text-orange-500 shrink-0 mt-0.5" />
|
||||
<div class="space-y-1">
|
||||
<span class="text-4xs font-black uppercase text-orange-500 tracking-widest">Policy Warning</span>
|
||||
<p class="text-5xs text-text-secondary leading-relaxed font-medium">Broad exclusion patterns can result in critical data being skipped during the archival process. Ensure patterns match only transient data.</p>
|
||||
<span class="text-xs font-bold text-orange-500 uppercase tracking-wider">Policy warning</span>
|
||||
<p class="text-xs text-text-secondary leading-relaxed font-medium">Broad exclusion patterns can result in critical data being skipped during the archival process. Ensure patterns match only transient data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,42 +353,30 @@
|
||||
|
||||
{:else if activeTab === 'scheduling'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500 space-y-6">
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20"><CalendarClock size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Scan Frequency</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Scheduled system discovery policy</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Scan frequency" icon={CalendarClock} class="mb-6 px-0" />
|
||||
<div class="flex gap-3">
|
||||
<div class="relative flex-1">
|
||||
<Terminal size={14} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input bind:value={scanSchedule} placeholder="0 2 * * *" class="h-10 bg-bg-primary/50 pl-10 border-border-color font-mono text-xs" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" class="h-10 px-3 text-5xs uppercase font-black tracking-widest" onclick={() => scanSchedule = "0 * * * *"}>Hourly</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-5xs uppercase font-black tracking-widest" onclick={() => scanSchedule = "0 2 * * *"}>Daily</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-[10px] font-semibold" onclick={() => scanSchedule = "0 * * * *"}>Hourly</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-[10px] font-semibold" onclick={() => scanSchedule = "0 2 * * *"}>Daily</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-action-color/10 rounded-xl text-action-color border border-action-color/20"><CalendarClock size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Archival Frequency</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Scheduled media ingestion policy</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Archival frequency" icon={CalendarClock} iconColor="text-action-color" class="mb-6 px-0" />
|
||||
<div class="flex gap-3">
|
||||
<div class="relative flex-1">
|
||||
<Terminal size={14} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input bind:value={archivalSchedule} placeholder="0 4 * * 0" class="h-10 bg-bg-primary/50 pl-10 border-border-color font-mono text-xs" />
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" class="h-10 px-3 text-5xs uppercase font-black tracking-widest" onclick={() => archivalSchedule = "0 4 * * 0"}>Weekly</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-5xs uppercase font-black tracking-widest" onclick={() => archivalSchedule = "0 4 1 * *"}>Monthly</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-[10px] font-semibold" onclick={() => archivalSchedule = "0 4 * * 0"}>Weekly</Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-[10px] font-semibold" onclick={() => archivalSchedule = "0 4 1 * *"}>Monthly</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -419,14 +384,8 @@
|
||||
|
||||
{:else if activeTab === 'notifications'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500">
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20"><Bell size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Alerting Endpoints</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Apprise-compatible notification URLs</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Alerting endpoints" icon={Bell} class="mb-6 px-0" />
|
||||
<div class="space-y-3">
|
||||
{#each notificationUrls as url, i}
|
||||
<div class="flex gap-2">
|
||||
@@ -434,35 +393,29 @@
|
||||
<Globe size={14} class="absolute left-4 top-3 text-text-secondary opacity-50" />
|
||||
<Input bind:value={notificationUrls[i]} placeholder="prowl://apikey" class="h-10 bg-bg-primary/50 pl-10 border-border-color font-mono text-xs" />
|
||||
</div>
|
||||
<Button variant="outline" class="h-10 px-3 text-5xs uppercase font-black tracking-widest border-border-color" onclick={() => testNotify(notificationUrls[i])}>Test</Button>
|
||||
<Button variant="ghost" class="h-10 w-10 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeNotify(i)}><Trash2 size={16} /></Button>
|
||||
<Button variant="outline" class="h-10 px-3 text-[10px] font-semibold border-border-color" onclick={() => testNotify(notificationUrls[i])}>Test</Button>
|
||||
<Button variant="ghost" class="h-10 w-10 shrink-0 rounded-xl bg-error-color/5 text-error-color/60 hover:bg-error-color/10 hover:text-error-color" onclick={() => removeNotify(i)}><Trash2 size={18} /></Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button variant="outline" class="w-full h-12 border-dashed border-2 border-border-color font-black uppercase tracking-widest text-3xs" onclick={addNotify}><Plus size={16} class="mr-2" /> Add Notification Endpoint</Button>
|
||||
<Button variant="outline" class="w-full h-11 border-dashed border-2 font-medium text-sm" onclick={addNotify}><Plus size={20} class="mr-2" /> Add notification endpoint</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'system'}
|
||||
<div class="animate-in slide-in-from-bottom-4 duration-500 space-y-6">
|
||||
<Card class="p-6 bg-bg-secondary border-border-color shadow-xl">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20"><Database size={20} /></div>
|
||||
<div>
|
||||
<h3 class="text-lg font-black text-text-primary uppercase tracking-tight">Index Management</h3>
|
||||
<p class="text-4xs text-text-secondary font-medium uppercase tracking-wider opacity-60">Backup and restore the system state</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card class="p-5 shadow-xl">
|
||||
<SectionHeader title="Index management" icon={Database} class="mb-6 px-0" />
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<Button variant="outline" class="h-14 font-black uppercase tracking-widest text-3xs border-border-color hover:bg-blue-500/5 group" onclick={handleExport} disabled={exporting}>
|
||||
<Button variant="outline" class="h-14 font-medium text-sm group" onclick={handleExport} disabled={exporting}>
|
||||
{#if exporting}
|
||||
<RotateCw size={18} class="mr-2 animate-spin" /> Compiling...
|
||||
{:else}
|
||||
<Download size={18} class="mr-2 text-blue-400 group-hover:scale-110 transition-transform" /> Export Database Index
|
||||
<Download size={18} class="mr-2 text-blue-400 group-hover:scale-110 transition-transform" /> Export database index
|
||||
{/if}
|
||||
</Button>
|
||||
<Button variant="outline" class="h-14 font-black uppercase tracking-widest text-3xs border-border-color hover:bg-orange-500/5 group opacity-50 cursor-not-allowed">
|
||||
<Upload size={18} class="mr-2 text-orange-400" /> Import Index (Restricted)
|
||||
<Button variant="outline" class="h-14 font-medium text-sm group opacity-50 cursor-not-allowed">
|
||||
<Upload size={18} class="mr-2 text-orange-400" /> Import index (Restricted)
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user