mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-09-04 11:15:42 +00:00
refactor: optimize state management and filtering in AccessControl and ObjectsTable components
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useMemo, useState} from 'react';
|
||||||
import {useNavigate} from 'react-router-dom';
|
import {useNavigate} from 'react-router-dom';
|
||||||
import {Badge} from '@/components/ui/badge';
|
import {Badge} from '@/components/ui/badge';
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
@@ -64,7 +64,6 @@ export function ObjectsTable({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [sortColumn, setSortColumn] = useState<SortColumn>('name');
|
const [sortColumn, setSortColumn] = useState<SortColumn>('name');
|
||||||
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
const [sortDirection, setSortDirection] = useState<SortDirection>('asc');
|
||||||
const [filteredObjects, setFilteredObjects] = useState<S3Object[]>([]);
|
|
||||||
// Store tokens for each page: [undefined (page 1), token1 (page 2), token2 (page 3), ...]
|
// Store tokens for each page: [undefined (page 1), token1 (page 2), token2 (page 3), ...]
|
||||||
const [pageTokens, setPageTokens] = useState<(string | undefined)[]>([undefined]);
|
const [pageTokens, setPageTokens] = useState<(string | undefined)[]>([undefined]);
|
||||||
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
||||||
@@ -86,14 +85,13 @@ export function ObjectsTable({
|
|||||||
}
|
}
|
||||||
}, [initialized, initialPageToken, initialItemsPerPage, itemsPerPage, nextContinuationToken, onPageChange, onItemsPerPageChange]);
|
}, [initialized, initialPageToken, initialItemsPerPage, itemsPerPage, nextContinuationToken, onPageChange, onItemsPerPageChange]);
|
||||||
|
|
||||||
const sortObjects = (objList: S3Object[]): S3Object[] => {
|
const filteredObjects = useMemo(() => {
|
||||||
const sorted = [...objList].sort((a, b) => {
|
const query = searchQuery.toLowerCase();
|
||||||
// Always put folders before files
|
const filtered = objects.filter((obj) => obj.key.toLowerCase().includes(query));
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
const aIsFolder = a.isFolder ? 1 : 0;
|
const aIsFolder = a.isFolder ? 1 : 0;
|
||||||
const bIsFolder = b.isFolder ? 1 : 0;
|
const bIsFolder = b.isFolder ? 1 : 0;
|
||||||
if (aIsFolder !== bIsFolder) {
|
if (aIsFolder !== bIsFolder) return bIsFolder - aIsFolder;
|
||||||
return bIsFolder - aIsFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
let compareValue = 0;
|
let compareValue = 0;
|
||||||
switch (sortColumn) {
|
switch (sortColumn) {
|
||||||
@@ -116,20 +114,7 @@ export function ObjectsTable({
|
|||||||
|
|
||||||
return sortDirection === 'asc' ? compareValue : -compareValue;
|
return sortDirection === 'asc' ? compareValue : -compareValue;
|
||||||
});
|
});
|
||||||
|
}, [objects, searchQuery, sortColumn, sortDirection, currentPath]);
|
||||||
return sorted;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Effect 1: Apply client-side filtering and sorting (NO pagination reset)
|
|
||||||
useEffect(() => {
|
|
||||||
const filtered = objects.filter((obj) =>
|
|
||||||
obj.key.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
const sorted = sortObjects(filtered);
|
|
||||||
setFilteredObjects(sorted);
|
|
||||||
|
|
||||||
// Do NOT reset pagination - search/sort are client-side operations
|
|
||||||
}, [searchQuery, objects, sortColumn, sortDirection]);
|
|
||||||
|
|
||||||
// Effect 2: Reset pagination ONLY on path navigation
|
// Effect 2: Reset pagination ONLY on path navigation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -192,6 +177,7 @@ export function ObjectsTable({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
<TooltipProvider>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -304,55 +290,53 @@ export function ObjectsTable({
|
|||||||
{obj.lastModified ? (() => {
|
{obj.lastModified ? (() => {
|
||||||
const d = new Date(obj.lastModified);
|
const d = new Date(obj.lastModified);
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<div className="decoration-dashed decoration-1 underline underline-offset-6 cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
|
||||||
<div className="decoration-dashed decoration-1 underline underline-offset-6 cursor-pointer text-muted-foreground hover:text-foreground transition-colors">
|
{d.toLocaleDateString('en-GB', {
|
||||||
{d.toLocaleDateString('en-GB', {
|
day: '2-digit',
|
||||||
day: '2-digit',
|
month: 'short',
|
||||||
month: 'short',
|
year: 'numeric',
|
||||||
year: 'numeric',
|
})} {d.toLocaleTimeString('en-GB', {
|
||||||
})} {d.toLocaleTimeString('en-GB', {
|
hour: '2-digit',
|
||||||
hour: '2-digit',
|
minute: '2-digit',
|
||||||
minute: '2-digit',
|
second: '2-digit',
|
||||||
second: '2-digit',
|
hour12: false,
|
||||||
hour12: false,
|
})} CET
|
||||||
})} CET
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="space-y-1 min-w-max">
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<span className="text-sm text-gray-400 w-20 text-right">UTC</span>
|
||||||
|
<span className="text-sm text-white">
|
||||||
|
{d.toLocaleString('en-GB', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
timeZone: 'UTC',
|
||||||
|
})} UTC
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
<div className="flex gap-3 items-center">
|
||||||
<TooltipContent>
|
<span className="text-sm text-gray-400 w-20 text-right">Relative</span>
|
||||||
<div className="space-y-1 min-w-max">
|
<span className="text-sm text-white">
|
||||||
<div className="flex gap-3 items-center">
|
{formatRelativeTime(d)}
|
||||||
<span className="text-sm text-gray-400 w-20 text-right">UTC</span>
|
</span>
|
||||||
<span className="text-sm text-white">
|
|
||||||
{d.toLocaleString('en-GB', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: false,
|
|
||||||
timeZone: 'UTC',
|
|
||||||
})} UTC
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<span className="text-sm text-gray-400 w-20 text-right">Relative</span>
|
|
||||||
<span className="text-sm text-white">
|
|
||||||
{formatRelativeTime(d)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<span className="text-sm text-gray-400 w-20 text-right">Timestamp</span>
|
|
||||||
<span className="text-sm text-white font-mono">
|
|
||||||
{d.toISOString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipContent>
|
<div className="flex gap-3 items-center">
|
||||||
</Tooltip>
|
<span className="text-sm text-gray-400 w-20 text-right">Timestamp</span>
|
||||||
</TooltipProvider>
|
<span className="text-sm text-white font-mono">
|
||||||
|
{d.toISOString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})() : null}
|
})() : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -390,6 +374,7 @@ export function ObjectsTable({
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* Pagination Controls */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { objectsApi } from '@/lib/api';
|
import { objectsApi } from '@/lib/api';
|
||||||
import type { S3Object, UploadTask } from '@/types';
|
import type { S3Object, UploadTask } from '@/types';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -13,8 +13,9 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
const [nextContinuationToken, setNextContinuationToken] = useState<string | undefined>(undefined);
|
const [nextContinuationToken, setNextContinuationToken] = useState<string | undefined>(undefined);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(25);
|
const [itemsPerPage, setItemsPerPage] = useState(25);
|
||||||
const [currentContinuationToken, setCurrentContinuationToken] = useState<string | undefined>(undefined);
|
const [currentContinuationToken, setCurrentContinuationToken] = useState<string | undefined>(undefined);
|
||||||
const [previousPath, setPreviousPath] = useState<string>(currentPath);
|
const previousPathRef = useRef<string>(currentPath);
|
||||||
const [uploadTasks, setUploadTasks] = useState<UploadTask[]>([]);
|
const [uploadTasks, setUploadTasks] = useState<UploadTask[]>([]);
|
||||||
|
const clearTasksTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const fetchObjects = useCallback(async (continuationToken?: string, isRefresh = false, isNav = false) => {
|
const fetchObjects = useCallback(async (continuationToken?: string, isRefresh = false, isNav = false) => {
|
||||||
if (!bucketName) return;
|
if (!bucketName) return;
|
||||||
@@ -46,24 +47,26 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bucketName) return;
|
if (!bucketName) return;
|
||||||
|
|
||||||
// Detect if this is a path change (navigation) or initial load
|
const isPathChange = previousPathRef.current !== currentPath && objects.length > 0;
|
||||||
const isPathChange = previousPath !== currentPath && objects.length > 0;
|
previousPathRef.current = currentPath;
|
||||||
setPreviousPath(currentPath);
|
|
||||||
|
|
||||||
// Use navigation mode if it's a path change, otherwise use normal loading
|
|
||||||
fetchObjects(undefined, false, isPathChange);
|
fetchObjects(undefined, false, isPathChange);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [bucketName, currentPath, itemsPerPage]);
|
}, [bucketName, currentPath, itemsPerPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (clearTasksTimerRef.current) clearTimeout(clearTasksTimerRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const uploadFiles = useCallback(async (files: File[]) => {
|
const uploadFiles = useCallback(async (files: File[]) => {
|
||||||
if (!bucketName) return false;
|
if (!bucketName) return false;
|
||||||
|
|
||||||
// Check if files are from a folder upload
|
const hasRelativePaths = files.some((file) => !!file.webkitRelativePath);
|
||||||
const hasRelativePaths = files.some((file: any) => file.webkitRelativePath);
|
|
||||||
|
|
||||||
// Get unique folders from the files
|
|
||||||
const folders = new Set<string>();
|
const folders = new Set<string>();
|
||||||
files.forEach((file: any) => {
|
files.forEach((file) => {
|
||||||
if (file.webkitRelativePath) {
|
if (file.webkitRelativePath) {
|
||||||
const parts = file.webkitRelativePath.split('/');
|
const parts = file.webkitRelativePath.split('/');
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) {
|
||||||
@@ -72,9 +75,8 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize upload tasks
|
|
||||||
const tasks: UploadTask[] = files.map((file, index) => {
|
const tasks: UploadTask[] = files.map((file, index) => {
|
||||||
const relativePath = (file as any).webkitRelativePath || file.name;
|
const relativePath = file.webkitRelativePath || file.name;
|
||||||
const key = currentPath ? `${currentPath}${relativePath}` : relativePath;
|
const key = currentPath ? `${currentPath}${relativePath}` : relativePath;
|
||||||
return {
|
return {
|
||||||
id: `${Date.now()}-${index}`,
|
id: `${Date.now()}-${index}`,
|
||||||
@@ -88,53 +90,36 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
|
|
||||||
setUploadTasks(tasks);
|
setUploadTasks(tasks);
|
||||||
|
|
||||||
// Upload files with progress tracking and error handling
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
|
|
||||||
// Upload files one by one
|
for (const task of tasks) {
|
||||||
const concurrency = 1;
|
try {
|
||||||
const uploadPromises: Promise<void>[] = [];
|
setUploadTasks(prev => prev.map(t =>
|
||||||
|
t.id === task.id ? { ...t, status: 'uploading' as const } : t
|
||||||
|
));
|
||||||
|
|
||||||
for (let i = 0; i < tasks.length; i += concurrency) {
|
await objectsApi.upload(bucketName, task.key, task.file, (progress) => {
|
||||||
const batch = tasks.slice(i, Math.min(i + concurrency, tasks.length));
|
setUploadTasks(prev => prev.map(t => {
|
||||||
|
if (t.id !== task.id || t.progress === progress) return t;
|
||||||
|
return { ...t, progress };
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
const batchPromises = batch.map(async (task) => {
|
setUploadTasks(prev => prev.map(t =>
|
||||||
try {
|
t.id === task.id ? { ...t, status: 'completed' as const, progress: 100 } : t
|
||||||
// Update task status to uploading
|
));
|
||||||
setUploadTasks(prev => prev.map(t =>
|
successCount++;
|
||||||
t.id === task.id ? { ...t, status: 'uploading' as const } : t
|
} catch (error) {
|
||||||
));
|
const errorMessage = error instanceof Error ? error.message : 'Upload failed';
|
||||||
|
setUploadTasks(prev => prev.map(t =>
|
||||||
await objectsApi.upload(bucketName, task.key, task.file, (progress) => {
|
t.id === task.id ? { ...t, status: 'error' as const, error: errorMessage } : t
|
||||||
setUploadTasks(prev => prev.map(t =>
|
));
|
||||||
t.id === task.id ? { ...t, progress } : t
|
errorCount++;
|
||||||
));
|
console.error(`Failed to upload ${task.key}:`, error);
|
||||||
});
|
}
|
||||||
|
|
||||||
// Update task status to completed
|
|
||||||
setUploadTasks(prev => prev.map(t =>
|
|
||||||
t.id === task.id ? { ...t, status: 'completed' as const, progress: 100 } : t
|
|
||||||
));
|
|
||||||
successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
// Update task status to error but continue with other uploads
|
|
||||||
const errorMessage = error instanceof Error ? error.message : 'Upload failed';
|
|
||||||
setUploadTasks(prev => prev.map(t =>
|
|
||||||
t.id === task.id ? { ...t, status: 'error' as const, error: errorMessage } : t
|
|
||||||
));
|
|
||||||
errorCount++;
|
|
||||||
console.error(`Failed to upload ${task.key}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadPromises.push(...batchPromises);
|
|
||||||
await Promise.all(batchPromises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(uploadPromises);
|
|
||||||
|
|
||||||
// Show summary toast
|
|
||||||
if (errorCount === 0) {
|
if (errorCount === 0) {
|
||||||
if (hasRelativePaths && folders.size > 0) {
|
if (hasRelativePaths && folders.size > 0) {
|
||||||
const folderNames = Array.from(folders).join(', ');
|
const folderNames = Array.from(folders).join(', ');
|
||||||
@@ -148,9 +133,10 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
toast.error(`Failed to upload ${errorCount} file${errorCount > 1 ? 's' : ''}`);
|
toast.error(`Failed to upload ${errorCount} file${errorCount > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear upload tasks after a delay
|
if (clearTasksTimerRef.current) clearTimeout(clearTasksTimerRef.current);
|
||||||
setTimeout(() => {
|
clearTasksTimerRef.current = setTimeout(() => {
|
||||||
setUploadTasks([]);
|
setUploadTasks([]);
|
||||||
|
clearTasksTimerRef.current = null;
|
||||||
}, 3000);
|
}, 3000);
|
||||||
|
|
||||||
await fetchObjects(currentContinuationToken, true);
|
await fetchObjects(currentContinuationToken, true);
|
||||||
@@ -161,7 +147,6 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
if (!bucketName) return false;
|
if (!bucketName) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistically remove the object from the UI
|
|
||||||
setObjects(prev => prev.filter(obj => obj.key !== key));
|
setObjects(prev => prev.filter(obj => obj.key !== key));
|
||||||
|
|
||||||
await objectsApi.delete(bucketName, key);
|
await objectsApi.delete(bucketName, key);
|
||||||
@@ -170,7 +155,6 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete object error:', error);
|
console.error('Delete object error:', error);
|
||||||
// Revert the optimistic update by refetching
|
|
||||||
await fetchObjects(currentContinuationToken, true);
|
await fetchObjects(currentContinuationToken, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -180,7 +164,6 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
if (!bucketName || keys.length === 0) return false;
|
if (!bucketName || keys.length === 0) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistically remove the objects from the UI
|
|
||||||
setObjects(prev => prev.filter(obj => !keys.includes(obj.key)));
|
setObjects(prev => prev.filter(obj => !keys.includes(obj.key)));
|
||||||
|
|
||||||
await objectsApi.deleteMultiple(bucketName, keys, currentPath || undefined);
|
await objectsApi.deleteMultiple(bucketName, keys, currentPath || undefined);
|
||||||
@@ -189,7 +172,6 @@ export function useBucketObjects(bucketName: string | null, currentPath: string
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Bulk delete error:', error);
|
console.error('Bulk delete error:', error);
|
||||||
// Revert the optimistic update by refetching
|
|
||||||
await fetchObjects(currentContinuationToken, true);
|
await fetchObjects(currentContinuationToken, true);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useMemo, useState} from 'react';
|
||||||
import {Header} from '@/components/layout/header';
|
import {Header} from '@/components/layout/header';
|
||||||
import {Button} from '@/components/ui/button';
|
import {Button} from '@/components/ui/button';
|
||||||
import {Input} from '@/components/ui/input';
|
import {Input} from '@/components/ui/input';
|
||||||
@@ -31,7 +31,6 @@ import {toast} from 'sonner';
|
|||||||
|
|
||||||
export function AccessControl() {
|
export function AccessControl() {
|
||||||
const [keys, setKeys] = useState<AccessKey[]>([]);
|
const [keys, setKeys] = useState<AccessKey[]>([]);
|
||||||
const [filteredKeys, setFilteredKeys] = useState<AccessKey[]>([]);
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
@@ -83,7 +82,6 @@ export function AccessControl() {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await accessApi.listKeys();
|
const data = await accessApi.listKeys();
|
||||||
setKeys(data);
|
setKeys(data);
|
||||||
setFilteredKeys(data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch keys:', error);
|
console.error('Failed to fetch keys:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -94,14 +92,15 @@ export function AccessControl() {
|
|||||||
fetchKeys();
|
fetchKeys();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredKeys = useMemo(
|
||||||
const filtered = keys.filter(
|
() =>
|
||||||
(key) =>
|
keys.filter(
|
||||||
key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(key) =>
|
||||||
key.accessKeyId.toLowerCase().includes(searchQuery.toLowerCase())
|
key.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
);
|
key.accessKeyId.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
setFilteredKeys(filtered);
|
),
|
||||||
}, [searchQuery, keys]);
|
[keys, searchQuery]
|
||||||
|
);
|
||||||
|
|
||||||
const handleCreateKey = async () => {
|
const handleCreateKey = async () => {
|
||||||
if (!newKeyName) {
|
if (!newKeyName) {
|
||||||
|
|||||||
Reference in New Issue
Block a user