mirror of
https://github.com/Nezumi-2711/s3-drive-storage-manage.git
synced 2026-09-22 05:41:46 +00:00
fix: improve the ui
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
||||
FolderTree,
|
||||
FolderOpen,
|
||||
RotateCw,
|
||||
Sparkles,
|
||||
AlertCircle,
|
||||
Plus,
|
||||
DownloadCloud,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
Lock,
|
||||
ArrowUpDown,
|
||||
HardDrive,
|
||||
CheckCircle2,
|
||||
RefreshCw,
|
||||
Cable,
|
||||
} from "lucide-react"
|
||||
@@ -164,27 +162,28 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
</div>
|
||||
|
||||
{/* Main Action Buttons */}
|
||||
<div className="flex items-center gap-2 flex-wrap sm:flex-nowrap">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsImportOpen(true)}
|
||||
disabled={isLoading}
|
||||
className="h-8 text-xs px-3 rounded-lg gap-1.5 font-medium shadow-xs"
|
||||
className="flex-1 md:flex-none h-8 text-[11px] sm:text-xs px-2 sm:px-3 rounded-lg gap-1 sm:gap-1.5 font-medium shadow-xs whitespace-nowrap justify-center min-w-0"
|
||||
title="Import existing Google Drive folders into your storage root"
|
||||
>
|
||||
<DownloadCloud className="h-3.5 w-3.5 text-blue-500" />
|
||||
<span>Import from Drive</span>
|
||||
<DownloadCloud className="h-3.5 w-3.5 text-blue-500 shrink-0" />
|
||||
<span className="hidden sm:inline">Import from Drive</span>
|
||||
<span className="sm:hidden">Import</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setIsCreateOpen(true)}
|
||||
disabled={isLoading}
|
||||
className="h-8 text-xs px-3.5 rounded-lg gap-1.5 font-medium shadow-sm shadow-blue-500/20"
|
||||
className="flex-1 md:flex-none h-8 text-[11px] sm:text-xs px-2 sm:px-3.5 rounded-lg gap-1 sm:gap-1.5 font-medium shadow-sm shadow-blue-500/20 whitespace-nowrap justify-center min-w-0"
|
||||
title="Create a new storage bucket"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<Plus className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>New Bucket</span>
|
||||
</Button>
|
||||
|
||||
@@ -193,11 +192,12 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
size="sm"
|
||||
onClick={handleForceRefresh}
|
||||
disabled={isLoading || isRefreshing}
|
||||
className="h-8 text-xs px-2.5 rounded-lg gap-1.5 font-medium shadow-xs"
|
||||
className="flex-1 md:flex-none h-8 text-[11px] sm:text-xs px-2 sm:px-2.5 rounded-lg gap-1 sm:gap-1.5 font-medium shadow-xs whitespace-nowrap justify-center min-w-0"
|
||||
title="Recalculate bucket stats from live storage"
|
||||
>
|
||||
<RotateCw className={`h-3.5 w-3.5 ${isRefreshing ? "animate-spin text-blue-500" : ""}`} />
|
||||
<RotateCw className={`h-3.5 w-3.5 shrink-0 ${isRefreshing ? "animate-spin text-blue-500" : ""}`} />
|
||||
<span className="hidden sm:inline">{isRefreshing ? "Scanning..." : "Recalculate"}</span>
|
||||
<span className="sm:hidden">{isRefreshing ? "Scanning" : "Recalc"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,11 +320,11 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="flex items-center gap-1.5 self-start sm:self-auto p-1 bg-muted/50 rounded-lg border border-border/60">
|
||||
<div className="flex items-center justify-between sm:justify-start gap-1.5 w-full sm:w-auto p-1 bg-muted/50 rounded-lg border border-border/60">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilterTab("all")}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-all ${
|
||||
className={`flex-1 sm:flex-initial justify-center px-2.5 py-1 rounded-md text-xs font-medium transition-all flex items-center gap-1 ${
|
||||
filterTab === "all"
|
||||
? "bg-card text-foreground shadow-xs font-semibold"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
@@ -335,7 +335,7 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilterTab("public")}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-all flex items-center gap-1 ${
|
||||
className={`flex-1 sm:flex-initial justify-center px-2.5 py-1 rounded-md text-xs font-medium transition-all flex items-center gap-1 ${
|
||||
filterTab === "public"
|
||||
? "bg-card text-emerald-600 dark:text-emerald-400 shadow-xs font-semibold"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
@@ -347,7 +347,7 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilterTab("private")}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium transition-all flex items-center gap-1 ${
|
||||
className={`flex-1 sm:flex-initial justify-center px-2.5 py-1 rounded-md text-xs font-medium transition-all flex items-center gap-1 ${
|
||||
filterTab === "private"
|
||||
? "bg-card text-foreground shadow-xs font-semibold"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
@@ -661,33 +661,6 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* S3 capabilities bottom bar */}
|
||||
<div className="pt-3 border-t border-border/60 flex flex-col sm:flex-row sm:items-center justify-between gap-2.5">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-[11px] font-semibold text-muted-foreground mr-1">S3 API Support:</span>
|
||||
{[
|
||||
{ name: "s3:ListObjectsV2", desc: "List bucket objects" },
|
||||
{ name: "s3:GetObject", desc: "Download objects" },
|
||||
{ name: "s3:PutObject", desc: "Upload files" },
|
||||
{ name: "s3:DeleteObject", desc: "Delete files" },
|
||||
{ name: "s3:CreateMultipartUpload", desc: "Multipart uploads" },
|
||||
].map((api) => (
|
||||
<span
|
||||
key={api.name}
|
||||
title={api.desc}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-md text-[10px] font-mono font-medium bg-secondary/80 text-secondary-foreground border border-border/60 hover:border-border transition-colors cursor-default"
|
||||
>
|
||||
<Sparkles className="h-2.5 w-2.5 text-blue-500" />
|
||||
{api.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground flex items-center gap-1">
|
||||
<CheckCircle2 className="h-3 w-3 text-emerald-500" />
|
||||
<span>Direct Google Drive mapping active</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { useState } from "react"
|
||||
import { useNavigate } from "react-router"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
Database,
|
||||
LogOut,
|
||||
ShieldCheck,
|
||||
RotateCw,
|
||||
Radio,
|
||||
Lock,
|
||||
Cable,
|
||||
} from "lucide-react"
|
||||
import { useAuth } from "@/features/auth/hooks/useAuth"
|
||||
import { useStatus } from "@/features/status/hooks/useStatus"
|
||||
import { useBucketStats } from "@/features/status/hooks/useBucketStats"
|
||||
import { statusKeys } from "@/features/status/api/status.keys"
|
||||
import { SystemOverviewCard } from "@/features/status/components/SystemOverviewCard"
|
||||
import { BucketStatsTable } from "@/features/status/components/BucketStatsTable"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ThemeToggle } from "@/components/ThemeToggle"
|
||||
import { formatRelativeTime } from "@/lib/format"
|
||||
|
||||
export default function Dashboard() {
|
||||
const { signOut } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const {
|
||||
data: statusData,
|
||||
@@ -43,23 +33,6 @@ export default function Dashboard() {
|
||||
navigate("/sign-in", { replace: true })
|
||||
}
|
||||
|
||||
const handleRefreshAll = async () => {
|
||||
try {
|
||||
setIsRefreshing(true)
|
||||
await queryClient.invalidateQueries({ queryKey: statusKeys.all })
|
||||
} finally {
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkedAtTime = statusData?.checkedAt
|
||||
? new Date(statusData.checkedAt).toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-background bg-grid-pattern flex flex-col overflow-x-hidden">
|
||||
{/* Navigation Header */}
|
||||
@@ -95,10 +68,6 @@ export default function Dashboard() {
|
||||
|
||||
{/* Actions & Badges */}
|
||||
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
|
||||
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg bg-secondary/70 border border-border/60 text-xs text-muted-foreground">
|
||||
<Lock className="h-3.5 w-3.5 text-emerald-500" />
|
||||
<span className="font-medium text-foreground/80">Admin Mode</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/integration")}
|
||||
@@ -124,55 +93,6 @@ export default function Dashboard() {
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative z-10 flex-1 max-w-6xl w-full mx-auto p-4 sm:p-6 lg:p-8 space-y-6">
|
||||
{/* Welcome Banner */}
|
||||
<div className="relative overflow-hidden rounded-2xl border border-blue-500/20 bg-gradient-to-br from-card via-card to-blue-500/5 p-6 sm:p-7 shadow-lg shadow-blue-950/5">
|
||||
<div className="absolute top-0 right-0 h-40 w-40 bg-gradient-to-bl from-cyan-500/10 via-blue-500/5 to-transparent rounded-bl-full pointer-events-none" />
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 dark:text-emerald-400 text-xs font-semibold">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
<span>Authenticated Session Active</span>
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-foreground sm:text-3xl">
|
||||
Gateway Overview & Control
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground max-w-2xl font-medium">
|
||||
Live metrics from Cloudflare Workers connected to Google Drive API with AWS S3 signature verification.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap sm:flex-col items-start sm:items-end gap-2 shrink-0">
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-xl bg-card border border-border/80 shadow-xs">
|
||||
<Radio className="h-4 w-4 text-emerald-500 animate-pulse" />
|
||||
<div className="text-left">
|
||||
<div className="text-[10px] uppercase font-bold text-muted-foreground">Edge Network</div>
|
||||
<div className="text-xs font-bold text-foreground">Global CDN Active</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{checkedAtTime && (
|
||||
<span className="text-[11px] text-muted-foreground font-medium">
|
||||
Updated {formatRelativeTime(statusData?.checkedAt)} ({checkedAtTime})
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefreshAll}
|
||||
disabled={isStatusLoading || isBucketsLoading || isRefreshing}
|
||||
className="h-7 text-xs px-2 rounded-lg gap-1"
|
||||
title="Refresh status and bucket overview"
|
||||
>
|
||||
<RotateCw className={`h-3 w-3 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||
<span className="hidden min-[420px]:inline">Refresh</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unified Status / Quick Overview */}
|
||||
<SystemOverviewCard
|
||||
status={statusData}
|
||||
|
||||
Reference in New Issue
Block a user