Skip to content

upcoming: [M3-9691] - Quotas for Object Storage #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

Merged
merged 14 commits into from
Apr 30, 2025
Merged
42 changes: 16 additions & 26 deletions packages/api-v4/src/quotas/types.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { ObjectStorageEndpointTypes } from 'src/object-storage';
import { Region } from 'src/regions';

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 +29,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: string;

/**
* The S3 endpoint URL to which this limit applies.
Expand All @@ -81,7 +71,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;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main change here - APIv4 gave us a different key than the expected one from the specs. This will be reflected through this PR


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,
});
119 changes: 42 additions & 77 deletions packages/manager/src/features/Account/Quotas/Quotas.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { regionFactory } from '@linode/utilities';
import { QueryClient } from '@tanstack/react-query';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
Expand All @@ -12,12 +11,9 @@
getQuotasFilters: vi.fn().mockReturnValue({}),
useFlags: vi.fn().mockReturnValue({}),
useGetLocationsForQuotaService: vi.fn().mockReturnValue({}),
useGetRegionsQuery: vi.fn().mockReturnValue({}),
}));

vi.mock('@linode/queries', async (importOriginal) => ({
...(await importOriginal()),
useRegionsQuery: queryMocks.useGetRegionsQuery,
useObjectStorageEndpoints: vi.fn().mockReturnValue({}),
convertResourceMetric: vi.fn().mockReturnValue({}),
pluralizeMetric: vi.fn().mockReturnValue({}),
}));

vi.mock('src/hooks/useFlags', () => {
Expand All @@ -28,28 +24,22 @@
};
});

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useObjectStorageEndpoints: queryMocks.useObjectStorageEndpoints,
};
});

vi.mock('./utils', () => ({
getQuotasFilters: queryMocks.getQuotasFilters,
useGetLocationsForQuotaService: queryMocks.useGetLocationsForQuotaService,
convertResourceMetric: queryMocks.convertResourceMetric,
pluralizeMetric: queryMocks.pluralizeMetric,
}));

describe('Quotas', () => {
beforeEach(() => {
queryMocks.useGetLocationsForQuotaService.mockReturnValue({
isFetchingRegions: false,
regions: [
regionFactory.build({ id: 'global', label: 'Global (Account level)' }),
],
});
queryMocks.useGetRegionsQuery.mockReturnValue({
data: [
regionFactory.build({ id: 'global', label: 'Global (Account level)' }),
regionFactory.build({ id: 'us-east', label: 'Newark, NJ' }),
],
isFetching: false,
});
});

const queryClient = new QueryClient({
defaultOptions: {
queries: {
Expand All @@ -64,89 +54,64 @@
});

expect(getByText('Quotas')).toBeInTheDocument();
expect(getByText('Learn More About Quotas')).toBeInTheDocument();
expect(getByText('Select a Service')).toBeInTheDocument();
expect(getByText('Learn more about quotas')).toBeInTheDocument();

Check warning on line 57 in packages/manager/src/features/Account/Quotas/Quotas.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":57,"column":12,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":57,"endColumn":21}

Check warning on line 57 in packages/manager/src/features/Account/Quotas/Quotas.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":57,"column":12,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":57,"endColumn":21}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those test linting rules will get straightened out soon

expect(getByText('Object Storage Endpoint')).toBeInTheDocument();

Check warning on line 58 in packages/manager/src/features/Account/Quotas/Quotas.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":58,"column":12,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":58,"endColumn":21}

Check warning on line 58 in packages/manager/src/features/Account/Quotas/Quotas.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":58,"column":12,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":58,"endColumn":21}
expect(
screen.getByPlaceholderText('Select an Object Storage S3 endpoint')

Check warning on line 60 in packages/manager/src/features/Account/Quotas/Quotas.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":60,"column":7,"nodeType":"MemberExpression","messageId":"preferImplicitAssert","endLine":60,"endColumn":34}
).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Select a region for Linodes')
getByText('Apply filters above to see quotas and current usage.')

Check warning on line 63 in packages/manager/src/features/Account/Quotas/Quotas.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":63,"column":7,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":63,"endColumn":16}

Check warning on line 63 in packages/manager/src/features/Account/Quotas/Quotas.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":63,"column":7,"nodeType":"Identifier","messageId":"preferImplicitAssert","endLine":63,"endColumn":16}
).toBeInTheDocument();
});

it('allows service selection', async () => {
it('allows endpoint selection', async () => {
queryMocks.useGetLocationsForQuotaService.mockReturnValue({
isFetchingS3Endpoints: false,
regions: null,
s3Endpoints: [{ label: 'endpoint1 (Standard E0)', value: 'endpoint1' }],

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

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Define a constant instead of duplicating this literal 3 times. Raw Output: {"ruleId":"sonarjs/no-duplicate-string","severity":1,"message":"Define a constant instead of duplicating this literal 3 times.","line":71,"column":30,"nodeType":"Literal","endLine":71,"endColumn":55}
service: 'object-storage',
});

const { getByPlaceholderText, getByRole } = renderWithTheme(<Quotas />, {
queryClient,
});

const serviceSelect = getByPlaceholderText('Select a service');

await waitFor(() => {
expect(serviceSelect).toHaveValue('Linodes');
expect(
getByPlaceholderText('Select a region for Linodes')
).toBeInTheDocument();
});
const endpointSelect = getByPlaceholderText(

Check warning on line 79 in packages/manager/src/features/Account/Quotas/Quotas.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.getByPlaceholderText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByPlaceholderText` instead","line":79,"column":28,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":79,"endColumn":48}
'Select an Object Storage S3 endpoint'
);

userEvent.click(serviceSelect);
await waitFor(() => {
const kubernetesOption = getByRole('option', { name: 'Kubernetes' });
userEvent.click(kubernetesOption);
expect(endpointSelect).not.toHaveValue(null);
});

await waitFor(() => {
expect(serviceSelect).toHaveValue('Kubernetes');
expect(
getByPlaceholderText('Select a region for Kubernetes')
).toBeInTheDocument();
expect(endpointSelect).toBeInTheDocument();
});

userEvent.click(serviceSelect);
await waitFor(() => {
const objectStorageOption = getByRole('option', {
name: 'Object Storage',
await userEvent.click(endpointSelect);
await waitFor(async () => {
const endpointOption = getByRole('option', {

Check warning on line 93 in packages/manager/src/features/Account/Quotas/Quotas.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.getByRole` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByRole` instead","line":93,"column":30,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":93,"endColumn":39}
name: 'endpoint1 (Standard E0)',
});
userEvent.click(objectStorageOption);
await userEvent.click(endpointOption);
});

await waitFor(() => {
expect(serviceSelect).toHaveValue('Object Storage');
expect(
getByPlaceholderText('Select an Object Storage S3 endpoint')
).toBeInTheDocument();
expect(endpointSelect).toHaveValue('endpoint1 (Standard E0)');
});
});

it('shows loading state when fetching data', () => {
queryMocks.useGetLocationsForQuotaService.mockReturnValue({
isFetchingRegions: true,
regions: [],
isFetchingS3Endpoints: true,
s3Endpoints: null,
service: 'object-storage',
});

const { getByPlaceholderText } = renderWithTheme(<Quotas />, {
queryClient,
});

expect(
getByPlaceholderText('Loading Linodes regions...')
).toBeInTheDocument();
});

it('shows a global option for regions', async () => {
const { getByPlaceholderText, getByRole } = renderWithTheme(<Quotas />, {
queryClient,
});

const regionSelect = getByPlaceholderText('Select a region for Linodes');
expect(regionSelect).toHaveValue('');

userEvent.click(regionSelect);
await waitFor(() => {
const globalOption = getByRole('option', {
name: 'Global (Account level) (global)',
});
userEvent.click(globalOption);
});

await waitFor(() => {
expect(regionSelect).toHaveValue('Global (Account level) (global)');
});
expect(getByPlaceholderText('Loading S3 endpoints...')).toBeInTheDocument();
});
});
Loading
Loading