Skip to content

Commit

Permalink
upcoming: [M3-7425] - OBJ MultiCluster - Add regions field in Create …
Browse files Browse the repository at this point in the history
…Access Key Flow (#10034)

* Regions Multi Select

* Add Regions field to AccessKeyDrawer

* Added changeset: OBJ MultiCluster - Add regions field in Create Access Key Drawer

* Get all buckets from regions useObjectStorageBucketsFromRegions

* Show validation error when no regions are selected

* Mock service for OBJ create key

* Code cleanup

* Create BucketPermissionsTable

* Implement Copy All

* Mock disable OBJ multicluster feature flag for access key tests

* Create HostNamesDrawer

* code cleanup

* User session feedback

* Update AccessKeyRegions.tsx

* fix broken tests.

* PR - Feedback from @jdamore-linode

* PR - feedback use RemovableSelectionsList to render selected regions

* Code cleanup

* Code cleanup

* Code cleanup

* Remove omc_createObjectStorageKeysSchema as it is not required.

* Fix - remove option

* Code cleanup

* PR feedback - @dwiley-akamai

---------

Co-authored-by: Joe D'Amore <jdamore@linode.com>
  • Loading branch information
cpathipa and Joe D'Amore authored Jan 25, 2024
1 parent e6c5a8e commit f7e327e
Show file tree
Hide file tree
Showing 26 changed files with 1,319 additions and 102 deletions.
17 changes: 17 additions & 0 deletions packages/api-v4/src/object-storage/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ export const getBucketsInCluster = (
)
);

/**
* getBucketsInRegion
*
* Gets a list of a user's Object Storage Buckets in the specified region.
*/
export const getBucketsInRegion = (
regionId: string,
params?: Params,
filters?: Filter
) =>
Request<Page<ObjectStorageBucket>>(
setMethod('GET'),
setParams(params),
setXFilter(filters),
setURL(`${API_ROOT}/object-storage/buckets/${encodeURIComponent(regionId)}`)
);

/**
* createBucket
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

OBJ MultiCluster - Add regions field in Create Access Key Drawer ([#10034](https://github.com/linode/manager/pull/10034))
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
import { objectStorageBucketFactory } from 'src/factories/objectStorage';
import { authenticate } from 'support/api/authentication';
import { createBucket } from '@linode/api-v4/lib/object-storage';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import {
interceptGetAccessKeys,
interceptCreateAccessKey,
} from 'support/intercepts/object-storage';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import { randomLabel } from 'support/util/random';
import { ui } from 'support/ui';
import { cleanUp } from 'support/util/cleanup';
Expand All @@ -32,6 +37,11 @@ describe('object storage access key end-to-end tests', () => {
interceptGetAccessKeys().as('getKeys');
interceptCreateAccessKey().as('createKey');

mockAppendFeatureFlags({
objMultiCluster: makeFeatureFlagData(false),
});
mockGetFeatureFlagClientstream();

cy.visitWithLogin('/object-storage/access-keys');
cy.wait('@getKeys');

Expand Down Expand Up @@ -119,6 +129,11 @@ describe('object storage access key end-to-end tests', () => {
).then(() => {
const keyLabel = randomLabel();

mockAppendFeatureFlags({
objMultiCluster: makeFeatureFlagData(false),
});
mockGetFeatureFlagClientstream();

interceptGetAccessKeys().as('getKeys');
interceptCreateAccessKey().as('createKey');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
*/

import { objectStorageKeyFactory } from 'src/factories/objectStorage';
import {
mockAppendFeatureFlags,
mockGetFeatureFlagClientstream,
} from 'support/intercepts/feature-flags';
import {
mockCreateAccessKey,
mockDeleteAccessKey,
mockGetAccessKeys,
} from 'support/intercepts/object-storage';
import { makeFeatureFlagData } from 'support/util/feature-flags';
import { randomLabel, randomNumber, randomString } from 'support/util/random';
import { ui } from 'support/ui';

Expand All @@ -25,6 +30,11 @@ describe('object storage access keys smoke tests', () => {
secret_key: randomString(39),
});

mockAppendFeatureFlags({
objMultiCluster: makeFeatureFlagData(false),
});
mockGetFeatureFlagClientstream();

mockGetAccessKeys([]).as('getKeys');
mockCreateAccessKey(mockAccessKey).as('createKey');

Expand Down Expand Up @@ -90,6 +100,11 @@ describe('object storage access keys smoke tests', () => {
secret_key: randomString(39),
});

mockAppendFeatureFlags({
objMultiCluster: makeFeatureFlagData(false),
});
mockGetFeatureFlagClientstream();

// Mock initial GET request to include an access key.
mockGetAccessKeys([accessKey]).as('getKeys');
mockDeleteAccessKey(accessKey.id).as('deleteKey');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ describe('Object Storage enrollment', () => {
* - Confirms that consistent pricing information is shown for all regions in the enable modal.
*/
it('can enroll in Object Storage', () => {
mockAppendFeatureFlags({
objMultiCluster: makeFeatureFlagData(false),
});
mockGetFeatureFlagClientstream();

const mockAccountSettings = accountSettingsFactory.build({
managed: false,
object_storage: 'disabled',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,19 @@ import React, { useState } from 'react';

import { regions } from 'src/__data__/regionsData';
import { Box } from 'src/components/Box';
import { Flag } from 'src/components/Flag';
import {
RemovableItem,
RemovableSelectionsList,
} from 'src/components/RemovableSelectionsList/RemovableSelectionsList';
import { SelectedRegionsList } from 'src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList';
import { sortByString } from 'src/utilities/sort-by';

import { RegionMultiSelect } from './RegionMultiSelect';
import { StyledFlagContainer } from './RegionSelect.styles';

import type { RegionMultiSelectProps } from './RegionSelect.types';
import type { Meta, StoryObj } from '@storybook/react';
import type { RegionSelectOption } from 'src/components/RegionSelect/RegionSelect.types';

interface SelectedRegionsProps {
onRemove: (data: RegionSelectOption) => void;
selectedRegions: RegionSelectOption[];
}

interface LabelComponentProps {
selection: RemovableItem;
}

const sortRegionOptions = (a: RegionSelectOption, b: RegionSelectOption) => {
return sortByString(a.label, b.label, 'asc');
};

const LabelComponent = ({ selection }: LabelComponentProps) => {
return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexGrow: 1,
}}
>
<StyledFlagContainer>
<Flag country={selection.data.country} />
</StyledFlagContainer>
{selection.label}
</Box>
);
};

const SelectedRegionsList = ({
onRemove,
selectedRegions,
}: SelectedRegionsProps) => {
const handleRemove = (item: RemovableItem) => {
onRemove(item.data);
};

return (
<RemovableSelectionsList
selectionData={selectedRegions.map((item, index) => {
return { ...item, id: index };
})}
LabelComponent={LabelComponent}
headerText=""
noDataText=""
onRemove={handleRemove}
/>
);
};

export const Default: StoryObj<RegionMultiSelectProps> = {
render: (args) => {
const SelectWrapper = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const regionsAtlanta = regionFactory.buildList(1, {
label: 'Atlanta, GA',
});
interface SelectedRegionsProps {
onRemove: (data: RegionSelectOption) => void;
onRemove: (region: string) => void;
selectedRegions: RegionSelectOption[];
}
const SelectedRegionsList = ({
Expand All @@ -33,7 +33,7 @@ const SelectedRegionsList = ({
{selectedRegions.map((region, index) => (
<li aria-label={region.label} key={index}>
{region.label}
<button onClick={() => onRemove(region)}>Remove</button>
<button onClick={() => onRemove(region.value)}>Remove</button>
</li>
))}
</ul>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => {
[accountAvailability, currentCapability, regions]
);

const handleRemoveOption = (optionToRemove: RegionSelectOption) => {
const handleRemoveOption = (regionToRemove: string) => {
const updatedSelectedOptions = selectedRegions.filter(
(option) => option.value !== optionToRemove.value
(option) => option.value !== regionToRemove
);
const updatedSelectedIds = updatedSelectedOptions.map(
(region) => region.value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from 'react';

import type {
AccountAvailability,
Capabilities,
Expand Down Expand Up @@ -45,7 +47,7 @@ export interface RegionMultiSelectProps
'label' | 'onChange' | 'options'
> {
SelectedRegionsList?: React.ComponentType<{
onRemove: (option: RegionSelectOption) => void;
onRemove: (region: string) => void;
selectedRegions: RegionSelectOption[];
}>;
currentCapability: Capabilities | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import * as React from 'react';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
import { SecretTokenDialog } from 'src/features/Profile/SecretTokenDialog/SecretTokenDialog';
import { useAccountManagement } from 'src/hooks/useAccountManagement';
import { useErrors } from 'src/hooks/useErrors';
import { useFlags } from 'src/hooks/useFlags';
import { useOpenClose } from 'src/hooks/useOpenClose';
import { usePagination } from 'src/hooks/usePagination';
import { useAccountSettings } from 'src/queries/accountSettings';
import { useObjectStorageAccessKeys } from 'src/queries/objectStorage';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';
import {
sendCreateAccessKeyEvent,
sendEditAccessKeyEvent,
Expand All @@ -25,6 +28,7 @@ import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils';

import { AccessKeyDrawer } from './AccessKeyDrawer';
import { AccessKeyTable } from './AccessKeyTable';
import { OMC_AccessKeyDrawer } from './OMC_AccessKeyDrawer';
import { RevokeAccessKeyDialog } from './RevokeAccessKeyDialog';
import ViewPermissionsDrawer from './ViewPermissionsDrawer';
import { MODE, OpenAccessDrawer } from './types';
Expand Down Expand Up @@ -81,6 +85,14 @@ export const AccessKeyLanding = (props: Props) => {
const displayKeysDialog = useOpenClose();
const revokeKeysDialog = useOpenClose();
const viewPermissionsDrawer = useOpenClose();
const flags = useFlags();
const { account } = useAccountManagement();

const isObjMultiClusterEnabled = isFeatureEnabled(
'Object Storage Access Key Regions',
Boolean(flags.objMultiCluster),
account?.capabilities ?? []
);

const handleCreateKey = (
values: ObjectStorageKeyRequest,
Expand Down Expand Up @@ -265,14 +277,26 @@ export const AccessKeyLanding = (props: Props) => {
page={pagination.page}
pageSize={pagination.pageSize}
/>
<AccessKeyDrawer
isRestrictedUser={props.isRestrictedUser}
mode={mode}
objectStorageKey={keyToEdit ? keyToEdit : undefined}
onClose={closeAccessDrawer}
onSubmit={mode === 'creating' ? handleCreateKey : handleEditKey}
open={accessDrawerOpen}
/>
{isObjMultiClusterEnabled ? (
<OMC_AccessKeyDrawer
isRestrictedUser={props.isRestrictedUser}
mode={mode}
objectStorageKey={keyToEdit ? keyToEdit : undefined}
onClose={closeAccessDrawer}
onSubmit={mode === 'creating' ? handleCreateKey : handleEditKey}
open={accessDrawerOpen}
/>
) : (
<AccessKeyDrawer
isRestrictedUser={props.isRestrictedUser}
mode={mode}
objectStorageKey={keyToEdit ? keyToEdit : undefined}
onClose={closeAccessDrawer}
onSubmit={mode === 'creating' ? handleCreateKey : handleEditKey}
open={accessDrawerOpen}
/>
)}

<ViewPermissionsDrawer
objectStorageKey={keyToEdit}
onClose={viewPermissionsDrawer.close}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import * as React from 'react';
import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
import { Hidden } from 'src/components/Hidden';
import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction';
import { useAccountManagement } from 'src/hooks/useAccountManagement';
import { useFlags } from 'src/hooks/useFlags';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import { OpenAccessDrawer } from './types';

Expand All @@ -18,6 +21,15 @@ interface Props {
export const AccessKeyMenu = (props: Props) => {
const { objectStorageKey, openDrawer, openRevokeDialog } = props;

const flags = useFlags();
const { account } = useAccountManagement();

const isObjMultiClusterEnabled = isFeatureEnabled(
'Object Storage Access Key Regions',
Boolean(flags.objMultiCluster),
account?.capabilities ?? []
);

const actions = [
{
onClick: () => {
Expand All @@ -41,21 +53,30 @@ export const AccessKeyMenu = (props: Props) => {

return (
<StyledInlineActionsContainer>
<Hidden mdDown>
{actions.map((thisAction) => (
<InlineMenuAction
actionText={thisAction.title}
key={thisAction.title}
onClick={thisAction.onClick}
/>
))}
</Hidden>
<Hidden mdUp>
{isObjMultiClusterEnabled ? (
<ActionMenu
actionsList={actions}
ariaLabel={`Action menu for Object Storage Key ${props.label}`}
/>
</Hidden>
) : (
<>
<Hidden mdDown>
{actions.map((thisAction) => (
<InlineMenuAction
actionText={thisAction.title}
key={thisAction.title}
onClick={thisAction.onClick}
/>
))}
</Hidden>
<Hidden mdUp>
<ActionMenu
actionsList={actions}
ariaLabel={`Action menu for Object Storage Key ${props.label}`}
/>
</Hidden>
</>
)}
</StyledInlineActionsContainer>
);
};
Expand Down
Loading

0 comments on commit f7e327e

Please sign in to comment.