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,
|
FolderTree,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
RotateCw,
|
RotateCw,
|
||||||
Sparkles,
|
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Plus,
|
Plus,
|
||||||
DownloadCloud,
|
DownloadCloud,
|
||||||
@@ -14,7 +13,6 @@ import {
|
|||||||
Lock,
|
Lock,
|
||||||
ArrowUpDown,
|
ArrowUpDown,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
CheckCircle2,
|
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Cable,
|
Cable,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
@@ -164,27 +162,28 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Action Buttons */}
|
{/* 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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsImportOpen(true)}
|
onClick={() => setIsImportOpen(true)}
|
||||||
disabled={isLoading}
|
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"
|
title="Import existing Google Drive folders into your storage root"
|
||||||
>
|
>
|
||||||
<DownloadCloud className="h-3.5 w-3.5 text-blue-500" />
|
<DownloadCloud className="h-3.5 w-3.5 text-blue-500 shrink-0" />
|
||||||
<span>Import from Drive</span>
|
<span className="hidden sm:inline">Import from Drive</span>
|
||||||
|
<span className="sm:hidden">Import</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setIsCreateOpen(true)}
|
onClick={() => setIsCreateOpen(true)}
|
||||||
disabled={isLoading}
|
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"
|
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>
|
<span>New Bucket</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -193,11 +192,12 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleForceRefresh}
|
onClick={handleForceRefresh}
|
||||||
disabled={isLoading || isRefreshing}
|
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"
|
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="hidden sm:inline">{isRefreshing ? "Scanning..." : "Recalculate"}</span>
|
||||||
|
<span className="sm:hidden">{isRefreshing ? "Scanning" : "Recalc"}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,11 +320,11 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
{/* 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFilterTab("all")}
|
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"
|
filterTab === "all"
|
||||||
? "bg-card text-foreground shadow-xs font-semibold"
|
? "bg-card text-foreground shadow-xs font-semibold"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
@@ -335,7 +335,7 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFilterTab("public")}
|
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"
|
filterTab === "public"
|
||||||
? "bg-card text-emerald-600 dark:text-emerald-400 shadow-xs font-semibold"
|
? "bg-card text-emerald-600 dark:text-emerald-400 shadow-xs font-semibold"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
@@ -347,7 +347,7 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setFilterTab("private")}
|
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"
|
filterTab === "private"
|
||||||
? "bg-card text-foreground shadow-xs font-semibold"
|
? "bg-card text-foreground shadow-xs font-semibold"
|
||||||
: "text-muted-foreground hover:text-foreground"
|
: "text-muted-foreground hover:text-foreground"
|
||||||
@@ -661,33 +661,6 @@ export function BucketStatsTable({ stats, isLoading, error }: BucketStatsTablePr
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,20 @@
|
|||||||
import { useState } from "react"
|
|
||||||
import { useNavigate } from "react-router"
|
import { useNavigate } from "react-router"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
|
||||||
import {
|
import {
|
||||||
Database,
|
Database,
|
||||||
LogOut,
|
LogOut,
|
||||||
ShieldCheck,
|
|
||||||
RotateCw,
|
|
||||||
Radio,
|
|
||||||
Lock,
|
|
||||||
Cable,
|
Cable,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { useAuth } from "@/features/auth/hooks/useAuth"
|
import { useAuth } from "@/features/auth/hooks/useAuth"
|
||||||
import { useStatus } from "@/features/status/hooks/useStatus"
|
import { useStatus } from "@/features/status/hooks/useStatus"
|
||||||
import { useBucketStats } from "@/features/status/hooks/useBucketStats"
|
import { useBucketStats } from "@/features/status/hooks/useBucketStats"
|
||||||
import { statusKeys } from "@/features/status/api/status.keys"
|
|
||||||
import { SystemOverviewCard } from "@/features/status/components/SystemOverviewCard"
|
import { SystemOverviewCard } from "@/features/status/components/SystemOverviewCard"
|
||||||
import { BucketStatsTable } from "@/features/status/components/BucketStatsTable"
|
import { BucketStatsTable } from "@/features/status/components/BucketStatsTable"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle"
|
import { ThemeToggle } from "@/components/ThemeToggle"
|
||||||
import { formatRelativeTime } from "@/lib/format"
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { signOut } = useAuth()
|
const { signOut } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: statusData,
|
data: statusData,
|
||||||
@@ -43,23 +33,6 @@ export default function Dashboard() {
|
|||||||
navigate("/sign-in", { replace: true })
|
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 (
|
return (
|
||||||
<div className="relative min-h-screen bg-background bg-grid-pattern flex flex-col overflow-x-hidden">
|
<div className="relative min-h-screen bg-background bg-grid-pattern flex flex-col overflow-x-hidden">
|
||||||
{/* Navigation Header */}
|
{/* Navigation Header */}
|
||||||
@@ -95,10 +68,6 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Actions & Badges */}
|
{/* Actions & Badges */}
|
||||||
<div className="flex items-center gap-2 sm:gap-3 shrink-0">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate("/integration")}
|
onClick={() => navigate("/integration")}
|
||||||
@@ -124,55 +93,6 @@ export default function Dashboard() {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* 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">
|
<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 */}
|
{/* Unified Status / Quick Overview */}
|
||||||
<SystemOverviewCard
|
<SystemOverviewCard
|
||||||
status={statusData}
|
status={statusData}
|
||||||
|
|||||||
Reference in New Issue
Block a user