From 24064e488bd47a404b252453f1c186e72a48289b Mon Sep 17 00:00:00 2001 From: Andres Pinto <143480783+apsantiso@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:31:34 +0200 Subject: [PATCH 1/5] feat: show hidden actions to all users, S3 permissions will determine if the action is possible or not --- .../views/SubAccountBucketsPage.tsx | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/sub-account/views/SubAccountBucketsPage.tsx b/src/sub-account/views/SubAccountBucketsPage.tsx index 4a4d7d2..1420a05 100644 --- a/src/sub-account/views/SubAccountBucketsPage.tsx +++ b/src/sub-account/views/SubAccountBucketsPage.tsx @@ -183,7 +183,8 @@ const BucketRow = ({ bucket, regionName, onOpen, onDelete, isAdmin }: BucketRowP style={{ display: 'flex', justifyContent: 'center' }} onClick={(e) => e.stopPropagation()} > - {hovered && isAdmin && ( + {/* TODO: hide based on user permissions once available */} + {hovered && ( onSearchChange('')} /> - {isAdmin && ( - - )} + {/* TODO: hide based on user permissions once available */} + From 1a15955be43061c51672d294ffad5244729ec0fc Mon Sep 17 00:00:00 2001 From: Andres Pinto <143480783+apsantiso@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:58:24 +0200 Subject: [PATCH 2/5] feat: add frontend pagination and searching --- .../views/SubAccountBucketsPage.tsx | 279 ++++++++---------- 1 file changed, 127 insertions(+), 152 deletions(-) diff --git a/src/sub-account/views/SubAccountBucketsPage.tsx b/src/sub-account/views/SubAccountBucketsPage.tsx index 1420a05..b293711 100644 --- a/src/sub-account/views/SubAccountBucketsPage.tsx +++ b/src/sub-account/views/SubAccountBucketsPage.tsx @@ -1,5 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useDebounce } from 'use-debounce'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { DotsThreeVerticalIcon, @@ -22,7 +21,7 @@ import { S3Client } from '@aws-sdk/client-s3'; import { useSubAccount } from '../context/SubAccountContext'; import { T, shadow, text, form } from '../tokens'; import subAccountAxios from '../core/sub-account-axios'; -import { useObjectPagination } from '../hooks/useObjectPagination'; +import { S3Credentials } from '../services/sub-account-s3-credentials.service'; import { Pagination } from '../../components/Pagination'; import { DeleteBucketConfirmModal } from '../components/DeleteBucketConfirmModal'; import { BucketLoggingSetup } from '../../components/buckets/BucketLoggingSetup'; @@ -37,18 +36,26 @@ interface BucketRecord { // ─── Helpers ───────────────────────────────────────────────────────────────── -function fmtDate(d?: Date): string { - if (!d) { +function formatDate(date?: Date): string { + if (!date) { return '—'; } - const day = d.toLocaleDateString('en-GB', { day: '2-digit' }); - const month = d.toLocaleDateString('en-GB', { month: 'short' }); - const year = d.getFullYear(); - const time = d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); + const day = date.toLocaleDateString('en-GB', { day: '2-digit' }); + const month = date.toLocaleDateString('en-GB', { month: 'short' }); + const year = date.getFullYear(); + const time = date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true }); return `${day}-${month}-${year} ${time}`; } +const createRegionS3Client = (endpoint: string, region: string, credentials: S3Credentials) => + new S3Client({ + endpoint: `https://${endpoint}`, + region, + credentials: { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey }, + forcePathStyle: true, + }); + // ─── StatStrip ──────────────────────────────────────────────────────────────── interface StatItem { @@ -115,12 +122,11 @@ interface BucketRowProps { regionName: string; onOpen: () => void; onDelete: () => void; - isAdmin: boolean; } -const BucketRow = ({ bucket, regionName, onOpen, onDelete, isAdmin }: BucketRowProps) => { - const [hovered, setHovered] = useState(false); - const [triggerHovered, setTriggerHovered] = useState(false); +const BucketRow = ({ bucket, regionName, onOpen, onDelete }: BucketRowProps) => { + const [isRowHovered, setIsRowHovered] = useState(false); + const [isTriggerHovered, setIsTriggerHovered] = useState(false); return (
setHovered(true)} - onMouseLeave={() => setHovered(false)} + onMouseEnter={() => setIsRowHovered(true)} + onMouseLeave={() => setIsRowHovered(false)} onClick={onOpen} > {/* Name */} @@ -162,7 +168,7 @@ const BucketRow = ({ bucket, regionName, onOpen, onDelete, isAdmin }: BucketRowP fontSize: 14, fontWeight: 500, color: `${T.primary}`, - textDecoration: hovered ? 'underline' : 'none', + textDecoration: isRowHovered ? 'underline' : 'none', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', @@ -176,7 +182,7 @@ const BucketRow = ({ bucket, regionName, onOpen, onDelete, isAdmin }: BucketRowP {regionName} {/* Created */} - {fmtDate(bucket.creationDate)} + {formatDate(bucket.creationDate)} {/* Actions */}
e.stopPropagation()} > {/* TODO: hide based on user permissions once available */} - {hovered && ( + {isRowHovered && ( setTriggerHovered(true)} - onMouseLeave={() => setTriggerHovered(false)} + onMouseEnter={() => setIsTriggerHovered(true)} + onMouseLeave={() => setIsTriggerHovered(false)} style={{ width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center', - background: triggerHovered ? T.gray10 : 'transparent', + background: isTriggerHovered ? T.gray10 : 'transparent', borderRadius: 6, color: T.gray60, }} @@ -229,6 +235,7 @@ const BucketRow = ({ bucket, regionName, onOpen, onDelete, isAdmin }: BucketRowP interface BucketsTableProps { buckets: BucketRecord[]; + totalCount: number; regions: SubAccountRegion[]; isLoading: boolean; search: string; @@ -236,13 +243,13 @@ interface BucketsTableProps { onOpen: (bucket: BucketRecord) => void; onDelete: (bucket: BucketRecord) => void; onCreateOpen: () => void; - isAdmin: boolean; } const TABLE_HEADERS = ['Name', 'Region', 'Created', '']; const BucketsTable = ({ buckets, + totalCount, regions, isLoading, search, @@ -250,7 +257,6 @@ const BucketsTable = ({ onOpen, onDelete, onCreateOpen, - isAdmin, }: BucketsTableProps) => (

Buckets

- {buckets.length} {buckets.length === 1 ? 'bucket' : 'buckets'} total + {totalCount} {totalCount === 1 ? 'bucket' : 'buckets'} total

@@ -351,7 +357,6 @@ const BucketsTable = ({ regionName={region?.name ?? b.regionSlug ?? '—'} onOpen={() => onOpen(b)} onDelete={() => onDelete(b)} - isAdmin={isAdmin} /> ); }) @@ -365,47 +370,31 @@ interface CreateBucketModalProps { isOpen: boolean; onClose: () => void; regions: SubAccountRegion[]; - credentials: { accessKeyId: string; secretAccessKey: string } | null; - loadBuckets: (options?: { signal?: AbortSignal }) => Promise; + credentials: S3Credentials | null; + bucketNames: string[]; onCreated: () => void; } const INITIAL_FORM_STATE = { bucketName: '', - selectedSlug: '', + selectedRegionSlug: '', versioningEnabled: false, objectLockEnabled: false, logging: { enabled: false, prefix: '', target: '' }, }; const CreateBucketModal = ({ - isOpen, onClose, regions, credentials, loadBuckets, onCreated, + isOpen, onClose, regions, credentials, bucketNames, onCreated, }: CreateBucketModalProps) => { const [formState, setFormState] = useState(INITIAL_FORM_STATE); const [isCreating, setIsCreating] = useState(false); - const [availableBucketNames, setAvailableBucketNames] = useState([]); - const { bucketName, selectedSlug, versioningEnabled, objectLockEnabled, logging } = formState; + const { bucketName, selectedRegionSlug, versioningEnabled, objectLockEnabled, logging } = formState; const updateForm = (patch: Partial) => setFormState((prev) => ({ ...prev, ...patch })); const canSubmitLogging = !logging.enabled || (!!logging.target && !!logging.prefix.trim()); - useEffect(() => { - if (!isOpen) { - return; - } - const controller = new AbortController(); - loadBuckets({ signal: controller.signal }) - .then(setAvailableBucketNames) - .catch((err) => { - if ((err as Error).name !== 'AbortError') { - notificationsService.error({ text: (err as Error).message }); - } - }); - return () => controller.abort(); - }, [isOpen, loadBuckets]); - useEffect(() => { if (isOpen) { return; @@ -413,13 +402,13 @@ const CreateBucketModal = ({ setFormState(INITIAL_FORM_STATE); }, [isOpen]); - const onToggleVersioning = (next: boolean) => { - updateForm({ versioningEnabled: next, ...(next ? {} : { objectLockEnabled: false }) }); + const onToggleVersioning = (enabled: boolean) => { + updateForm({ versioningEnabled: enabled, ...(enabled ? {} : { objectLockEnabled: false }) }); }; const selectedRegion = useMemo( - () => regions.find((r) => r.slug === selectedSlug) ?? regions[0] ?? null, - [regions, selectedSlug], + () => regions.find((r) => r.slug === selectedRegionSlug) ?? regions[0] ?? null, + [regions, selectedRegionSlug], ); const handleCreate = async () => { @@ -427,12 +416,7 @@ const CreateBucketModal = ({ return; } setIsCreating(true); - const regionClient = new S3Client({ - endpoint: `https://${selectedRegion.endpoint}`, - region: selectedRegion.slug, - credentials: { accessKeyId: credentials.accessKeyId, secretAccessKey: credentials.secretAccessKey }, - forcePathStyle: true, - }); + const regionClient = createRegionS3Client(selectedRegion.endpoint, selectedRegion.slug, credentials); try { await s3Service.createBucket(regionClient, bucketName, selectedRegion.slug, objectLockEnabled); if (versioningEnabled) { @@ -503,7 +487,7 @@ const CreateBucketModal = ({