Skip to content

Commit

Permalink
fix: [M3-7864] - Make the ACL (Object storage) select field carat (^)…
Browse files Browse the repository at this point in the history
… symbol consist with other select fields in the CM. (linode#10286)

* fix: [M3-7864] - Make the ACL (Object storage) select field carat (^) symbol consist with other select fields in the CM.

* Added changeset: Make the ACL (Object storage) select field carat (^) symbol consist with other select fields in the CM.

* Update selected value according to Autocomplete

* Update broken unit test

* Update AccessSelect.test.tsx

* Add e2e tests to view and update Bucket access (ACL)

* Update tests for AccessSelect refactor

* Remove mocking

* Update packages/manager/cypress/e2e/core/objectStorage/object-storage.e2e.spec.ts

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

* Update packages/manager/.changeset/pr-10286-fixed-1710435081738.md

Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>

* Show loading state for ACL select field.

---------

Co-authored-by: Joe D'Amore <jdamore@linode.com>
Co-authored-by: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com>
  • Loading branch information
3 people authored and bnussman committed Apr 4, 2024
1 parent 9a5ff49 commit 221d01d
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 34 deletions.
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-10286-fixed-1710435081738.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Direction of the Bucket Access ACL select field carat with `Autocomplete` ([#10286](https://github.com/linode/manager/pull/10286))
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
interceptDeleteBucket,
interceptGetBuckets,
interceptUploadBucketObjectS3,
interceptGetBucketAccess,
interceptUpdateBucketAccess,
} from 'support/intercepts/object-storage';
import { ui } from 'support/ui';
import { randomLabel } from 'support/util/random';
Expand Down Expand Up @@ -388,4 +390,47 @@ describe('object storage end-to-end tests', () => {
cy.findByText(emptyBucketMessage).should('be.visible');
});
});

/*
* - Confirms that user can update Bucket access.
*/
it('can update bucket access', () => {
const bucketLabel = randomLabel();
const bucketCluster = 'us-southeast-1';
const bucketAccessPage = `/object-storage/buckets/${bucketCluster}/${bucketLabel}/access`;

cy.defer(
setUpBucket(bucketLabel, bucketCluster),
'creating Object Storage bucket'
).then(() => {
interceptGetBucketAccess(bucketLabel, bucketCluster).as(
'getBucketAccess'
);
interceptUpdateBucketAccess(bucketLabel, bucketCluster).as(
'updateBucketAccess'
);
});

// Navigate to new bucket page, upload and delete an object.
cy.visitWithLogin(bucketAccessPage);

cy.wait('@getBucketAccess');

// Make object public, confirm it can be accessed.
cy.findByText('Access Control List (ACL)')
.should('be.visible')
.click()
.type('Public Read');

ui.autocompletePopper
.findByTitle('Public Read')
.should('be.visible')
.click();

ui.button.findByTitle('Save').should('be.visible').click();

cy.wait('@updateBucketAccess');

cy.findByText('Bucket access updated successfully.');
});
});
43 changes: 41 additions & 2 deletions packages/manager/cypress/support/intercepts/object-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
*/

import { sequentialStub } from 'support/stubs/sequential-stub';
import { makeErrorResponse } from 'support/util/errors';
import { apiMatcher } from 'support/util/intercepts';
import { paginateResponse } from 'support/util/paginate';
import { makeResponse } from 'support/util/response';

import type {
ObjectStorageBucket,
ObjectStorageKey,
ObjectStorageCluster,
ObjectStorageKey,
} from '@linode/api-v4';
import { makeErrorResponse } from 'support/util/errors';

/**
* Intercepts GET requests to fetch buckets.
Expand Down Expand Up @@ -439,3 +439,42 @@ export const mockGetClusters = (
paginateResponse(clusters)
);
};

/**
* Intercepts GET request to fetch access information (ACL, CORS) for a given Bucket.
*
*
* @param label - Object storage bucket label.
* @param cluster - Object storage bucket cluster.
* @param data - response data.
* @param statusCode - response status code.
*
* @returns Cypress chainable.
*/
export const interceptGetBucketAccess = (
label: string,
cluster: string
): Cypress.Chainable<null> => {
return cy.intercept(
'GET',
apiMatcher(`object-storage/buckets/${cluster}/${label}/access`)
);
};

/**
* Intercepts PUT request to update access information (ACL, CORS) for a given Bucket.
*
* @param label - Object storage bucket label.
* @param cluster - Object storage bucket cluster.
*
* @returns Cypress chainable.
*/
export const interceptUpdateBucketAccess = (
label: string,
cluster: string
): Cypress.Chainable<null> => {
return cy.intercept(
'PUT',
apiMatcher(`object-storage/buckets/${cluster}/${label}/access`)
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import * as React from 'react';

import { renderWithTheme } from 'src/utilities/testHelpers';
Expand All @@ -11,45 +12,60 @@ const mockGetAccess = vi.fn();
const mockUpdateAccess = vi.fn();

const props: Props = {
getAccess: mockGetAccess.mockResolvedValue({ acl: 'public-read' }),
getAccess: mockGetAccess.mockResolvedValue({ acl: 'private' }),
name: 'my-object-name',
updateAccess: mockUpdateAccess.mockResolvedValue({}),
variant: 'object',
};

describe('AccessSelect', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('shows the access', async () => {
renderWithTheme(<AccessSelect {...props} />);
const aclSelect = screen.getByRole('combobox');

// Confirm that combobox input field value is 'Private'.
await waitFor(() => {
expect(screen.getByText('Public Read')).toHaveAttribute(
'aria-selected',
'true'
);
expect(aclSelect).toBeEnabled();
expect(aclSelect).toHaveValue('Private');
});

// Confirm that 'Private' is selected upon opening the Autocomplete drop-down.
act(() => {
fireEvent.click(aclSelect);
fireEvent.change(aclSelect, { target: { value: 'P' } });
});

expect(screen.getByText('Private').closest('li')!).toHaveAttribute(
'aria-selected',
'true'
);
});

it('updates the access and submits the appropriate value', async () => {
renderWithTheme(<AccessSelect {...props} />);

const aclSelect = screen.getByRole('combobox');
const saveButton = screen.getByText('Save').closest('button')!;

await waitFor(() => {
const aclSelect = screen.getByTestId('select');
fireEvent.change(aclSelect, {
target: { value: 'private' },
});
expect(aclSelect).toBeEnabled();
expect(aclSelect).toHaveValue('Private');
});

expect(screen.getByText('Private')).toHaveAttribute(
'aria-selected',
'true'
);
act(() => {
fireEvent.click(aclSelect);
fireEvent.change(aclSelect, { target: { value: 'Authenticated Read' } });
});

const saveButton = screen.getByText('Save');
fireEvent.click(saveButton);
expect(aclSelect).toHaveValue('Authenticated Read');
userEvent.click(screen.getByText('Authenticated Read'));

expect(mockUpdateAccess).toHaveBeenCalledWith('private', true);
await waitFor(() => {
expect(screen.getByRole('combobox')).toHaveValue('Authenticated Read');
expect(saveButton).toBeEnabled();
});

fireEvent.click(saveButton);
expect(mockUpdateAccess).toHaveBeenCalledWith('authenticated-read', true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { Theme, styled } from '@mui/material/styles';
import * as React from 'react';

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 EnhancedSelect from 'src/components/EnhancedSelect';
import { FormControlLabel } from 'src/components/FormControlLabel';
import { Link } from 'src/components/Link';
import { Notice } from 'src/components/Notice/Notice';
Expand Down Expand Up @@ -128,6 +128,10 @@ export const AccessSelect = React.memo((props: Props) => {
? 'CORS Enabled'
: 'CORS Disabled';

const selectedOption =
_options.find((thisOption) => thisOption.value === selectedACL) ??
_options.find((thisOption) => thisOption.value === 'private');

return (
<>
{updateAccessSuccess ? (
Expand All @@ -139,24 +143,22 @@ export const AccessSelect = React.memo((props: Props) => {

{errorText ? <Notice text={errorText} variant="error" /> : null}

<EnhancedSelect
onChange={(selected) => {
<Autocomplete
onChange={(_, selected) => {
if (selected) {
setUpdateAccessSuccess(false);
setUpdateAccessError('');
setSelectedACL(selected.value as ACLType);
}
}}
value={_options.find(
(thisOption) => thisOption.value === selectedACL ?? 'private'
)}
data-testid="acl-select"
disabled={accessLoading}
isClearable={false}
isLoading={accessLoading}
disableClearable
disabled={Boolean(accessError)}
label="Access Control List (ACL)"
options={_options}
loading={accessLoading}
options={!accessLoading ? _options : []}
placeholder={accessLoading ? 'Loading access...' : 'Select an ACL...'}
value={!accessLoading ? selectedOption : undefined}
/>

<div style={{ marginTop: 8, minHeight: 16 }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
import { styled } from '@mui/material/styles';
import * as React from 'react';

import { Typography } from 'src/components/Typography';
import { Paper } from 'src/components/Paper';
import { Typography } from 'src/components/Typography';

import { AccessSelect } from './AccessSelect';

Expand Down

0 comments on commit 221d01d

Please sign in to comment.