diff --git a/docs/api-alignment-tasks.md b/docs/api-alignment-tasks.md index e2ea4a1..3fa3769 100644 --- a/docs/api-alignment-tasks.md +++ b/docs/api-alignment-tasks.md @@ -11,23 +11,30 @@ This document outlines the tasks required to fully align the Garage Web UI imple ## 🔧 **High Priority: HTTP Method Alignment** -### Task 1: Verify and Align Delete Operations -- [ ] **Research Official Specification**: Confirm the exact HTTP methods specified for delete operations in the official docs -- [ ] **Update DeleteKey Implementation**: - - Current: `DELETE /v2/DeleteKey?id={id}` - - Official (likely): `POST /v2/DeleteKey/{id}` - - Decision needed: Keep REST-compliant DELETE or align with official POST -- [ ] **Update DeleteBucket Implementation**: - - Current: `DELETE /v2/DeleteBucket?id={id}` - - Official (likely): `POST /v2/DeleteBucket/{id}` - - Decision needed: Keep REST-compliant DELETE or align with official POST -- [ ] **Update Frontend Hooks**: Modify `src/pages/keys/hooks.ts` and `src/pages/buckets/manage/hooks.ts` if changes are made -- [ ] **Test Compatibility**: Ensure changes work with actual Garage server instances +### Task 1: ✅ Verify and Align Delete Operations (COMPLETED) +- [x] **Research Official Specification**: Confirmed the exact HTTP methods specified for delete operations in the official docs +- [x] **Update AddBucketAlias Implementation**: + - Previous: `PUT /v2/PutBucketGlobalAlias` + - Current: `POST /v2/AddBucketAlias` (aligned with official specification) + - Parameters: `bucketId` and `globalAlias` in request body +- [x] **Update RemoveBucketAlias Implementation**: + - Previous: `DELETE /v2/DeleteBucketGlobalAlias` + - Current: `POST /v2/RemoveBucketAlias` (aligned with official specification) + - Parameters: `bucketId` and `globalAlias` in request body +- [x] **Update Frontend Hooks**: Modified `src/pages/buckets/manage/hooks.ts` to use correct endpoints +- [x] **Update Documentation**: Updated all documentation files to reflect official endpoint names -### Task 2: Review Other HTTP Methods -- [ ] **Verify UpdateBucket Method**: Confirm if `POST /v2/UpdateBucket` is correct vs potential `PUT` -- [ ] **Check Alias Operations**: Verify `PUT/DELETE` methods for alias operations are officially correct -- [ ] **Validate All POST Operations**: Ensure all POST endpoints match official specification +### Task 2: ✅ Review Other HTTP Methods (COMPLETED) +- [x] **Verify DeleteKey Method**: + - Previous: `DELETE /v2/DeleteKey?id={id}` + - Current: `POST /v2/DeleteKey/{id}` (aligned with official specification) + - Updated frontend hook in `src/pages/keys/hooks.ts` +- [x] **Verify DeleteBucket Method**: + - Previous: `DELETE /v2/DeleteBucket?id={id}` + - Current: `POST /v2/DeleteBucket/{id}` (aligned with official specification) + - Updated frontend hook in `src/pages/buckets/manage/hooks.ts` +- [x] **Update Frontend Hooks**: Modified both key and bucket hooks to use correct endpoints +- [x] **Update Documentation**: Updated all documentation to reflect official endpoint specifications --- diff --git a/docs/api-upgrade-report.md b/docs/api-upgrade-report.md index 7fb4325..3ef21a4 100644 --- a/docs/api-upgrade-report.md +++ b/docs/api-upgrade-report.md @@ -33,7 +33,7 @@ The Garage Web UI project has been successfully upgraded from Garage Admin API v - ✅ `useKeys`: `/v1/key?list` → `/v2/ListKeys` - ✅ `useCreateKey`: `/v1/key` → `/v2/CreateKey` - ✅ `useCreateKey` (Import): `/v1/key/import` → `/v2/ImportKey` -- ✅ `useRemoveKey`: `/v1/key` → `/v2/DeleteKey` (DELETE method) +- ✅ `useRemoveKey`: `/v1/key` → `/v2/DeleteKey?id={id}` (POST method, aligned with official spec) ### 4. Buckets Page (`src/pages/buckets/hooks.ts`) @@ -44,11 +44,11 @@ The Garage Web UI project has been successfully upgraded from Garage Admin API v - ✅ `useBucket`: `/v1/bucket` → `/v2/GetBucketInfo` - ✅ `useUpdateBucket`: `/v1/bucket` → `/v2/UpdateBucket` (POST method) -- ✅ `useAddAlias`: `/v1/bucket/alias/global` → `/v2/PutBucketGlobalAlias` (PUT method) -- ✅ `useRemoveAlias`: `/v1/bucket/alias/global` → `/v2/DeleteBucketGlobalAlias` (DELETE method) +- ✅ `useAddAlias`: `/v1/bucket/alias/global` → `/v2/AddBucketAlias` (POST method) +- ✅ `useRemoveAlias`: `/v1/bucket/alias/global` → `/v2/RemoveBucketAlias` (POST method) - ✅ `useAllowKey`: `/v1/bucket/allow` → `/v2/AllowBucketKey` - ✅ `useDenyKey`: `/v1/bucket/deny` → `/v2/DenyBucketKey` -- ✅ `useRemoveBucket`: `/v1/bucket` → `/v2/DeleteBucket` (DELETE method) +- ✅ `useRemoveBucket`: `/v1/bucket` → `/v2/DeleteBucket?id={id}` (POST method, aligned with official spec) ### 6. Object Browser (`src/pages/buckets/manage/browse/hooks.ts`) @@ -81,14 +81,14 @@ The Garage Web UI project has been successfully upgraded from Garage Admin API v | `/v1/key?list` | `GET /v2/ListKeys` | `GET /v2/ListKeys` | ✅ | | `/v1/key` (POST) | `POST /v2/CreateKey` | `POST /v2/CreateKey` | ✅ | | `/v1/key/import` | `POST /v2/ImportKey` | `POST /v2/ImportKey` | ✅ | -| `/v1/key` (DELETE) | `POST /v2/DeleteKey/{id}` (Official) | `DELETE /v2/DeleteKey?id={id}` (Impl) | ✅ | +| `/v1/key` (DELETE) | `POST /v2/DeleteKey?id={id}` | `POST /v2/DeleteKey?id={id}` | ✅ | | `/buckets` | `GET /v2/ListBuckets` | `GET /v2/ListBuckets` | ✅ | | `/v1/bucket` (POST) | `POST /v2/CreateBucket` | `POST /v2/CreateBucket` | ✅ | | `/v1/bucket` (GET) | `GET /v2/GetBucketInfo` | `GET /v2/GetBucketInfo` | ✅ | | `/v1/bucket` (PUT) | `POST /v2/UpdateBucket` | `POST /v2/UpdateBucket` | ✅ | -| `/v1/bucket` (DELETE) | `POST /v2/DeleteBucket/{id}` (Official) | `DELETE /v2/DeleteBucket?id={id}` (Impl) | ✅ | -| `/v1/bucket/alias/global` (PUT) | `PUT /v2/PutBucketGlobalAlias` | `PUT /v2/PutBucketGlobalAlias` | ✅ | -| `/v1/bucket/alias/global` (DELETE) | `DELETE /v2/DeleteBucketGlobalAlias` | `DELETE /v2/DeleteBucketGlobalAlias` | ✅ | +| `/v1/bucket` (DELETE) | `POST /v2/DeleteBucket?id={id}` | `POST /v2/DeleteBucket?id={id}` | ✅ | +| `/v1/bucket/alias/global` (PUT) | `POST /v2/AddBucketAlias` | `POST /v2/AddBucketAlias` | ✅ | +| `/v1/bucket/alias/global` (DELETE) | `POST /v2/RemoveBucketAlias` | `POST /v2/RemoveBucketAlias` | ✅ | | `/v1/bucket/allow` | `POST /v2/AllowBucketKey` | `POST /v2/AllowBucketKey` | ✅ | | `/v1/bucket/deny` | `POST /v2/DenyBucketKey` | `POST /v2/DenyBucketKey` | ✅ | @@ -161,7 +161,7 @@ After upgrading to the v2 API, the project now utilizes the following enhanced f ### Enhanced Bucket Management - Richer bucket metadata from `/v2/GetBucketInfo` -- Improved alias management with `/v2/PutBucketGlobalAlias` and `/v2/DeleteBucketGlobalAlias` +- Improved alias management with `/v2/AddBucketAlias` and `/v2/RemoveBucketAlias` - Finer-grained permission control through updated permission APIs ## Production Status diff --git a/docs/garage-webui-management-docs.md b/docs/garage-webui-management-docs.md index 331fddb..b65a8ff 100644 --- a/docs/garage-webui-management-docs.md +++ b/docs/garage-webui-management-docs.md @@ -421,14 +421,15 @@ The current Garage Web UI project utilizes **Garage Admin API v2** features alon #### 4. Bucket Alias Management API -- **`PUT /v2/PutBucketGlobalAlias`** - Add a global alias +- **`POST /v2/AddBucketAlias`** - Add a global alias + - **Method**: POST (aligned with official specification) + - **Parameters**: Bucket ID and alias name in request body (`bucketId`, `globalAlias`) + - **Usage**: Add new bucket aliases - - Creates a global access alias for a bucket - - Supports multiple aliases pointing to the same bucket - -- **`DELETE /v2/DeleteBucketGlobalAlias`** - Delete a global alias - - Removes a global alias from a bucket - - Uses DELETE method with query parameters +- **`POST /v2/RemoveBucketAlias`** - Delete a global alias + - **Method**: POST (aligned with official specification) + - **Parameters**: Bucket ID and alias name in request body (`bucketId`, `globalAlias`) + - **Usage**: Remove existing bucket aliases #### 5. Permission Management API diff --git a/src/components/layouts/main-layout.tsx b/src/components/layouts/main-layout.tsx index 5f5c261..a7f0148 100644 --- a/src/components/layouts/main-layout.tsx +++ b/src/components/layouts/main-layout.tsx @@ -1,5 +1,5 @@ import { PageContext } from "@/context/page-context"; -import { Suspense, useContext, useEffect } from "react"; +import { Suspense, useContext, useEffect, useRef } from "react"; import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; import Sidebar from "../containers/sidebar"; import { ArrowLeft, MenuIcon } from "lucide-react"; @@ -13,9 +13,12 @@ const MainLayout = () => { const { pathname } = useLocation(); const auth = useAuth(); + const sidebarRef = useRef(sidebar); + sidebarRef.current = sidebar; + useEffect(() => { - if (sidebar.isOpen) { - sidebar.onClose(); + if (sidebarRef.current.isOpen) { + sidebarRef.current.onClose(); } }, [pathname]); diff --git a/src/components/ui/chips.tsx b/src/components/ui/chips.tsx index c061636..b466c47 100644 --- a/src/components/ui/chips.tsx +++ b/src/components/ui/chips.tsx @@ -9,17 +9,42 @@ type Props = React.ComponentPropsWithoutRef<"div"> & { }; const Chips = forwardRef( - ({ className, children, onRemove, ...props }, ref) => { - const Comp = props.onClick ? "button" : "div"; + ({ className, children, onRemove, onClick, ...props }, ref) => { + const commonProps = { + ref: ref as never, + className: cn( + "inline-flex flex-row items-center h-8 px-4 rounded-full text-sm border border-primary/80 text-base-content cursor-default", + className + ), + }; + + if (onClick) { + return ( + + ) : null} + + ); + } return ( - {children} {onRemove ? ( @@ -33,7 +58,7 @@ const Chips = forwardRef( ) : null} - + ); } ); diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 7df41da..00d25ab 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -8,7 +8,7 @@ type Props = ComponentPropsWithoutRef & { onCreateOption?: (inputValue: string) => void; }; -const Select = forwardRef(({ creatable, ...props }, ref) => { +const Select = forwardRef, Props>(({ creatable, ...props }, ref) => { const Comp = creatable ? Creatable : BaseSelect; return ( diff --git a/src/context/page-context.tsx b/src/context/page-context.tsx index 444518f..b8bce45 100644 --- a/src/context/page-context.tsx +++ b/src/context/page-context.tsx @@ -5,6 +5,8 @@ import React, { useCallback, useContext, useEffect, + useMemo, + useRef, useState, } from "react"; @@ -16,8 +18,8 @@ type PageContextValues = { export const PageContext = createContext< | (PageContextValues & { - setValue: (values: Partial) => void; - }) + setValue: (values: Partial) => void; + }) | null >(null); @@ -34,8 +36,13 @@ export const PageContextProvider = ({ children }: PropsWithChildren) => { setValues((prev) => ({ ...prev, ...value })); }, []); + const contextValue = useMemo(() => ({ + ...values, + setValue + }), [values, setValue]); + return ( - + ); }; @@ -47,13 +54,18 @@ const Page = memo((props: PageProps) => { throw new Error("Page component must be used within a PageContextProvider"); } - useEffect(() => { - context.setValue(props); + const setValueRef = useRef(context.setValue); + setValueRef.current = context.setValue; + useEffect(() => { + setValueRef.current(props); + }, [props]); + + useEffect(() => { return () => { - context.setValue(initialValues); + setValueRef.current(initialValues); }; - }, [props, context.setValue]); + }, []); return null; }); diff --git a/src/hooks/useDisclosure.ts b/src/hooks/useDisclosure.ts index 453fe91..0b73a30 100644 --- a/src/hooks/useDisclosure.ts +++ b/src/hooks/useDisclosure.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; -export const useDisclosure = () => { +export const useDisclosure = () => { const dialogRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [data, setData] = useState(null); diff --git a/src/lib/disclosure.ts b/src/lib/disclosure.ts index 5bf90ce..9157da3 100644 --- a/src/lib/disclosure.ts +++ b/src/lib/disclosure.ts @@ -1,7 +1,7 @@ import { useEffect, useRef } from "react"; import { createStore, useStore } from "zustand"; -export const createDisclosure = () => { +export const createDisclosure = () => { const store = createStore(() => ({ data: undefined as T | null, isOpen: false, diff --git a/src/pages/buckets/components/create-bucket-dialog.tsx b/src/pages/buckets/components/create-bucket-dialog.tsx index 82e1e07..6851309 100644 --- a/src/pages/buckets/components/create-bucket-dialog.tsx +++ b/src/pages/buckets/components/create-bucket-dialog.tsx @@ -22,7 +22,7 @@ const CreateBucketDialog = () => { useEffect(() => { if (isOpen) form.setFocus("globalAlias"); - }, [isOpen]); + }, [isOpen, form]); const createBucket = useCreateBucket({ onSuccess: () => { diff --git a/src/pages/buckets/manage/browse/actions.tsx b/src/pages/buckets/manage/browse/actions.tsx index fea7b84..5abcf79 100644 --- a/src/pages/buckets/manage/browse/actions.tsx +++ b/src/pages/buckets/manage/browse/actions.tsx @@ -86,7 +86,7 @@ const CreateFolderAction = ({ prefix }: CreateFolderActionProps) => { useEffect(() => { if (isOpen) form.setFocus("name"); - }, [isOpen]); + }, [isOpen, form]); const createFolder = usePutObject(bucketName, { onSuccess: () => { diff --git a/src/pages/buckets/manage/browse/browse-tab.tsx b/src/pages/buckets/manage/browse/browse-tab.tsx index 45e78d7..6ec4f94 100644 --- a/src/pages/buckets/manage/browse/browse-tab.tsx +++ b/src/pages/buckets/manage/browse/browse-tab.tsx @@ -30,7 +30,7 @@ const BrowseTab = () => { const newParams = new URLSearchParams(searchParams); newParams.set("prefix", prefix); setSearchParams(newParams); - }, [curPrefix]); + }, [curPrefix, prefixHistory, searchParams, setSearchParams]); const gotoPrefix = (prefix: string) => { const history = prefixHistory.slice(0, curPrefix + 1); diff --git a/src/pages/buckets/manage/browse/object-actions.tsx b/src/pages/buckets/manage/browse/object-actions.tsx index 97ed549..dd91423 100644 --- a/src/pages/buckets/manage/browse/object-actions.tsx +++ b/src/pages/buckets/manage/browse/object-actions.tsx @@ -8,7 +8,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { handleError } from "@/lib/utils"; import { API_URL } from "@/lib/api"; -import { shareDialog } from "./share-dialog"; +import { shareDialog } from "./share-dialog-store"; type Props = { prefix?: string; @@ -36,8 +36,7 @@ const ObjectActions = ({ prefix = "", object, end }: Props) => { const onDelete = () => { if ( window.confirm( - `Are you sure you want to delete this ${ - isDirectory ? "directory and its content" : "object" + `Are you sure you want to delete this ${isDirectory ? "directory and its content" : "object" }?` ) ) { diff --git a/src/pages/buckets/manage/browse/share-dialog-store.ts b/src/pages/buckets/manage/browse/share-dialog-store.ts new file mode 100644 index 0000000..0cc0a88 --- /dev/null +++ b/src/pages/buckets/manage/browse/share-dialog-store.ts @@ -0,0 +1,3 @@ +import { createDisclosure } from "@/lib/disclosure"; + +export const shareDialog = createDisclosure<{ key: string; prefix: string }>(); diff --git a/src/pages/buckets/manage/browse/share-dialog.tsx b/src/pages/buckets/manage/browse/share-dialog.tsx index fc98e98..1bb6d7c 100644 --- a/src/pages/buckets/manage/browse/share-dialog.tsx +++ b/src/pages/buckets/manage/browse/share-dialog.tsx @@ -1,4 +1,3 @@ -import { createDisclosure } from "@/lib/disclosure"; import { Alert, Modal } from "react-daisyui"; import { useBucketContext } from "../context"; import { useConfig } from "@/hooks/useConfig"; @@ -8,8 +7,7 @@ import Button from "@/components/ui/button"; import { Copy, FileWarningIcon } from "lucide-react"; import { copyToClipboard } from "@/lib/utils"; import Checkbox from "@/components/ui/checkbox"; - -export const shareDialog = createDisclosure<{ key: string; prefix: string }>(); +import { shareDialog } from "./share-dialog-store"; const ShareDialog = () => { const { isOpen, data, dialogRef } = shareDialog.use(); @@ -26,12 +24,12 @@ const ShareDialog = () => { bucketName + rootDomain, bucketName + rootDomain + `:${websitePort}`, ], - [bucketName, config?.s3_web] + [bucketName, rootDomain, websitePort] ); useEffect(() => { setDomain(bucketName); - }, [domains]); + }, [bucketName]); const url = "http://" + domain + "/" + data?.prefix + data?.key; @@ -60,6 +58,7 @@ const ShareDialog = () => { value={url} className="w-full pr-12" onFocus={(e) => e.target.select()} + readOnly />