diff --git a/Cargo.lock b/Cargo.lock index c10655c..3f38ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,21 +48,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -410,18 +395,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-compression" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-executor" version = "1.14.0" @@ -873,27 +846,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.20.2" @@ -1062,26 +1014,6 @@ dependencies = [ "tokio-util", ] -[[package]] -name = "compression-codecs" -version = "0.4.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", - "zstd", - "zstd-safe", -] - -[[package]] -name = "compression-core" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1792,7 +1724,6 @@ dependencies = [ name = "grass-assets" version = "0.1.0" dependencies = [ - "mime_guess", "rust-embed", ] @@ -1869,7 +1800,6 @@ dependencies = [ "toml 0.8.23", "totp-rs", "tower", - "tower-http", "tracing", "tracing-subscriber", "url", @@ -5313,20 +5243,15 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "async-compression", "bitflags", "bytes", - "futures-core", "futures-util", "http", "http-body", "pin-project-lite", - "tokio", - "tokio-util", "tower", "tower-layer", "tower-service", - "tracing", "url", ] @@ -6480,31 +6405,3 @@ dependencies = [ "log", "simd-adler32", ] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 7650f76..10b5de1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,6 @@ tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "signal", tokio-util = { version = "0.7", features = ["io"] } totp-rs = { version = "5", features = ["gen_secret", "otpauth"] } toml = { version = "0.8" } -tower-http = { version = "0.6", features = ["cors", "compression-full", "trace"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt", "json"] } url = "2.5.4" diff --git a/apps/console/src/components/avatar-crop.ts b/apps/console/src/components/avatar-crop.ts index 6d5306d..ee27037 100644 --- a/apps/console/src/components/avatar-crop.ts +++ b/apps/console/src/components/avatar-crop.ts @@ -7,7 +7,7 @@ export interface AvatarCropInput { offsetY: number; } -export interface AvatarCrop { +interface AvatarCrop { sourceX: number; sourceY: number; sourceSize: number; diff --git a/apps/console/src/components/settings-sidebar-sections.tsx b/apps/console/src/components/settings-sidebar-sections.tsx new file mode 100644 index 0000000..92b7d0b --- /dev/null +++ b/apps/console/src/components/settings-sidebar-sections.tsx @@ -0,0 +1,57 @@ +import { ArrowLeftIcon, type LucideIcon } from "lucide-react"; +import { NavLink } from "react-router"; + +import { + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@/components/ui/sidebar"; + +export function SettingsSidebarSections({ + title, + back, + sections, +}: { + title: string; + back: { to: string; label: string; tooltip: string }; + sections: { to: string; label: string; icon: LucideIcon; active: boolean }[]; +}) { + return ( + <> + + + + + + + + {back.label} + + + + + + + + {title} + + + {sections.map((section) => ( + + + + + {section.label} + + + + ))} + + + + + ); +} diff --git a/apps/console/src/features/account/account-menu.tsx b/apps/console/src/features/account/account-menu.tsx new file mode 100644 index 0000000..bcbb863 --- /dev/null +++ b/apps/console/src/features/account/account-menu.tsx @@ -0,0 +1,79 @@ +import { LogOutIcon, ShieldCheckIcon, UserRoundIcon } from "lucide-react"; +import { NavLink, useNavigate } from "react-router"; + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { useAuth } from "@/features/auth/auth-context"; +import { apiUrl } from "@/lib/api"; +import { showErrorToast } from "@/lib/toast"; + +export function AccountAvatar({ className }: { className: string }) { + const { user } = useAuth(); + return ( + + {user?.avatar_url && ( + + )} + + {(user?.display_name || user?.email || "GW").slice(0, 2).toUpperCase()} + + + ); +} + +export function AccountMenuContent({ + showAdministration = false, + ...position +}: { + showAdministration?: boolean; + align?: "start" | "center" | "end"; + side?: "top" | "right" | "bottom" | "left"; +}) { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + const signOut = async () => { + try { + await logout(); + navigate("/login", { replace: true }); + } catch (cause) { + showErrorToast(cause); + } + }; + return ( + + + +

{user?.display_name ?? user?.email}

+

{user?.email}

+
+
+ + + + + Personal settings + + + {showAdministration && ( + + + Administration + + + )} + + + + + Log out + + +
+ ); +} diff --git a/apps/console/src/features/account/account-sidebar-nav.tsx b/apps/console/src/features/account/account-sidebar-nav.tsx index 8df696a..dc1cae8 100644 --- a/apps/console/src/features/account/account-sidebar-nav.tsx +++ b/apps/console/src/features/account/account-sidebar-nav.tsx @@ -1,14 +1,7 @@ -import { ArrowLeftIcon, ShieldCheckIcon, UserRoundIcon } from "lucide-react"; -import { NavLink, useLocation } from "react-router"; +import { ShieldCheckIcon, UserRoundIcon } from "lucide-react"; +import { useLocation } from "react-router"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; +import { SettingsSidebarSections } from "@/components/settings-sidebar-sections"; const sections = [ { to: "/account/profile", label: "Profile", icon: UserRoundIcon }, @@ -19,42 +12,13 @@ export function AccountSidebarNav() { const location = useLocation(); return ( - <> - - - - - - - - Console - - - - - - - - Personal settings - - - {sections.map((section) => ( - - - - - {section.label} - - - - ))} - - - - + ({ + ...section, + active: location.pathname === section.to, + }))} + /> ); } diff --git a/apps/console/src/features/account/security-route.tsx b/apps/console/src/features/account/security-route.tsx index 2886034..401b575 100644 --- a/apps/console/src/features/account/security-route.tsx +++ b/apps/console/src/features/account/security-route.tsx @@ -1,3 +1,4 @@ +import { formatTimestamp } from "@/lib/format-timestamp"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { MailIcon, PlusIcon, SmartphoneIcon, Trash2Icon } from "lucide-react"; import { useState } from "react"; @@ -51,12 +52,6 @@ export function SecurityRoute() { if (security.isLoading) return ; if (security.isError || !security.data) return null; - const formatTimestamp = (value: string | null | undefined, fallback: string) => { - if (!value) return fallback; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? "Unknown date" : date.toLocaleString(); - }; - return (
diff --git a/apps/console/src/features/admin/admin-sections.ts b/apps/console/src/features/admin/admin-sections.ts index 209b047..f729a55 100644 --- a/apps/console/src/features/admin/admin-sections.ts +++ b/apps/console/src/features/admin/admin-sections.ts @@ -15,7 +15,7 @@ import { type LucideIcon, } from "lucide-react"; -export interface AdminSection { +interface AdminSection { to: string; label: string; icon: LucideIcon; diff --git a/apps/console/src/features/admin/admin.api.ts b/apps/console/src/features/admin/admin.api.ts index c33b639..e717b0f 100644 --- a/apps/console/src/features/admin/admin.api.ts +++ b/apps/console/src/features/admin/admin.api.ts @@ -1,3 +1,4 @@ +import type { StorageConfiguration, StorageInput } from "@/features/storage/storage-form"; import type { BuildStatus, ReleaseStatus, @@ -7,13 +8,13 @@ import type { AuditPage } from "@/features/audit/audit.api"; import { request } from "@/lib/api"; import type { Announcement } from "@/features/announcements/announcements.api"; -export interface AdministrationStatus { +interface AdministrationStatus { service: string; mode: "ready"; version: string; } -export type NodeStatus = "pending" | "active" | "draining" | "offline" | "disabled"; +type NodeStatus = "pending" | "active" | "draining" | "offline" | "disabled"; export interface AdminNode { id: string; @@ -36,13 +37,7 @@ export interface AdminNode { created_at: string; } -export type NodeDeletionStatus = - | "queued" - | "migrating" - | "draining" - | "deleting" - | "failed" - | "completed"; +type NodeDeletionStatus = "queued" | "migrating" | "draining" | "deleting" | "failed" | "completed"; export interface AdminNodeDeletionJob { id: string; @@ -69,7 +64,7 @@ export interface AdminNodeDeletionPlan { }>; } -export type NodeConfigurationSyncStatus = "pending" | "applying" | "applied" | "failed"; +type NodeConfigurationSyncStatus = "pending" | "applying" | "applied" | "failed"; export interface AdminNodeConfigurationSync { desired: NodeConfiguration | null; @@ -122,30 +117,23 @@ export interface NodeConfiguration { log: { level: string; format: "pretty" | "json" }; } -export interface AdminNodeResources { +interface AdminNodeResources { cpu_millicores: number; memory_mb: number; disk_mb: number; max_deployments: number; } -export interface AdminNodeUsage { +interface AdminNodeUsage { cpu_millicores: number; memory_mb: number; disk_mb: number; deployments: number; } -export interface UpdateNodeCapacityInput { - capacity_cpu_millicores: number; - capacity_memory_mb: number; - capacity_disk_mb: number; - max_deployments: number; -} +type LocalProcessState = "stopped" | "running" | "backoff" | "failed"; -export type LocalProcessState = "stopped" | "running" | "backoff" | "failed"; - -export interface AdminLocalProcess { +interface AdminLocalProcess { state: LocalProcessState; pid: number | null; started_at: string | null; @@ -161,7 +149,7 @@ export interface AdminLocalProcessInfo { process: AdminLocalProcess; } -export interface AdminQuotaLimit { +interface AdminQuotaLimit { dimension: string; limit_value: number; period: "none" | "monthly"; @@ -233,7 +221,7 @@ export interface AdminBatchItemResult { message?: string; } -export interface AdminUserFilters { +interface AdminUserFilters { q?: string; status?: AdminUser["status"]; role?: AdminUser["platform_role"]; @@ -241,7 +229,7 @@ export interface AdminUserFilters { export type AdminCodeStatus = "available" | "used" | "expired" | "revoked"; -export interface AdminCodeUser { +interface AdminCodeUser { id: string; email: string; display_name: string | null; @@ -267,7 +255,7 @@ export interface AdminGeneratedCode { created_at: string; } -export interface AdminCodesPage { +interface AdminCodesPage { scopes: string[]; codes: AdminCode[]; pagination: { @@ -278,7 +266,7 @@ export interface AdminCodesPage { }; } -export interface AdminRegistrationEmailCreator { +interface AdminRegistrationEmailCreator { id: string; email?: string | null; display_name?: string | null; @@ -291,7 +279,7 @@ export interface AdminRegistrationEmail { created_by: AdminRegistrationEmailCreator | null; } -export type AdminMfaEnforcement = "none" | "platform_admins" | "all_users"; +type AdminMfaEnforcement = "none" | "platform_admins" | "all_users"; export interface AdminMfaPolicy { allowed_factors: Array<"totp" | "email">; @@ -306,7 +294,7 @@ export interface AdminUserMfaPolicy { required_factors: Array<"totp" | "email">; } -export interface AdminUserMfaResponse { +interface AdminUserMfaResponse { factors: AdminMfaFactor[]; policy: AdminUserMfaPolicy; allowed_factors: Array<"totp" | "email">; @@ -326,7 +314,7 @@ export interface AdminPasswordPolicy { history_count: number; } -export interface AdminMfaFactor { +interface AdminMfaFactor { id: string; kind: "totp" | "email"; label: string | null; @@ -354,7 +342,7 @@ export interface AdminIdentityProvider { updated_at: string; } -export interface AdminTeamGroupRef { +interface AdminTeamGroupRef { id: string; code: string; name: string; @@ -371,14 +359,14 @@ export interface AdminTeam { created_at: string; } -export interface AdminTeamFilters { +interface AdminTeamFilters { q?: string; kind?: AdminTeam["kind"]; group_id?: string; quota_plan_id?: string; } -export interface AdminTeamDetail { +interface AdminTeamDetail { team: AdminTeam; members: { user_id: string; @@ -412,13 +400,13 @@ export interface QuotaLimitInput { limit_value: number | null; } -export interface AdminProjectTeamRef { +interface AdminProjectTeamRef { id: string; slug: string; name: string; } -export interface AdminDeploymentSummary { +interface AdminDeploymentSummary { id: string; environment: "production" | "preview"; build_status: BuildStatus; @@ -440,12 +428,12 @@ export interface AdminProject { created_at: string; } -export interface AdminProjectFilters { +interface AdminProjectFilters { q?: string; status?: AdminProject["status"]; } -export interface AdminProjectDetail { +interface AdminProjectDetail { id: string; uuid: string; slug: string; @@ -510,7 +498,7 @@ export interface AdminProjectDomain { updated_at: string; } -export interface AdminDeploymentGovernanceResult { +interface AdminDeploymentGovernanceResult { deployment: Pick< AdminGovernanceDeployment, "id" | "project_id" | "release_status" | "serve_status" @@ -518,7 +506,7 @@ export interface AdminDeploymentGovernanceResult { reason: string | null; } -export interface AdminDomainGovernanceResult { +interface AdminDomainGovernanceResult { domain: Pick< AdminProjectDomain, | "id" @@ -602,41 +590,13 @@ export interface AdminSettings { restart_required_sections: Array<"server" | "redis" | "node_manager" | "migration" | "log">; } -export type AdminStorageBackend = "local" | "s3" | "minio" | "r2"; - -export interface AdminStorageConfiguration { - backend: AdminStorageBackend; - local_root: string; - endpoint: string; - region: string; - bucket: string; - prefix: string; - force_path_style: boolean; - allow_http: boolean; - credentials_configured: boolean; -} - -export interface AdminStorageInput { - backend: AdminStorageBackend; - local_root?: string; - endpoint?: string; - region?: string; - bucket?: string; - prefix?: string; - force_path_style?: boolean; - allow_http?: boolean; - access_key_id?: string; - secret_access_key?: string; - session_token?: string; -} - -export type AdminStorageMigrationStatus = "pending" | "running" | "succeeded" | "failed"; +type AdminStorageMigrationStatus = "pending" | "running" | "succeeded" | "failed"; export interface AdminStorageMigration { id: string; status: AdminStorageMigrationStatus; - source: AdminStorageConfiguration; - target: AdminStorageConfiguration; + source: StorageConfiguration; + target: StorageConfiguration; copied_objects: number; copied_bytes: number; total_objects: number | null; @@ -648,14 +608,14 @@ export interface AdminStorageMigration { } export interface AdminStorageState { - storage: AdminStorageConfiguration; + storage: StorageConfiguration; maintenance: boolean; migration: AdminStorageMigration | null; } -export type AdminAnnouncement = Announcement; +type AdminAnnouncement = Announcement; -export interface AdminAnnouncementsPage { +interface AdminAnnouncementsPage { announcements: AdminAnnouncement[]; pagination: { page: number; @@ -796,12 +756,6 @@ export const adminApi = { method: "POST", }), - updateNodeCapacity: (nodeId: string, input: UpdateNodeCapacityInput) => - request<{ node: AdminNode }>(`/api/v1/admin/nodes/${nodeId}`, { - method: "PATCH", - body: JSON.stringify(input), - }), - updateNodeConfiguration: (nodeId: string, input: NodeConfiguration) => request<{ node: AdminNode }>(`/api/v1/admin/nodes/${nodeId}/configuration`, { method: "PUT", @@ -1113,23 +1067,18 @@ export const adminApi = { getStorage: () => request("/api/v1/admin/storage"), - testStorage: (input: AdminStorageInput) => + testStorage: (input: StorageInput) => request<{ tested: true }>("/api/v1/admin/storage/test", { method: "POST", body: JSON.stringify(input), }), - createStorageMigration: (input: AdminStorageInput) => + createStorageMigration: (input: StorageInput) => request<{ migration: AdminStorageMigration }>("/api/v1/admin/storage/migrations", { method: "POST", body: JSON.stringify(input), }), - getStorageMigration: () => - request>( - "/api/v1/admin/storage/migrations", - ), - listIdentityProviders: () => request<{ providers: AdminIdentityProvider[] }>("/api/v1/admin/identity-providers"), diff --git a/apps/console/src/features/admin/cleanup.api.ts b/apps/console/src/features/admin/cleanup.api.ts index 86fd6b4..d6c0fb7 100644 --- a/apps/console/src/features/admin/cleanup.api.ts +++ b/apps/console/src/features/admin/cleanup.api.ts @@ -16,7 +16,7 @@ export interface CleanupPreview { skipped: number; } -export interface AuditCleanupFilters extends AuditFilters { +interface AuditCleanupFilters extends AuditFilters { snapshot_before?: number; } diff --git a/apps/console/src/features/admin/components/nodes-panel.tsx b/apps/console/src/features/admin/components/nodes-panel.tsx index 45b393a..d1c48dc 100644 --- a/apps/console/src/features/admin/components/nodes-panel.tsx +++ b/apps/console/src/features/admin/components/nodes-panel.tsx @@ -1,3 +1,4 @@ +import { SettingSwitch } from "./setting-switch"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { PlayIcon, @@ -54,7 +55,6 @@ import { } from "@/components/ui/select"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; -import { Switch } from "@/components/ui/switch"; import { Table, TableBody, @@ -622,30 +622,6 @@ function ConfigurationNumberField({ ); } -function ConfigurationSwitch({ - id, - label, - description, - checked, - onCheckedChange, -}: { - id: string; - label: string; - description?: string; - checked: boolean; - onCheckedChange: (checked: boolean) => void; -}) { - return ( - - - {label} - {description && {description}} - - - - ); -} - function EditConfigurationDialog({ node }: { node: AdminNode }) { const queryClient = useQueryClient(); const source = node.configuration.desired ?? node.configuration.effective; @@ -819,7 +795,7 @@ function EditConfigurationDialog({ node }: { node: AdminNode }) {
Capabilities - -
- HTTPS - Development and logging - void; +}) { + return ( + + + {label} + {description && {description}} + + + + ); +} diff --git a/apps/console/src/features/admin/components/settings-layout.tsx b/apps/console/src/features/admin/components/settings-layout.tsx deleted file mode 100644 index 5eb6223..0000000 --- a/apps/console/src/features/admin/components/settings-layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Outlet } from "react-router"; - -export function SettingsLayout() { - return ; -} diff --git a/apps/console/src/features/admin/components/settings-panel.tsx b/apps/console/src/features/admin/components/settings-panel.tsx index 308fad1..6039c7d 100644 --- a/apps/console/src/features/admin/components/settings-panel.tsx +++ b/apps/console/src/features/admin/components/settings-panel.tsx @@ -1,16 +1,10 @@ +import { SettingSwitch } from "./setting-switch"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { - Field, - FieldContent, - FieldDescription, - FieldGroup, - FieldLabel, - FieldTitle, -} from "@/components/ui/field"; +import { Field, FieldDescription, FieldGroup, FieldLabel, FieldTitle } from "@/components/ui/field"; import { Input } from "@/components/ui/input"; import { Select, @@ -21,7 +15,6 @@ import { } from "@/components/ui/select"; import { SettingsCard } from "@/components/settings-card"; import { Skeleton } from "@/components/ui/skeleton"; -import { Switch } from "@/components/ui/switch"; import { useBranding } from "@/features/branding/branding-context"; import { adminApi, type AdminSettings } from "../admin.api"; @@ -89,30 +82,6 @@ function SaveAction({ pending, saved }: { pending: boolean; saved: boolean }) { ); } -function BooleanSetting({ - id, - label, - description, - checked, - onCheckedChange, -}: { - id: string; - label: string; - description?: string; - checked: boolean; - onCheckedChange: (checked: boolean) => void; -}) { - return ( - - - {label} - {description && {description}} - - - - ); -} - function ConfigurationStatus({ label, configured }: { label: string; configured: boolean }) { return ( @@ -492,7 +461,7 @@ function SettingsForm({ initial, section }: { initial: AdminSettings; section: S action={} > - - - } > - - - - - - - - - Administration - - - - - - - - Settings - - - {sections.map((section) => ( - - - - - {section.label} - - - - ))} - - - - + ({ + ...section, + active: location.pathname === section.to, + }))} + /> ); } diff --git a/apps/console/src/features/admin/components/storage-settings-panel.tsx b/apps/console/src/features/admin/components/storage-settings-panel.tsx index b8ef076..764b72b 100644 --- a/apps/console/src/features/admin/components/storage-settings-panel.tsx +++ b/apps/console/src/features/admin/components/storage-settings-panel.tsx @@ -1,3 +1,10 @@ +import { + storageDefaults, + storageInput, + type StorageBackend, + type StorageInput, + type StorageConfiguration, +} from "@/features/storage/storage-form"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2Icon, @@ -42,28 +49,17 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; -import { - adminApi, - type AdminStorageBackend, - type AdminStorageConfiguration, - type AdminStorageInput, - type AdminStorageMigration, - type AdminStorageState, -} from "../admin.api"; +import { adminApi, type AdminStorageMigration, type AdminStorageState } from "../admin.api"; const ACTIVE_MIGRATION_STATUSES = new Set(["pending", "running"]); -const PROVIDER_LABELS: Record = { +const PROVIDER_LABELS: Record = { local: "Local filesystem", s3: "S3-compatible", minio: "MinIO", r2: "Cloudflare R2", }; -function defaultRegion(backend: AdminStorageBackend) { - return backend === "r2" ? "auto" : "us-east-1"; -} - function migrationProgress(migration: AdminStorageMigration) { if (migration.total_objects === 0) { return migration.status === "succeeded" ? 100 : 0; @@ -72,7 +68,7 @@ function migrationProgress(migration: AdminStorageMigration) { return Math.min(100, Math.round((migration.copied_objects / migration.total_objects) * 100)); } -function CurrentStorage({ storage }: { storage: AdminStorageConfiguration }) { +function CurrentStorage({ storage }: { storage: StorageConfiguration }) { const remote = storage.backend !== "local"; return (
@@ -170,14 +166,14 @@ function MigrationStatus({ migration }: { migration: AdminStorageMigration }) { export function StorageSettingsPanel() { const queryClient = useQueryClient(); - const [backend, setBackend] = useState("local"); + const [backend, setBackend] = useState("local"); const [localRoot, setLocalRoot] = useState(null); const [endpoint, setEndpoint] = useState(""); - const [region, setRegion] = useState(defaultRegion("local")); + const [region, setRegion] = useState(storageDefaults("local").region); const [bucket, setBucket] = useState(""); const [prefix, setPrefix] = useState(""); - const [forcePathStyle, setForcePathStyle] = useState(false); - const [allowHttp, setAllowHttp] = useState(false); + const [forcePathStyle, setForcePathStyle] = useState(storageDefaults("local").forcePathStyle); + const [allowHttp, setAllowHttp] = useState(storageDefaults("local").allowHttp); const [accessKeyId, setAccessKeyId] = useState(""); const [secretAccessKey, setSecretAccessKey] = useState(""); const [sessionToken, setSessionToken] = useState(""); @@ -198,24 +194,20 @@ export function StorageSettingsPanel() { const remote = backend !== "local"; const maintenance = storageQuery.data?.maintenance ?? false; - const input = (): AdminStorageInput => { - if (backend === "local") { - return { backend, local_root: currentRoot.trim() }; - } - return { + const input = (): StorageInput => + storageInput({ backend, - local_root: currentRoot.trim(), - endpoint: endpoint.trim(), - region: region.trim(), - bucket: bucket.trim(), - prefix: prefix.trim().replace(/^\/+|\/+$/g, ""), - force_path_style: forcePathStyle, - allow_http: allowHttp, - ...(accessKeyId.trim() && { access_key_id: accessKeyId.trim() }), - ...(secretAccessKey.trim() && { secret_access_key: secretAccessKey.trim() }), - ...(sessionToken.trim() && { session_token: sessionToken.trim() }), - }; - }; + localRoot: currentRoot, + endpoint, + region, + bucket, + prefix, + forcePathStyle, + allowHttp, + accessKeyId, + secretAccessKey, + sessionToken, + }); const testMutation = useMutation({ mutationFn: () => adminApi.testStorage(input()), @@ -238,12 +230,12 @@ export function StorageSettingsPanel() { setConnectionVerified(false); }; - const changeBackend = (value: AdminStorageBackend) => { + const changeBackend = (value: StorageBackend) => { change(() => { setBackend(value); - setRegion(defaultRegion(value)); - setForcePathStyle(value === "minio"); - setAllowHttp(value === "minio"); + setRegion(storageDefaults(value).region); + setForcePathStyle(storageDefaults(value).forcePathStyle); + setAllowHttp(storageDefaults(value).allowHttp); }); }; @@ -275,7 +267,7 @@ export function StorageSettingsPanel() { Target backend