diff --git a/packages/manager/.changeset/pr-10952-tech-stories-1726576261944.md b/packages/manager/.changeset/pr-10952-tech-stories-1726576261944.md new file mode 100644 index 00000000000..ad951701cb3 --- /dev/null +++ b/packages/manager/.changeset/pr-10952-tech-stories-1726576261944.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Optimize AccessSelect component: Use React Hook Form & React Query ([#10952](https://github.com/linode/manager/pull/10952)) diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.test.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.test.tsx index bd7b58082c9..a02a5ea5b1f 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.test.tsx @@ -2,6 +2,7 @@ import { act, fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { AccessSelect } from './AccessSelect'; @@ -11,31 +12,27 @@ import type { ObjectStorageEndpointTypes } from '@linode/api-v4'; const CORS_ENABLED_TEXT = 'CORS Enabled'; const AUTHENTICATED_READ_TEXT = 'Authenticated Read'; +const BUCKET_ACCESS_URL = '*object-storage/buckets/*/*/access'; +const OBJECT_ACCESS_URL = '*object-storage/buckets/*/*/object-acl'; vi.mock('src/components/EnhancedSelect/Select'); -const mockGetAccess = vi.fn().mockResolvedValue({ - acl: 'private', - cors_enabled: true, -}); -const mockUpdateAccess = vi.fn().mockResolvedValue({}); - const defaultProps: Props = { + clusterOrRegion: 'in-maa', endpointType: 'E1', - getAccess: mockGetAccess, name: 'my-object-name', - updateAccess: mockUpdateAccess, variant: 'bucket', }; describe('AccessSelect', () => { const renderComponent = (props: Partial = {}) => - renderWithTheme(); + renderWithTheme(, { + flags: { objectStorageGen2: { enabled: true } }, + }); beforeEach(() => { vi.clearAllMocks(); }); - it.each([ ['bucket', 'E0', true], ['bucket', 'E1', true], @@ -48,13 +45,21 @@ describe('AccessSelect', () => { ])( 'shows correct UI for %s variant and %s endpoint type', async (variant, endpointType, shouldShowCORS) => { + server.use( + http.get(BUCKET_ACCESS_URL, () => { + return HttpResponse.json({ acl: 'private', cors_enabled: true }); + }), + http.get(OBJECT_ACCESS_URL, () => { + return HttpResponse.json({ acl: 'private' }); + }) + ); + renderComponent({ endpointType: endpointType as ObjectStorageEndpointTypes, variant: variant as 'bucket' | 'object', }); const aclSelect = screen.getByRole('combobox'); - await waitFor(() => { expect(aclSelect).toBeEnabled(); expect(aclSelect).toHaveValue('Private'); @@ -69,7 +74,6 @@ describe('AccessSelect', () => { 'aria-selected', 'true' ); - if (shouldShowCORS) { await waitFor(() => { expect(screen.getByLabelText(CORS_ENABLED_TEXT)).toBeInTheDocument(); @@ -92,13 +96,25 @@ describe('AccessSelect', () => { it('updates the access and CORS settings and submits the appropriate values', async () => { renderComponent(); + server.use( + http.get(BUCKET_ACCESS_URL, () => { + return HttpResponse.json({ acl: 'private', cors_enabled: true }); + }), + http.put(BUCKET_ACCESS_URL, () => { + return HttpResponse.json({}); + }) + ); + const aclSelect = screen.getByRole('combobox'); const saveButton = screen.getByText('Save').closest('button')!; - await waitFor(() => { - expect(aclSelect).toBeEnabled(); - expect(aclSelect).toHaveValue('Private'); - }); + await waitFor( + () => { + expect(aclSelect).toBeEnabled(); + expect(aclSelect).toHaveValue('Private'); + }, + { interval: 100, timeout: 5000 } + ); // Wait for CORS toggle to appear and be checked const corsToggle = await screen.findByRole('checkbox', { @@ -131,12 +147,8 @@ describe('AccessSelect', () => { }); await userEvent.click(saveButton); - expect(mockUpdateAccess).toHaveBeenCalledWith('authenticated-read', false); - - await userEvent.click(corsToggle); - await waitFor(() => expect(corsToggle).toBeChecked()); - - await userEvent.click(saveButton); - expect(mockUpdateAccess).toHaveBeenCalledWith('authenticated-read', true); + await waitFor(() => + screen.findByText('Bucket access updated successfully.') + ); }); }); diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx index b3985278092..3663cb4700b 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/AccessSelect.tsx @@ -1,9 +1,8 @@ -import { styled } from '@mui/material/styles'; import * as React from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; -import { Button } from 'src/components/Button/Button'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { FormControlLabel } from 'src/components/FormControlLabel'; import { Link } from 'src/components/Link'; @@ -11,6 +10,12 @@ import { Notice } from 'src/components/Notice/Notice'; import { Toggle } from 'src/components/Toggle/Toggle'; import { Typography } from 'src/components/Typography'; import { useOpenClose } from 'src/hooks/useOpenClose'; +import { + useBucketAccess, + useObjectAccess, + useUpdateBucketAccessMutation, + useUpdateObjectAccessMutation, +} from 'src/queries/object-storage/queries'; import { capitalize } from 'src/utilities/capitalize'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; @@ -22,14 +27,15 @@ import type { ObjectStorageBucketAccess, ObjectStorageEndpointTypes, ObjectStorageObjectACL, + UpdateObjectStorageBucketAccessPayload, } from '@linode/api-v4/lib/object-storage'; import type { Theme } from '@mui/material/styles'; export interface Props { + bucketName?: string; + clusterOrRegion: string; endpointType?: ObjectStorageEndpointTypes; - getAccess: () => Promise; name: string; - updateAccess: (acl: ACLType, cors_enabled?: boolean) => Promise<{}>; variant: 'bucket' | 'object'; } @@ -40,21 +46,8 @@ function isUpdateObjectStorageBucketAccessPayload( } export const AccessSelect = React.memo((props: Props) => { - const { endpointType, getAccess, name, updateAccess, variant } = props; - // Access data for this Object (from the API). - const [aclData, setACLData] = React.useState(null); - const [corsData, setCORSData] = React.useState(true); - const [accessLoading, setAccessLoading] = React.useState(false); - const [accessError, setAccessError] = React.useState(''); - // The ACL Option currently selected in the component. - const [selectedACL, setSelectedACL] = React.useState(null); - // The CORS Option currently selected in the component. - const [selectedCORSOption, setSelectedCORSOption] = React.useState(true); // TODO: OBJGen2 - We need to handle this in upcoming PR - // State for submitting access options. - const [updateAccessLoading, setUpdateAccessLoading] = React.useState(false); - const [updateAccessError, setUpdateAccessError] = React.useState(''); - const [updateAccessSuccess, setUpdateAccessSuccess] = React.useState(false); - // State for dealing with the confirmation modal when selecting read/write. + const { bucketName, clusterOrRegion, endpointType, name, variant } = props; + const { close: closeDialog, isOpen, open: openDialog } = useOpenClose(); const label = capitalize(variant); const isCorsAvailable = @@ -62,59 +55,63 @@ export const AccessSelect = React.memo((props: Props) => { endpointType !== 'E2' && endpointType !== 'E3'; - React.useEffect(() => { - setUpdateAccessError(''); - setAccessError(''); - setUpdateAccessSuccess(false); - setAccessLoading(true); - getAccess() - .then((response) => { - setAccessLoading(false); - const { acl } = response; - // Don't show "public-read-write" for Objects here; use "custom" instead - // since "public-read-write" Objects are basically the same as "public-read". - const _acl = - variant === 'object' && acl === 'public-read-write' ? 'custom' : acl; - setACLData(_acl); - setSelectedACL(_acl); - if (isUpdateObjectStorageBucketAccessPayload(response)) { - const { cors_enabled } = response; - if (typeof cors_enabled === 'boolean') { - setCORSData(cors_enabled); - setSelectedCORSOption(cors_enabled); - } - } - }) - .catch((err) => { - setAccessLoading(false); - setAccessError(getErrorStringOrDefault(err)); - }); - }, [getAccess, variant]); + const { + data: bucketAccessData, + error: bucketAccessError, + isFetching: bucketAccessIsFetching, + } = useBucketAccess(clusterOrRegion, name, variant === 'bucket'); - const handleSubmit = () => { - // TS safety check. - if (!name || !selectedACL) { - return; + const { + data: objectAccessData, + error: objectAccessError, + isFetching: objectAccessIsFetching, + } = useObjectAccess( + bucketName || '', + clusterOrRegion, + { name }, + variant === 'object' + ); + + const { + error: updateBucketAccessError, + isSuccess: updateBucketAccessSuccess, + mutateAsync: updateBucketAccess, + } = useUpdateBucketAccessMutation(clusterOrRegion, name); + + const { + error: updateObjectAccessError, + isSuccess: updateObjectAccessSuccess, + mutateAsync: updateObjectAccess, + } = useUpdateObjectAccessMutation(clusterOrRegion, bucketName || '', name); + + const formValues = React.useMemo(() => { + const data = variant === 'object' ? objectAccessData : bucketAccessData; + + if (data) { + const { acl } = data; + // Don't show "public-read-write" for Objects here; use "custom" instead + // since "public-read-write" Objects are basically the same as "public-read". + const _acl = + variant === 'object' && acl === 'public-read-write' ? 'custom' : acl; + const cors_enabled = isUpdateObjectStorageBucketAccessPayload(data) + ? data.cors_enabled ?? false + : true; + return { acl: _acl as ACLType, cors_enabled }; } + return { acl: 'private' as ACLType, cors_enabled: true }; + }, [bucketAccessData, objectAccessData, , variant]); - setUpdateAccessSuccess(false); - setUpdateAccessLoading(true); - setUpdateAccessError(''); - setAccessError(''); - closeDialog(); + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + watch, + } = useForm>({ + defaultValues: formValues, + values: formValues, + }); - updateAccess(selectedACL, selectedCORSOption) - .then(() => { - setUpdateAccessSuccess(true); - setACLData(selectedACL); - setCORSData(selectedCORSOption); - setUpdateAccessLoading(false); - }) - .catch((err) => { - setUpdateAccessLoading(false); - setUpdateAccessError(getErrorStringOrDefault(err)); - }); - }; + const selectedACL = watch('acl'); const aclOptions = variant === 'bucket' ? bucketACLOptions : objectACLOptions; @@ -127,78 +124,119 @@ export const AccessSelect = React.memo((props: Props) => { // select "public-read-write" as an Object ACL, which is just equivalent to // "public-read", so we don't present it as an option. const _options = - aclData === 'custom' + selectedACL === 'custom' ? [{ label: 'Custom', value: 'custom' }, ...aclOptions] : aclOptions; - const aclLabel = _options.find( - (thisOption) => thisOption.value === selectedACL - )?.label; - + const aclLabel = _options.find((option) => option.value === selectedACL) + ?.label; const aclCopy = selectedACL ? copy[variant][selectedACL] : null; - const errorText = accessError || updateAccessError; + const errorText = + getErrorStringOrDefault(bucketAccessError || '') || + getErrorStringOrDefault(objectAccessError || '') || + getErrorStringOrDefault(updateBucketAccessError || '') || + getErrorStringOrDefault(updateObjectAccessError || '') || + errors.acl?.message; - const CORSLabel = accessLoading - ? 'Loading access...' - : selectedCORSOption - ? 'CORS Enabled' - : 'CORS Disabled'; + const onSubmit = handleSubmit(async (data) => { + closeDialog(); + if (errorText) { + return; + } - const selectedOption = - _options.find((thisOption) => thisOption.value === selectedACL) ?? - _options.find((thisOption) => thisOption.value === 'private'); + if (variant === 'bucket') { + // Don't send the ACL with the payload if it's "custom", since it's + // not valid (though it's a valid return type). + const payload = + data.acl === 'custom' ? { cors_enabled: data.cors_enabled } : data; + await updateBucketAccess(payload); + } else { + await updateObjectAccess(data.acl); + } + }); return ( - <> - {updateAccessSuccess ? ( +
+ {(updateBucketAccessSuccess || updateObjectAccessSuccess) && ( - ) : null} + )} - {errorText ? : null} + {errorText && ( + + )} - { - if (selected) { - setUpdateAccessSuccess(false); - setUpdateAccessError(''); - setSelectedACL(selected.value as ACLType); - } - }} - data-testid="acl-select" - disableClearable - disabled={Boolean(accessError) || accessLoading} - label="Access Control List (ACL)" - loading={accessLoading} - options={!accessLoading ? _options : []} - placeholder={accessLoading ? 'Loading access...' : 'Select an ACL...'} - value={!accessLoading ? selectedOption : undefined} + ( + { + if (selected) { + field.onChange(selected.value); + } + }} + placeholder={ + bucketAccessIsFetching || objectAccessIsFetching + ? 'Loading access...' + : 'Select an ACL...' + } + data-testid="acl-select" + disableClearable + disabled={bucketAccessIsFetching || objectAccessIsFetching} + label="Access Control List (ACL)" + loading={bucketAccessIsFetching || objectAccessIsFetching} + options={_options} + value={_options.find((option) => option.value === field.value)} + /> + )} + control={control} + name="acl" + rules={{ required: 'ACL is required' }} />
- {aclLabel && aclCopy ? ( + {aclLabel && aclCopy && ( {aclLabel}: {aclCopy} - ) : null} + )}
- {isCorsAvailable ? ( - setSelectedCORSOption((prev) => !prev)} + {isCorsAvailable && ( + ( + + } + label={ + bucketAccessIsFetching || objectAccessIsFetching + ? 'Loading access...' + : field.value + ? 'CORS Enabled' + : 'CORS Disabled' + } + style={{ display: 'block', marginTop: 16 }} /> - } - label={CORSLabel} - style={{ display: 'block', marginTop: 16 }} + )} + control={control} + name="cors_enabled" /> - ) : null} + )} {isCorsAvailable ? ( @@ -209,8 +247,7 @@ export const AccessSelect = React.memo((props: Props) => { . - ) : ( - // TODO: OBJGen2 - We need to handle link in upcoming PR + ) : endpointType && variant === 'bucket' ? ( ({ @@ -221,19 +258,19 @@ export const AccessSelect = React.memo((props: Props) => { and E3. Learn more. - )} + ) : null} { - // This isn't really a sane option: open a dialog for confirmation. if (selectedACL === 'public-read-write') { openDialog(); } else { - handleSubmit(); + onSubmit(); } }, sx: (theme: Theme) => ({ @@ -251,7 +288,7 @@ export const AccessSelect = React.memo((props: Props) => { label: 'Cancel', onClick: closeDialog, }} - primaryButtonProps={{ label: 'Confirm', onClick: handleSubmit }} + primaryButtonProps={{ label: 'Confirm', onClick: onSubmit }} style={{ padding: 0 }} /> )} @@ -263,12 +300,6 @@ export const AccessSelect = React.memo((props: Props) => { Everyone will be able to list, create, overwrite, and delete Objects in this Bucket. This is not recommended. - + ); }); - -export const StyledSubmitButton = styled(Button, { - label: 'StyledFileUploadsContainer', -})(({ theme }) => ({ - marginTop: theme.spacing(3), -})); diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketAccess.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketAccess.tsx index 8305b1d5015..096209ce702 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/BucketAccess.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/BucketAccess.tsx @@ -1,7 +1,3 @@ -import { - getBucketAccess, - updateBucketAccess, -} from '@linode/api-v4/lib/object-storage'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -10,10 +6,7 @@ import { Typography } from 'src/components/Typography'; import { AccessSelect } from './AccessSelect'; -import type { - ACLType, - ObjectStorageEndpointTypes, -} from '@linode/api-v4/lib/object-storage'; +import type { ObjectStorageEndpointTypes } from '@linode/api-v4/lib/object-storage'; export const StyledRootContainer = styled(Paper, { label: 'StyledRootContainer', @@ -34,16 +27,8 @@ export const BucketAccess = React.memo((props: Props) => { Bucket Access { - // Don't send the ACL with the payload if it's "custom", since it's - // not valid (though it's a valid return type). - const payload = - acl === 'custom' ? { cors_enabled } : { acl, cors_enabled }; - - return updateBucketAccess(clusterId, bucketName, payload); - }} + clusterOrRegion={clusterId} endpointType={endpointType} - getAccess={() => getBucketAccess(clusterId, bucketName)} name={bucketName} variant="bucket" /> diff --git a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.tsx index 52fc380cfcc..dbb4e111a0c 100644 --- a/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketDetail/ObjectDetailsDrawer.tsx @@ -1,7 +1,3 @@ -import { - getObjectACL, - updateObjectACL, -} from '@linode/api-v4/lib/object-storage'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -17,10 +13,7 @@ import { readableBytes } from 'src/utilities/unitConversions'; import { AccessSelect } from './AccessSelect'; -import type { - ACLType, - ObjectStorageEndpointTypes, -} from '@linode/api-v4/lib/object-storage'; +import type { ObjectStorageEndpointTypes } from '@linode/api-v4/lib/object-storage'; export interface ObjectDetailsDrawerProps { bucketName: string; @@ -93,16 +86,8 @@ export const ObjectDetailsDrawer = React.memo( <> - getObjectACL({ - bucket: bucketName, - clusterId, - params: { name }, - }) - } - updateAccess={(acl: ACLType) => - updateObjectACL(clusterId, bucketName, name, acl) - } + bucketName={bucketName} + clusterOrRegion={clusterId} endpointType={endpointType} name={name} variant="object" diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx index b4ebf39c075..7743e840963 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.tsx @@ -1,7 +1,3 @@ -import { - getBucketAccess, - updateBucketAccess, -} from '@linode/api-v4/lib/object-storage'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -24,10 +20,7 @@ import { readableBytes } from 'src/utilities/unitConversions'; import { AccessSelect } from '../BucketDetail/AccessSelect'; import { BucketRateLimitTable } from './BucketRateLimitTable'; -import type { - ACLType, - ObjectStorageBucket, -} from '@linode/api-v4/lib/object-storage'; +import type { ObjectStorageBucket } from '@linode/api-v4/lib/object-storage'; export interface BucketDetailsDrawerProps { onClose: () => void; @@ -159,28 +152,11 @@ export const BucketDetailsDrawer = React.memo( )} {cluster && label && ( - getBucketAccess( - isObjMultiClusterEnabled && currentRegion - ? currentRegion.id - : cluster, - label - ) + clusterOrRegion={ + isObjMultiClusterEnabled && currentRegion + ? currentRegion.id + : cluster } - updateAccess={(acl: ACLType, cors_enabled: boolean) => { - // Don't send the ACL with the payload if it's "custom", since it's - // not valid (though it's a valid return type). - const payload = - acl === 'custom' ? { cors_enabled } : { acl, cors_enabled }; - - return updateBucketAccess( - isObjMultiClusterEnabled && currentRegion - ? currentRegion.id - : cluster, - label, - payload - ); - }} endpointType={endpoint_type} name={label} variant="bucket" diff --git a/packages/manager/src/queries/object-storage/queries.ts b/packages/manager/src/queries/object-storage/queries.ts index 09ca1d0f534..4b552df609e 100644 --- a/packages/manager/src/queries/object-storage/queries.ts +++ b/packages/manager/src/queries/object-storage/queries.ts @@ -4,10 +4,14 @@ import { deleteBucket, deleteBucketWithRegion, deleteSSLCert, + getBucketAccess, + getObjectACL, getObjectList, getObjectStorageKeys, getObjectURL, getSSLCert, + updateBucketAccess, + updateObjectACL, uploadSSLCert, } from '@linode/api-v4'; import { createQueryKeys } from '@lukemorales/query-key-factory'; @@ -40,20 +44,24 @@ import { prefixToQueryKey } from './utilities'; import type { BucketsResponse, BucketsResponseType } from './requests'; import type { + ACLType, APIError, CreateObjectStorageBucketPayload, CreateObjectStorageBucketSSLPayload, CreateObjectStorageObjectURLPayload, ObjectStorageBucket, + ObjectStorageBucketAccess, ObjectStorageBucketSSL, ObjectStorageCluster, ObjectStorageEndpoint, ObjectStorageKey, + ObjectStorageObjectACL, ObjectStorageObjectList, ObjectStorageObjectURL, Params, PriceType, ResourcePage, + UpdateObjectStorageBucketAccessPayload, } from '@linode/api-v4'; export const objectStorageQueries = createQueryKeys('object-storage', { @@ -63,6 +71,10 @@ export const objectStorageQueries = createQueryKeys('object-storage', { }), bucket: (clusterOrRegion: string, bucketName: string) => ({ contextQueries: { + access: { + queryFn: () => getBucketAccess(clusterOrRegion, bucketName), + queryKey: null, + }, objects: { // This is a placeholder queryFn and QueryKey. View the `useObjectBucketObjectsInfiniteQuery` implementation for details. queryFn: null, @@ -179,6 +191,70 @@ export const useObjectStorageAccessKeys = (params: Params) => placeholderData: keepPreviousData, }); +export const useBucketAccess = ( + clusterOrRegion: string, + bucket: string, + queryEnabled: boolean +) => + useQuery({ + ...objectStorageQueries.bucket(clusterOrRegion, bucket)._ctx.access, + enabled: queryEnabled, + }); + +export const useObjectAccess = ( + bucket: string, + clusterId: string, + params: { name: string }, + queryEnabled: boolean +) => + useQuery({ + enabled: queryEnabled, + queryFn: () => getObjectACL({ bucket, clusterId, params }), + queryKey: [bucket, clusterId, params.name], + }); + +export const useUpdateBucketAccessMutation = ( + clusterOrRegion: string, + bucket: string +) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[], UpdateObjectStorageBucketAccessPayload>({ + mutationFn: (data) => updateBucketAccess(clusterOrRegion, bucket, data), + onSuccess: (_, variables) => { + queryClient.setQueryData( + objectStorageQueries.bucket(clusterOrRegion, bucket)._ctx.access + .queryKey, + (oldData) => ({ + acl: variables?.acl ?? 'private', + acl_xml: oldData?.acl_xml ?? '', + cors_enabled: variables?.cors_enabled ?? null, + cors_xml: oldData?.cors_xml ?? null, + }) + ); + }, + }); +}; + +export const useUpdateObjectAccessMutation = ( + clusterId: string, + bucketName: string, + name: string +) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[], ACLType>({ + mutationFn: (data) => updateObjectACL(clusterId, bucketName, name, data), + onSuccess: (_, acl) => { + queryClient.setQueryData( + [bucketName, clusterId, name], + (oldData) => ({ + acl, + acl_xml: oldData?.acl_xml ?? null, + }) + ); + }, + }); +}; + export const useCreateBucketMutation = () => { const queryClient = useQueryClient(); return useMutation<