Skip to content

Commit

Permalink
save progress
Browse files Browse the repository at this point in the history
  • Loading branch information
abailly-akamai committed Feb 12, 2025
1 parent a3b4b68 commit d190130
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 181 deletions.
2 changes: 1 addition & 1 deletion packages/api-v4/src/quotas/quotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const getQuotas = (
* @param type { QuotaType } retrieve a quota within this service type.
* @param id { number } the quota ID to look up.
*/
export const getQuotaUsage = (type: QuotaType, id: number) =>
export const getQuotaUsage = (type?: QuotaType, id?: number) =>
Request<QuotaUsage>(
setURL(`${API_ROOT}/${type}/quotas/${id}/usage`),
setMethod('GET')
Expand Down
1 change: 1 addition & 0 deletions packages/api-v4/src/quotas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface Quota {
* 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';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export const StyledAutocompleteContainer = styled(Box, {
'& .MuiAutocomplete-root .MuiAutocomplete-inputRoot': {
paddingRight: 8,
},
// if subheader is empty, hide it
// If the subheader is empty, hide it to avoid empty padded space
// This can happen for options that do not belong to a region (e.g. "Global")
'& .MuiListSubheader-root:empty': {
display: 'none',
},
Expand Down
241 changes: 126 additions & 115 deletions packages/manager/src/features/Account/Quotas/Quotas.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { getQuotaUsage, quotaTypes } from '@linode/api-v4';
import { Button, Divider, Paper, Select, Stack, Typography } from '@linode/ui';
import {
CircleProgress,
Divider,
Paper,
Select,
Stack,
Typography,
} from '@linode/ui';
import { useQueries } from '@tanstack/react-query';
import * as React from 'react';

import { DocsLink } from 'src/components/DocsLink/DocsLink';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
import { regionFactory } from 'src/factories';
import { useQuotasQuery } from 'src/queries/quotas/quotas';

import { useGetLocationsForQuotaService } from './utils';
Expand All @@ -21,71 +29,54 @@ export const Quotas = () => {
label: 'Linodes',
value: 'linode',
});
const [selectedLocation, setSelectedLocation] = React.useState<
SelectOption<'global' | Region['id']>
>({
label: 'Global (Account level)',
value: 'global',
});
const [queryEnabled, setQueryEnabled] = React.useState(false);

const serviceOptions = Object.entries(quotaTypes).map(([key, value]) => ({
label: value,
value: key as QuotaType,
}));
const [selectedLocation, setSelectedLocation] = React.useState<SelectOption<
'global' | Region['id']
> | null>(null);
const locationData = useGetLocationsForQuotaService(selectedService.value);

// Fetch locations for the selected service to populate the location selects
// This can be a region or a label + id for S3 endpoints
const {
isFetching: isFetchingRegions,
locationsForQuotaService,
objectStorageQuotas,
service,
} = useGetLocationsForQuotaService(selectedService.value);

// Disable the select and view quotas button if there is no region for the selected service
// ("Global" is a default entry hence the <= 1 check)
const noQuotaRegions = locationsForQuotaService.length <= 1;

// fetch quotas for the selected service and region
const { data: quotas } = useQuotasQuery(
data: quotas,
isFetching: isFetchingQuotas,
refetch,
} = useQuotasQuery(
selectedService.value,
{},
{
region_applied:
selectedService.value !== 'object-storage'
? selectedLocation.value
? selectedLocation?.value
: undefined,
s3_endpoint:
selectedService.value === 'object-storage'
? selectedLocation.value
? selectedLocation?.value
: undefined,
},
queryEnabled
Boolean(selectedLocation?.value)
);

const onSelectServiceChange = (
_event: React.SyntheticEvent<Element, Event>,
value: SelectOption<QuotaType>
) => {
setQueryEnabled(false);
setSelectedService(value);
setSelectedLocation({
label: 'Global (Account level)',
value: 'global',
});
};
// Build Service options
const serviceOptions = Object.entries(quotaTypes).map(([key, value]) => ({
label: value,
value: key as QuotaType,
}));

// The CTA will enable running all quota queries
const onClickViewQuotas = () => {
setQueryEnabled(true);
};
// Build Location options

const { regions, s3Endpoints } = locationData;
const globalOption = regionFactory.build({
capabilities: [],
id: 'global',
label: 'Global (Account level)',
});
const memoizedLocationOptions = React.useMemo(() => {
return [globalOption, ...(regions ?? [])];
}, [regions, globalOption]);

// Fetch the usage for each quota
const quotaIds = quotas?.data.map((quota) => quota.quota_id) ?? [];
const quotaUsageQueries = useQueries({
queries: quotaIds.map((quotaId) => ({
enabled: queryEnabled && Boolean(quotas),
enabled: selectedService && Boolean(selectedLocation) && Boolean(quotas),
queryFn: () => getQuotaUsage(selectedService.value, quotaId),
queryKey: ['quota-usage', selectedService.value, quotaId],
})),
Expand All @@ -97,12 +88,29 @@ export const Quotas = () => {
...quota,
usage: quotaUsageQueries?.[index]?.data,
}));
const objectStorageQuotasWithUsage = objectStorageQuotas?.map(
(quota, index) => ({
...quota,
usage: quotaUsageQueries?.[index]?.data,
})

// Loading logic
// - Locations
const isFetchingLocations =
'isFetchingObjectStorageQuotas' in locationData
? locationData.isFetchingObjectStorageQuotas
: locationData.isFetchingRegions;
// - Quotas
const isLoadingQuotaUsage = quotaUsageQueries.some(
(query) => query.isLoading
);
const isLoadingQuotasTable =
isFetchingQuotas || isLoadingQuotaUsage || !quotasWithUsage;

// Handlers
const onSelectServiceChange = (
_event: React.SyntheticEvent<Element, Event>,
value: SelectOption<QuotaType>
) => {
setSelectedService(value);
setSelectedLocation(null);
refetch();
};

return (
<>
Expand All @@ -119,63 +127,64 @@ export const Quotas = () => {
label="Select a Service"
onChange={onSelectServiceChange}
options={serviceOptions}
placeholder="Select a service"
value={selectedService}
/>
<Stack alignItems="flex-end" direction="row">
{service === 'object-storage' ? (
<Select
onChange={(_event, value) => {
setSelectedLocation({
label: value?.label,
value: value?.value,
});
setQueryEnabled(false);
}}
value={{
label:
locationsForQuotaService.find(
(loc) => loc.value === selectedLocation.value
)?.label ?? '',
value: selectedLocation.value,
}}
label="Object Storage Endpoint"
options={locationsForQuotaService}
placeholder="Select an Object Storage S3 endpoint"
searchable
sx={{ flexGrow: 1, mr: 2 }}
/>
) : (
<RegionSelect
onChange={(_event, value) => {
setSelectedLocation({
label: value.label,
value: value.id,
});
setQueryEnabled(false);
}}
placeholder={
isFetchingRegions
? `Loading ${selectedService.label} regions...`
: `Select a region for ${selectedService.label}`
}
currentCapability={undefined}
disableClearable
disabled={isFetchingRegions || noQuotaRegions}
loading={isFetchingRegions}
noOptionsText={`No resource found for ${selectedService.label}`}
regions={locationsForQuotaService}
sx={{ flexGrow: 1, mr: 2 }}
value={noQuotaRegions ? undefined : selectedLocation.value}
/>
)}
<Button
buttonType="primary"
disabled={isFetchingRegions || noQuotaRegions}
onClick={onClickViewQuotas}
>
View Quotas
</Button>
</Stack>

{selectedService.value === 'object-storage' ? (
<Select
onChange={(_event, value) => {
setSelectedLocation({
label: value?.label,
value: value?.value,
});
}}
options={
memoizedLocationOptions.map((location) => ({
label: location.label,
value: location.label,
})) ?? []
}
placeholder={
isFetchingLocations
? `Loading ${selectedService.label} S3 endpoints...`
: 'Select an Object Storage S3 endpoint'
}
value={{
label:
s3Endpoints?.find(
(loc) => loc.label === selectedLocation?.value
)?.label ?? '',
value: selectedLocation?.value ?? '',
}}
label="Object Storage Endpoint"
loading={isFetchingLocations}
searchable
sx={{ flexGrow: 1, mr: 2 }}
/>
) : (
<RegionSelect
onChange={(_event, value) => {
setSelectedLocation({
label: value.label,
value: value.id,
});
}}
placeholder={
isFetchingLocations
? `Loading ${selectedService.label} regions...`
: `Select a region for ${selectedService.label}`
}
currentCapability={undefined}
disableClearable
disabled={isFetchingLocations}
loading={isFetchingLocations}
noOptionsText={`No resource found for ${selectedService.label}`}
regions={memoizedLocationOptions}
sx={{ flexGrow: 1, mr: 2 }}
value={selectedLocation?.value}
/>
)}
</Stack>
<Stack direction="row" justifyContent="space-between">
<Typography variant="h3">Quotas</Typography>
Expand All @@ -185,9 +194,10 @@ export const Quotas = () => {
</Stack>
</Stack>
<Stack direction="row" spacing={2}>
{selectedLocation &&
(quotas || objectStorageQuotas) &&
queryEnabled && (
{selectedLocation ? (
isLoadingQuotasTable ? (
<CircleProgress />
) : (
<pre
style={{
backgroundColor: '#f5f5f5',
Expand All @@ -197,13 +207,14 @@ export const Quotas = () => {
width: '100%',
}}
>
{JSON.stringify(
quotasWithUsage || objectStorageQuotasWithUsage,
null,
2
)}
{JSON.stringify(quotasWithUsage, null, 2)}
</pre>
)}
)
) : (
<Typography>
Select a service and region to view quotas
</Typography>
)}
</Stack>
</Stack>
</Paper>
Expand Down
Loading

0 comments on commit d190130

Please sign in to comment.