Skip to content

Commit dbcea0e

Browse files
new: [STORIF-187] - Global quota usage table created.
1 parent 738f57d commit dbcea0e

File tree

11 files changed

+299
-14
lines changed

11 files changed

+299
-14
lines changed

packages/api-v4/src/quotas/quotas.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,38 @@ export const getQuotaUsage = (type: QuotaType, id: string) =>
5252
setURL(`${BETA_API_ROOT}/${type}/quotas/${id}/usage`),
5353
setMethod('GET'),
5454
);
55+
56+
/**
57+
* getGlobalQuotas
58+
*
59+
* Returns a paginated list of global quotas for a particular service specified by `type`.
60+
*
61+
* This request can be filtered on `quota_name`, `service_name` and `scope`.
62+
*
63+
* @param type { QuotaType } retrieve quotas within this service type.
64+
*/
65+
export const getGlobalQuotas = (
66+
type: QuotaType,
67+
params: Params = {},
68+
filter: Filter = {},
69+
) =>
70+
Request<Page<Quota>>(
71+
setURL(`${BETA_API_ROOT}/${type}/global-quotas`),
72+
setMethod('GET'),
73+
setXFilter(filter),
74+
setParams(params),
75+
);
76+
77+
/**
78+
* getGlobalQuotaUsage
79+
*
80+
* Returns the usage for a single global quota within a particular service specified by `type`.
81+
*
82+
* @param type { QuotaType } retrieve a quota within this service type.
83+
* @param id { string } the quota ID to look up.
84+
*/
85+
export const getGlobalQuotaUsage = (type: QuotaType, id: string) =>
86+
Request<QuotaUsage>(
87+
setURL(`${BETA_API_ROOT}/${type}/global-quotas/${id}/usage`),
88+
setMethod('GET'),
89+
);

packages/manager/cypress/e2e/core/account/quotas-storage.spec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ describe('Quota workflow tests', () => {
100100
usage: Math.round(mockQuotas[2].quota_limit * 0.1),
101101
}),
102102
];
103+
103104
cy.wrap(selectedDomain).as('selectedDomain');
104105
cy.wrap(mockEndpoints).as('mockEndpoints');
105106
cy.wrap(mockQuotas).as('mockQuotas');
106107
cy.wrap(mockQuotaUsages).as('mockQuotaUsages');
108+
107109
mockGetObjectStorageQuotaUsages(
108110
selectedDomain,
109111
'bytes',
@@ -134,6 +136,7 @@ describe('Quota workflow tests', () => {
134136
},
135137
}).as('getFeatureFlags');
136138
});
139+
137140
it('Quotas and quota usages display properly', function () {
138141
cy.visitWithLogin('/account/quotas');
139142
cy.wait(['@getFeatureFlags', '@getObjectStorageEndpoints']);
@@ -332,9 +335,11 @@ describe('Quota workflow tests', () => {
332335
.should('be.visible')
333336
.click();
334337
cy.wait('@getQuotasError');
335-
cy.get('[data-qa-error-msg="true"]')
336-
.should('be.visible')
337-
.should('have.text', errorMsg);
338+
cy.get('[data-testid="endpoint-quotas-table-container"]').within(() => {
339+
cy.get('[data-qa-error-msg="true"]')
340+
.should('be.visible')
341+
.should('have.text', errorMsg);
342+
});
338343
});
339344
});
340345

@@ -508,9 +513,12 @@ describe('Quota workflow tests', () => {
508513
.should('be.visible')
509514
.click();
510515
cy.wait('@getQuotasError');
511-
cy.get('[data-qa-error-msg="true"]')
512-
.should('be.visible')
513-
.should('have.text', errorMsg);
516+
517+
cy.get('[data-testid="endpoint-quotas-table-container"]').within(() => {
518+
cy.get('[data-qa-error-msg="true"]')
519+
.should('be.visible')
520+
.should('have.text', errorMsg);
521+
});
514522
});
515523

516524
// this test executed in context of internal user, using mockApiInternalUser()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
3+
import { Table } from 'src/components/Table/Table';
4+
import { TableBody } from 'src/components/TableBody';
5+
import { TableCell } from 'src/components/TableCell/TableCell';
6+
import { TableHead } from 'src/components/TableHead';
7+
import { TableRow } from 'src/components/TableRow/TableRow';
8+
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
9+
import { TableRowError } from 'src/components/TableRowError/TableRowError';
10+
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
11+
12+
import { useGetObjGlobalQuotasWithUsage } from '../hooks/useGetObjGlobalQuotasWithUsage';
13+
import { GlobalQuotasTableRow } from './GlobalQuotasTableRow';
14+
15+
const quotaRowMinHeight = 58;
16+
17+
export const GlobalQuotasTable = () => {
18+
const {
19+
data: globalQuotasWithUsage,
20+
isFetching: isFetchingGlobalQuotas,
21+
isError: globalQuotasError,
22+
} = useGetObjGlobalQuotasWithUsage();
23+
24+
return (
25+
<Table
26+
data-testid="table-endpoint-global-quotas"
27+
sx={(theme) => ({
28+
marginTop: theme.spacingFunction(16),
29+
minWidth: theme.breakpoints.values.sm,
30+
})}
31+
>
32+
<TableHead>
33+
<TableRow>
34+
<TableCell sx={{ width: '25%' }}>Quota Name</TableCell>
35+
<TableCell sx={{ width: '30%' }}>Account Quota Value</TableCell>
36+
<TableCell sx={{ width: '35%' }}>Usage</TableCell>
37+
</TableRow>
38+
</TableHead>
39+
40+
<TableBody>
41+
{isFetchingGlobalQuotas ? (
42+
<TableRowLoading columns={3} sx={{ height: quotaRowMinHeight }} />
43+
) : globalQuotasError ? (
44+
<TableRowError
45+
colSpan={3}
46+
message="There was an error retrieving global object storage quotas."
47+
/>
48+
) : globalQuotasWithUsage.length === 0 ? (
49+
<TableRowEmpty
50+
colSpan={3}
51+
message="There is no data available for this service."
52+
sx={{ height: quotaRowMinHeight }}
53+
/>
54+
) : (
55+
globalQuotasWithUsage.map((globalQuota, index) => {
56+
return (
57+
<GlobalQuotasTableRow globalQuota={globalQuota} key={index} />
58+
);
59+
})
60+
)}
61+
</TableBody>
62+
</Table>
63+
);
64+
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Box, TooltipIcon, Typography } from '@linode/ui';
2+
import React from 'react';
3+
4+
import { QuotaUsageBar } from 'src/components/QuotaUsageBar/QuotaUsageBar';
5+
import { TableCell } from 'src/components/TableCell/TableCell';
6+
import { TableRow } from 'src/components/TableRow/TableRow';
7+
8+
import { convertResourceMetric, pluralizeMetric } from '../utils';
9+
10+
import type { Quota, QuotaUsage } from '@linode/api-v4';
11+
12+
interface GlobalQuotaWithUsage extends Quota {
13+
usage?: QuotaUsage;
14+
}
15+
interface Params {
16+
globalQuota: GlobalQuotaWithUsage;
17+
}
18+
19+
const quotaRowMinHeight = 58;
20+
21+
export const GlobalQuotasTableRow = ({ globalQuota }: Params) => {
22+
const { convertedLimit, convertedResourceMetric } = convertResourceMetric({
23+
initialResourceMetric: pluralizeMetric(
24+
globalQuota.quota_limit,
25+
globalQuota.resource_metric
26+
),
27+
initialUsage: globalQuota.usage?.usage ?? 0,
28+
initialLimit: globalQuota.quota_limit,
29+
});
30+
31+
return (
32+
<TableRow sx={{ height: quotaRowMinHeight }}>
33+
<TableCell>
34+
<Box alignItems="center" display="flex" flexWrap="nowrap">
35+
<Typography
36+
sx={{
37+
whiteSpace: 'nowrap',
38+
}}
39+
>
40+
{globalQuota.quota_name}
41+
</Typography>
42+
<TooltipIcon
43+
placement="top"
44+
status="info"
45+
sxTooltipIcon={{
46+
position: 'relative',
47+
top: -2,
48+
}}
49+
text={globalQuota.description}
50+
tooltipPosition="right"
51+
/>
52+
</Box>
53+
</TableCell>
54+
55+
<TableCell>
56+
{convertedLimit?.toLocaleString() ?? 'unknown'}{' '}
57+
{convertedResourceMetric}
58+
</TableCell>
59+
60+
<TableCell>
61+
<Box sx={{ maxWidth: '80%' }}>
62+
{globalQuota.usage?.usage ? (
63+
<QuotaUsageBar
64+
limit={globalQuota.quota_limit}
65+
resourceMetric={globalQuota.resource_metric}
66+
usage={globalQuota.usage.usage}
67+
/>
68+
) : (
69+
<Typography>n/a</Typography>
70+
)}
71+
</Box>
72+
</TableCell>
73+
</TableRow>
74+
);
75+
};

packages/manager/src/features/Account/Quotas/Quotas.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle';
1414
import { Link } from 'src/components/Link';
1515
import { useFlags } from 'src/hooks/useFlags';
1616

17-
import { QuotasTable } from './QuotasTable';
17+
import { GlobalQuotasTable } from './GlobalQuotasTable/GlobalQuotasTable';
18+
import { QuotasTable } from './QuotasTable/QuotasTable';
1819
import { useGetLocationsForQuotaService } from './utils';
1920

2021
import type { Quota } from '@linode/api-v4';
@@ -41,14 +42,26 @@ export const Quotas = () => {
4142
return (
4243
<>
4344
<DocumentTitleSegment segment="Quotas" />
45+
46+
<Paper
47+
sx={(theme: Theme) => ({
48+
marginTop: theme.spacingFunction(16),
49+
})}
50+
variant="outlined"
51+
>
52+
<Typography variant="h2">Object Storage: global</Typography>
53+
54+
<GlobalQuotasTable />
55+
</Paper>
56+
4457
<Paper
4558
sx={(theme: Theme) => ({
4659
marginTop: theme.spacingFunction(16),
4760
})}
4861
variant="outlined"
4962
>
5063
<Stack>
51-
<Typography variant="h2">Object Storage</Typography>
64+
<Typography variant="h2">Object Storage: per-endpoint</Typography>
5265
<Box sx={{ display: 'flex' }}>
5366
<Notice spacingTop={16} variant="info">
5467
<Typography>
@@ -109,7 +122,12 @@ export const Quotas = () => {
109122
</Link>
110123
.
111124
</Typography>
112-
<Stack direction="column" spacing={2}>
125+
126+
<Stack
127+
data-testid="endpoint-quotas-table-container"
128+
direction="column"
129+
spacing={2}
130+
>
113131
<QuotasTable
114132
selectedLocation={selectedLocation}
115133
selectedService={{

packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx renamed to packages/manager/src/features/Account/Quotas/QuotasTable/QuotasTable.test.tsx

File renamed without changes.

packages/manager/src/features/Account/Quotas/QuotasTable.tsx renamed to packages/manager/src/features/Account/Quotas/QuotasTable/QuotasTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'
1515
import { useFlags } from 'src/hooks/useFlags';
1616
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
1717

18-
import { QuotasIncreaseForm } from './QuotasIncreaseForm';
18+
import { QuotasIncreaseForm } from '../QuotasIncreaseForm';
19+
import { getQuotasFilters } from '../utils';
1920
import { QuotasTableRow } from './QuotasTableRow';
20-
import { getQuotasFilters } from './utils';
2121

2222
import type { Filter, Quota, QuotaType } from '@linode/api-v4';
2323
import type { SelectOption } from '@linode/ui';

packages/manager/src/features/Account/Quotas/QuotasTableRow.tsx renamed to packages/manager/src/features/Account/Quotas/QuotasTable/QuotasTableRow.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { TableRow } from 'src/components/TableRow/TableRow';
99
import { useFlags } from 'src/hooks/useFlags';
1010
import { useIsAkamaiAccount } from 'src/hooks/useIsAkamaiAccount';
1111

12-
import { convertResourceMetric, getQuotaError, pluralizeMetric } from './utils';
12+
import {
13+
convertResourceMetric,
14+
getQuotaError,
15+
pluralizeMetric,
16+
} from '../utils';
1317

1418
import type { Quota, QuotaUsage } from '@linode/api-v4';
1519
import type { UseQueryResult } from '@tanstack/react-query';
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
globalQuotaQueries,
3+
useGlobalQuotasQuery,
4+
useQueries,
5+
} from '@linode/queries';
6+
import React from 'react';
7+
8+
const SERVICE = 'object-storage';
9+
10+
export function useGetObjGlobalQuotasWithUsage() {
11+
const {
12+
data: globalQuotas,
13+
error: globalQuotasError,
14+
isFetching: isFetchingGlobalQuotas,
15+
} = useGlobalQuotasQuery(SERVICE);
16+
17+
// Quota Usage Queries
18+
// For each global quota, fetch the usage in parallel
19+
// This will only fetch for the paginated set
20+
const globalQuotaIds =
21+
globalQuotas?.data.map((quota) => quota.quota_id) ?? [];
22+
const globalQuotaUsageQueries = useQueries({
23+
queries: globalQuotaIds.map((quotaId) =>
24+
globalQuotaQueries.service(SERVICE)._ctx.usage(quotaId)
25+
),
26+
});
27+
28+
// Combine the quotas with their usage
29+
const globalQuotasWithUsage = React.useMemo(
30+
() =>
31+
globalQuotas?.data.map((quota, index) => ({
32+
...quota,
33+
usage: globalQuotaUsageQueries?.[index]?.data,
34+
})) ?? [],
35+
[globalQuotas, globalQuotaUsageQueries]
36+
);
37+
38+
return {
39+
data: globalQuotasWithUsage,
40+
isError:
41+
globalQuotasError ||
42+
globalQuotaUsageQueries.some((query) => query.isError),
43+
isFetching:
44+
isFetchingGlobalQuotas ||
45+
globalQuotaUsageQueries.some((query) => query.isFetching),
46+
};
47+
}

packages/queries/src/quotas/keys.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { getQuota, getQuotas, getQuotaUsage } from '@linode/api-v4';
1+
import {
2+
getGlobalQuotas,
3+
getGlobalQuotaUsage,
4+
getQuota,
5+
getQuotas,
6+
getQuotaUsage,
7+
} from '@linode/api-v4';
28
import { createQueryKeys } from '@lukemorales/query-key-factory';
39

410
import { getAllQuotas } from './requests';
@@ -28,3 +34,19 @@ export const quotaQueries = createQueryKeys('quotas', {
2834
queryKey: [type],
2935
}),
3036
});
37+
38+
export const globalQuotaQueries = createQueryKeys('global-quotas', {
39+
service: (type: QuotaType) => ({
40+
contextQueries: {
41+
paginated: (params: Params = {}, filter: Filter = {}) => ({
42+
queryFn: () => getGlobalQuotas(type, params, filter),
43+
queryKey: [params, filter],
44+
}),
45+
usage: (id: string) => ({
46+
queryFn: () => getGlobalQuotaUsage(type, id),
47+
queryKey: [id],
48+
}),
49+
},
50+
queryKey: [type],
51+
}),
52+
});

0 commit comments

Comments
 (0)