Skip to content

upcoming: [M3-9691] - Update Quotas UI to be for OBJ only #12071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 17 additions & 26 deletions packages/api-v4/src/quotas/types.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { ObjectStorageEndpointTypes } from 'src/object-storage';
import { Region } from 'src/regions';

import type { StorageSymbol } from '../../../utilities/src/helpers/unitConversions';
import type { ObjectStorageEndpointTypes } from 'src/object-storage';
import type { Region } from 'src/regions';
/**
* A Quota is a service used limit that is rated based on service metrics such
* as vCPUs used, instances or storage size.
*/
export interface Quota {
/**
* A unique identifier for the quota.
* Longer explanatory description for the quota.
*/
quota_id: number;
description: string;

/**
* Customer facing label describing the quota.
* The OBJ endpoint type to which this limit applies.
*
* For OBJ limits only.
*/
quota_name: string;
endpoint_type?: ObjectStorageEndpointTypes;

/**
* Longer explanatory description for the quota.
* A unique identifier for the quota.
*/
description: string;
quota_id: number;

/**
* The account-wide limit for this service, measured in units
Expand All @@ -28,33 +30,22 @@ export interface Quota {
quota_limit: number;

/**
* The unit of measurement for this service limit.
* Customer facing label describing the quota.
*/
resource_metric:
| 'instance'
| 'CPU'
| 'GPU'
| 'VPU'
| 'cluster'
| 'node'
| 'bucket'
| 'object'
| 'byte';
quota_name: string;

/**
* The region slug to which this limit applies.
*
* OBJ limits are applied by endpoint, not region.
* This below really just is a `string` type but being verbose helps with reading comprehension.
*/
region_applied?: Region['id'] | 'global';
region_applied?: 'global' | Region['id'];

/**
* The OBJ endpoint type to which this limit applies.
*
* For OBJ limits only.
* The unit of measurement for this service limit.
*/
endpoint_type?: ObjectStorageEndpointTypes;
resource_metric: StorageSymbol;

/**
* The S3 endpoint URL to which this limit applies.
Expand All @@ -81,7 +72,7 @@ export interface QuotaUsage {
*
* This can be null if the user does not have resources for the given Quota Name.
*/
used: number | null;
usage: null | number;
}

export const quotaTypes = {
Expand Down
2 changes: 1 addition & 1 deletion packages/manager/src/factories/quotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export const quotaFactory = Factory.Sync.makeFactory<Quota>({

export const quotaUsageFactory = Factory.Sync.makeFactory<QuotaUsage>({
quota_limit: 50,
used: 25,
usage: 25,
});
162 changes: 52 additions & 110 deletions packages/manager/src/features/Account/Quotas/Quotas.tsx
Original file line number Diff line number Diff line change
@@ -1,130 +1,76 @@
import { quotaTypes } from '@linode/api-v4';
import { useIsGeckoEnabled } from '@linode/shared';
import { Divider, Paper, Select, Stack, Typography } from '@linode/ui';
import { Divider, Notice, Paper, Select, Stack, Typography } from '@linode/ui';
import * as React from 'react';
import { useHistory } from 'react-router-dom';

import { DocsLink } from 'src/components/DocsLink/DocsLink';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
import { useFlags } from 'src/hooks/useFlags';
import { Link } from 'src/components/Link';

import { QuotasTable } from './QuotasTable';
import { useGetLocationsForQuotaService } from './utils';

import type { Quota, QuotaType } from '@linode/api-v4';
import type { Quota } from '@linode/api-v4';
import type { SelectOption } from '@linode/ui';
import type { Theme } from '@mui/material';

export const Quotas = () => {
const flags = useFlags();
const { isGeckoLAEnabled } = useIsGeckoEnabled(
flags.gecko2?.enabled,
flags.gecko2?.la
);
const history = useHistory();
const [selectedService, setSelectedService] = React.useState<
SelectOption<QuotaType>
>({
label: 'Linodes',
value: 'linode',
});
const [selectedLocation, setSelectedLocation] = React.useState<SelectOption<
Quota['region_applied']
> | null>(null);
const locationData = useGetLocationsForQuotaService(selectedService.value);

const serviceOptions = Object.entries(quotaTypes).map(([key, value]) => ({
label: value,
value: key as QuotaType,
}));
const [selectedLocation, setSelectedLocation] =
React.useState<null | SelectOption<Quota['region_applied']>>(null);
const locationData = useGetLocationsForQuotaService('object-storage');

const { regions, s3Endpoints } = locationData;
const { s3Endpoints } = locationData;
const isFetchingLocations =
'isFetchingS3Endpoints' in locationData
? locationData.isFetchingS3Endpoints
: locationData.isFetchingRegions;

// Handlers
const onSelectServiceChange = (
_event: React.SyntheticEvent<Element, Event>,
value: SelectOption<QuotaType>
) => {
setSelectedService(value);
setSelectedLocation(null);
// remove search params
history.push('/account/quotas');
};

return (
<>
<DocumentTitleSegment segment="Quotas" />
<Paper
sx={(theme: Theme) => ({
marginTop: theme.spacing(2),
marginTop: theme.spacingFunction(16),
})}
variant="outlined"
>
<Stack>
<Typography variant="h2">Object Storage</Typography>
<Notice spacingTop={16} variant="info">
<Typography sx={{ py: 0.5 }}>
View your Object Storage quotas by applying the endpoint filter
below.{' '}
<Link to="https://techdocs.akamai.com/cloud-computing/docs/quotas">
Learn more about quotas
</Link>
.
</Typography>
</Notice>
<Stack spacing={1}>
<Select
label="Select a Service"
onChange={onSelectServiceChange}
options={serviceOptions}
placeholder="Select a service"
value={selectedService}
disabled={isFetchingLocations}
label="Object Storage Endpoint"
loading={isFetchingLocations}
onChange={(_event, value) => {
setSelectedLocation({
label: value?.label,
value: value?.value,
});
history.push('/account/quotas');
}}
options={
s3Endpoints?.map((location) => ({
label: location.label,
value: location.value,
})) ?? []
}
placeholder={
isFetchingLocations
? `Loading S3 endpoints...`
: 'Select an Object Storage S3 endpoint'
}
searchable
sx={{ flexGrow: 1, mr: 2 }}
/>
{selectedService.value === 'object-storage' ? (
<Select
onChange={(_event, value) => {
setSelectedLocation({
label: value?.label,
value: value?.value,
});
history.push('/account/quotas');
}}
options={
s3Endpoints?.map((location) => ({
label: location.label,
value: location.value,
})) ?? []
}
placeholder={
isFetchingLocations
? `Loading ${selectedService.label} S3 endpoints...`
: 'Select an Object Storage S3 endpoint'
}
disabled={isFetchingLocations}
label="Object Storage Endpoint"
loading={isFetchingLocations}
searchable
sx={{ flexGrow: 1, mr: 2 }}
/>
) : (
<RegionSelect
onChange={(_event, region) => {
setSelectedLocation({
label: region.label,
value: region.id,
});
history.push('/account/quotas');
}}
placeholder={
isFetchingLocations
? `Loading ${selectedService.label} regions...`
: `Select a region for ${selectedService.label}`
}
currentCapability={undefined}
disableClearable
disabled={isFetchingLocations}
isGeckoLAEnabled={isGeckoLAEnabled}
loading={isFetchingLocations}
noOptionsText={`No resource found for ${selectedService.label}`}
regions={regions ?? []}
sx={{ flexGrow: 1, mr: 2 }}
value={selectedLocation?.value}
/>
)}
</Stack>
<Divider spacingBottom={40} spacingTop={40} />
<Stack
Expand All @@ -133,27 +79,23 @@ export const Quotas = () => {
marginBottom={2}
>
<Typography variant="h3">Quotas</Typography>
<Stack
sx={(theme) => ({
position: 'relative',
top: `-${theme.spacing(2)}`,
})}
alignItems="center"
direction="row"
spacing={3}
>
{/* TODO LIMITS_M1: update once link is available */}
<DocsLink href="#" label="Learn More About Quotas" />
</Stack>
</Stack>
<Typography>
This table shows quotas and usage. If you need to increase a quota,
select <strong>Request an Increase</strong> from the Actions menu.
select Request Increase from the Actions menu. Usage can also be
found using the{' '}
<Link to="https://techdocs.akamai.com/cloud-computing/docs/use-s3cmd-with-object-storage">
S3 APIs
</Link>
.
</Typography>
<Stack direction="column" spacing={2}>
<QuotasTable
selectedLocation={selectedLocation}
selectedService={selectedService}
selectedService={{
label: 'Object Storage',
value: 'object-storage',
}}
/>
</Stack>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@
it('should render', () => {
const { getByRole, getByTestId, getByText } = renderWithTheme(
<QuotasTable
selectedLocation={null}
selectedService={{
label: 'Linodes',
value: 'linode',
}}
selectedLocation={null}
/>
);
expect(
Expand All @@ -72,7 +72,7 @@
];
const quotaUsage = quotaUsageFactory.build({
quota_limit: 100,
used: 10,
usage: 10,
});
queryMocks.useQueries.mockReturnValue([
{
Expand Down Expand Up @@ -109,13 +109,13 @@

const quota = quotas[0];

await waitFor(() => {

Check failure on line 112 in packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx

View workflow job for this annotation

GitHub Actions / test-manager

src/features/Account/Quotas/QuotasTable.test.tsx > QuotasTable > should render a table with the correct data

TestingLibraryElementError: Unable to find an element with the text: 100. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible. Ignored nodes: comments, script, style <body> <div> <div class="css-13rlpmx-StyledTableWrapper" > <table class="MuiTable-root css-brclvr-MuiTable-root" role="table" > <thead class="MuiTableHead-root css-1a7iywq-MuiTableHead-root" > <tr class="MuiTableRow-root MuiTableRow-hover MuiTableRow-head css-1yom1d6-MuiTableRow-root-StyledTableRow" > <th class="MuiTableCell-root MuiTableCell-head MuiTableCell-sizeMedium css-ty1gt5-MuiTableCell-root" scope="col" > Quota Name </th> <th class="MuiTableCell-root MuiTableCell-head MuiTableCell-sizeMedium css-1uv7ss8-MuiTableCell-root" scope="col" > Account Quota Value </th> <th class="MuiTableCell-root MuiTableCell-head MuiTableCell-sizeMedium css-1nue31s-MuiTableCell-root" scope="col" > Usage </th> <th class="MuiTableCell-root MuiTableCell-head MuiTableCell-sizeMedium css-ht2z7x-MuiTableCell-root" scope="col" /> </tr> </thead> <tbody class="MuiTableBody-root css-gmh7jj-MuiTableBody-root" > <tr class="MuiTableRow-root MuiTableRow-hover css-d2ua2g-MuiTableRow-root-StyledTableRow" > <td class="MuiTableCell-root MuiTableCell-body MuiTableCell-sizeMedium css-gc3b4u-MuiTableCell-root" > <div class="MuiBox-root css-br0te4" > <p class="MuiTypography-root MuiTypography-body1 css-11ae3d0-MuiTypography-root" > Random Quota </p> <button aria-label="Random Quota Description" class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeLarge css-1fslen1-MuiButtonBase-root-MuiIconButton-root" data-mui-internal-clone-element="true" data-qa-help-button="true" data-qa-help-tooltip="true" data-qa-tooltip="Random Quota Description" tabindex="0" type="button" > <svg aria-hidden="true" class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-1fi7mzk-MuiSvgIcon-root" data-testid="HelpOutlineIcon" focusable="false" viewBox="0 0 24 24" > <path d="M11 18h2v-2h-2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8m0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4" /> </svg> </button> </div> </td> <td class="MuiTableCell-root MuiTableCell-body MuiTableCell-sizeMedium css-gc3b4u-MuiTableCell-root" > 100 CPU s </td> <td class="MuiTableCell-root MuiTableCell-body MuiTableCell-sizeMedium css-gc3b4u-MuiTableCell-root" > <div class="MuiBox-root css-1vunpsv" > <div class="css-1kd096k" > <span aria-valuemax="100" aria-valuemin="0" aria-valuenow="10" class="MuiLinearProgress-root MuiLinearProgr
expect(getByText(quota.quota_name)).toBeInTheDocument();
expect(getByText(quota.quota_limit)).toBeInTheDocument();
expect(getByLabelText(quota.description)).toBeInTheDocument();
expect(getByTestId('linear-progress')).toBeInTheDocument();
expect(

Check warning on line 117 in packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid using multiple assertions within `waitFor` callback Raw Output: {"ruleId":"testing-library/no-wait-for-multiple-assertions","severity":1,"message":"Avoid using multiple assertions within `waitFor` callback","line":117,"column":7,"nodeType":"ExpressionStatement","messageId":"noWaitForMultipleAssertion","endLine":119,"endColumn":29}
getByText(`${quotaUsage.used} of ${quotaUsage.quota_limit} CPUs used`)
getByText(`${quotaUsage.usage} of ${quotaUsage.quota_limit} CPUs used`)

Check warning on line 118 in packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid destructuring queries from `render` result, use `screen.getByText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByText` instead","line":118,"column":9,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":118,"endColumn":18}

Check warning on line 118 in packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found Raw Output: {"ruleId":"testing-library/prefer-implicit-assert","severity":1,"message":"Don't wrap `getBy*` query with `expect` & presence matchers like `toBeInTheDocument` or `not.toBeNull` as `getBy*` queries fail implicitly when element is not found","line":118,"column":9,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":118,"endColumn":18}
).toBeInTheDocument();
expect(
getByLabelText(`Action menu for quota ${quota.quota_name}`)
Expand Down
12 changes: 6 additions & 6 deletions packages/manager/src/features/Account/Quotas/QuotasTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { AttachmentError } from 'src/features/Support/SupportTicketDetail/S
const quotaRowMinHeight = 58;

interface QuotasTableProps {
selectedLocation: SelectOption<Quota['region_applied']> | null;
selectedLocation: null | SelectOption<Quota['region_applied']>;
selectedService: SelectOption<QuotaType>;
}

Expand Down Expand Up @@ -96,7 +96,7 @@ export const QuotasTable = (props: QuotasTableProps) => {
<>
<Table
sx={(theme) => ({
marginTop: theme.spacing(2),
marginTop: theme.spacingFunction(16),
minWidth: theme.breakpoints.values.sm,
})}
>
Expand All @@ -112,7 +112,7 @@ export const QuotasTable = (props: QuotasTableProps) => {
{hasSelectedLocation && isFetchingQuotas ? (
<TableRowLoading
columns={4}
rows={5}
rows={3}
sx={{ height: quotaRowMinHeight }}
/>
) : !selectedLocation ? (
Expand All @@ -129,7 +129,7 @@ export const QuotasTable = (props: QuotasTableProps) => {
/>
) : (
quotasWithUsage.map((quota, index) => {
const hasQuotaUsage = quota.usage?.used !== null;
const hasQuotaUsage = quota.usage?.usage !== null;

return (
<QuotasTableRow
Expand Down Expand Up @@ -159,13 +159,13 @@ export const QuotasTable = (props: QuotasTableProps) => {
)}

<Dialog
onClose={() => setSupportModalOpen(false)}
open={supportModalOpen}
sx={{
'& .MuiDialog-paper': {
width: '600px',
},
}}
onClose={() => setSupportModalOpen(false)}
open={supportModalOpen}
title="Increase Quota"
>
{selectedQuota && (
Expand Down
Loading
Loading