@@ -87,6 +87,21 @@ interface ObjectRow {
8787 LastModified : string
8888}
8989
90+ type ObjectListError = "accessDenied" | "loadFailed"
91+
92+ function isAccessDeniedError ( error : unknown ) : boolean {
93+ const serviceError = error as {
94+ $metadata ?: { httpStatusCode ?: number }
95+ Code ?: string
96+ name ?: string
97+ message ?: string
98+ }
99+ if ( serviceError ?. $metadata ?. httpStatusCode === 403 ) return true
100+
101+ const code = ( serviceError ?. Code ?? serviceError ?. name ?? serviceError ?. message ?? "" ) . toLowerCase ( )
102+ return code === "accessdenied" || code === "forbidden" || code . includes ( "access denied" )
103+ }
104+
90105interface ObjectListProps {
91106 bucket : string
92107 path : string
@@ -123,12 +138,11 @@ export function ObjectList({
123138 const [ searchTerm , setSearchTerm ] = React . useState ( "" )
124139 const [ showDeleted , setShowDeleted ] = useLocalStorage ( "object-list-show-deleted" , false )
125140 const [ loading , setLoading ] = React . useState ( false )
141+ const [ listError , setListError ] = React . useState < ObjectListError | null > ( null )
126142 const [ data , setData ] = React . useState < ObjectRow [ ] > ( [ ] )
127143 const [ nextToken , setNextToken ] = React . useState < string | undefined > ( )
128144 const [ showScrollShortcuts , setShowScrollShortcuts ] = React . useState ( false )
129145 const [ bucketVersioningState , setBucketVersioningState ] = React . useState < BucketVersioningState > ( "unknown" )
130- const [ versioningError , setVersioningError ] = React . useState ( "" )
131- const [ versioningReload , setVersioningReload ] = React . useState ( 0 )
132146 const [ deleteDialogOpen , setDeleteDialogOpen ] = React . useState ( false )
133147 const [ deleteDialogKeys , setDeleteDialogKeys ] = React . useState < string [ ] > ( [ ] )
134148 const [ deleteAllVersions , setDeleteAllVersions ] = React . useState ( false )
@@ -203,6 +217,7 @@ export function ObjectList({
203217 const requestScope = activeScopeRef . current
204218 loadingRef . current = true
205219 setLoading ( true )
220+ if ( ! shouldAppend ) setListError ( null )
206221 try {
207222 const response = await listObject ( bucket , prefix || undefined , resolvedPageSize , token , {
208223 includeDeleted : showDeleted ,
@@ -226,6 +241,7 @@ export function ObjectList({
226241 }
227242
228243 setNextToken ( r . NextContinuationToken )
244+ setListError ( null )
229245
230246 const prefixItems : ObjectRow [ ] = ( r . CommonPrefixes ?? [ ] ) . map ( ( item ) => ( {
231247 Key : item . Prefix ?? "" ,
@@ -261,6 +277,7 @@ export function ObjectList({
261277 setNextToken ( undefined )
262278 if ( ! shouldAppend ) {
263279 setData ( [ ] )
280+ setListError ( isAccessDeniedError ( error ) ? "accessDenied" : "loadFailed" )
264281 }
265282 }
266283 } finally {
@@ -327,7 +344,6 @@ export function ObjectList({
327344
328345 const loadBucketVersioningStatus = async ( ) => {
329346 setBucketVersioningState ( "unknown" )
330- setVersioningError ( "" )
331347
332348 try {
333349 const resp = await getBucketVersioning ( bucket )
@@ -338,7 +354,6 @@ export function ObjectList({
338354 console . error ( "Failed to load bucket versioning status:" , error )
339355 if ( ! cancelled ) {
340356 setBucketVersioningState ( "unknown" )
341- setVersioningError ( t ( "Failed to get data" ) )
342357 }
343358 }
344359 }
@@ -348,7 +363,7 @@ export function ObjectList({
348363 return ( ) => {
349364 cancelled = true
350365 }
351- } , [ bucket , getBucketVersioning , t , versioningReload ] )
366+ } , [ bucket , getBucketVersioning ] )
352367
353368 const displayKey = React . useCallback (
354369 ( key : string ) => {
@@ -531,7 +546,6 @@ export function ObjectList({
531546 size = "sm"
532547 onClick = { ( ) => openDeleteDialog ( [ row . original . Key ] ) }
533548 disabled = { bucketVersioningState === "unknown" }
534- aria-describedby = { bucketVersioningState === "unknown" ? "object-versioning-status" : undefined }
535549 >
536550 < RiDeleteBin5Line className = "size-4" aria-hidden />
537551 < span > { t ( "Delete" ) } </ span >
@@ -781,7 +795,6 @@ export function ObjectList({
781795 className = "border-destructive text-destructive"
782796 onClick = { handleBatchDelete }
783797 disabled = { bucketVersioningState === "unknown" }
784- aria-describedby = { bucketVersioningState === "unknown" ? "object-versioning-status" : undefined }
785798 >
786799 < RiDeleteBin5Line className = "size-4" aria-hidden />
787800 < span > { t ( "Delete Selected" ) } </ span >
@@ -824,44 +837,30 @@ export function ObjectList({
824837 </ div >
825838 </ PageHeader >
826839
827- { bucketVersioningState === "unknown" ? (
828- < div
829- id = "object-versioning-status"
830- role = { versioningError ? "alert" : "status" }
831- className = "flex flex-col gap-3 border border-border bg-muted/30 p-3 text-sm sm:flex-row sm:items-center sm:justify-between"
832- >
833- < span className = { versioningError ? "text-destructive" : "text-muted-foreground" } >
834- { versioningError || t ( "Loading…" ) }
835- </ span >
836- { versioningError ? (
837- < Button
838- type = "button"
839- variant = "outline"
840- className = "w-full sm:w-auto"
841- onClick = { ( ) => setVersioningReload ( ( value ) => value + 1 ) }
842- >
843- < RiRefreshLine className = "size-4" aria-hidden />
844- { t ( "Refresh" ) }
845- </ Button >
846- ) : null }
847- </ div >
848- ) : null }
849-
850840 < DataTable
851841 table = { table }
852842 isLoading = { displayState === "loading" || displayState === "filtered-loading" }
843+ errorTitle = {
844+ listError === "accessDenied"
845+ ? t ( "Access Denied" )
846+ : listError === "loadFailed"
847+ ? t ( "Failed to load objects" )
848+ : undefined
849+ }
853850 emptyTitle = { emptyTitle }
854851 emptyDescription = { emptyDescription }
855852 />
856853
857- < div className = "flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground" >
858- < span >
859- { t ( "Loaded {count} objects" , {
860- count : data . length ,
861- } ) }
862- </ span >
863- < span > { t ( "Filtering and sorting apply to loaded objects" ) } </ span >
864- </ div >
854+ { ! listError ? (
855+ < div className = "flex flex-wrap items-center justify-between gap-3 text-sm text-muted-foreground" >
856+ < span >
857+ { t ( "Loaded {count} objects" , {
858+ count : data . length ,
859+ } ) }
860+ </ span >
861+ < span > { t ( "Filtering and sorting apply to loaded objects" ) } </ span >
862+ </ div >
863+ ) : null }
865864
866865 { nextToken ? (
867866 < div ref = { loadMoreRef } className = "flex min-h-10 items-center justify-center text-sm text-muted-foreground" >
0 commit comments