- {linodeInterfacesFlag?.enabled &&
}
+ {isLinodeInterfacesEnabled &&
}
{
configuration into your Linode at boot.
-
+
({
usePreferences: vi.fn().mockReturnValue({}),
}));
-vi.mock('src/queries/profile/preferences', async () => {
- const actual = await vi.importActual('src/queries/profile/preferences');
+vi.mock('@linode/queries', async () => {
+ const actual = await vi.importActual('@linode/queries');
return {
...actual,
usePreferences: queryMocks.usePreferences,
diff --git a/packages/manager/src/features/Account/ObjectStorageSettings.tsx b/packages/manager/src/features/Account/ObjectStorageSettings.tsx
index a01f8868914..db2e0a7522a 100644
--- a/packages/manager/src/features/Account/ObjectStorageSettings.tsx
+++ b/packages/manager/src/features/Account/ObjectStorageSettings.tsx
@@ -12,9 +12,8 @@ import * as React from 'react';
import { Link } from 'src/components/Link';
import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog';
-import { useAccountSettings } from 'src/queries/account/settings';
+import { useAccountSettings, useProfile } from '@linode/queries';
import { useCancelObjectStorageMutation } from 'src/queries/object-storage/queries';
-import { useProfile } from 'src/queries/profile/profile';
export const ObjectStorageSettings = () => {
const { data: profile } = useProfile();
diff --git a/packages/manager/src/features/Account/Quotas.tsx b/packages/manager/src/features/Account/Quotas.tsx
deleted file mode 100644
index ef2c78c8930..00000000000
--- a/packages/manager/src/features/Account/Quotas.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unused-vars */
-import { Autocomplete, Divider, Paper, Stack, Typography } from '@linode/ui';
-import { DateTime } from 'luxon';
-import * as React from 'react';
-
-import { DateTimeDisplay } from 'src/components/DateTimeDisplay';
-import { DocsLink } from 'src/components/DocsLink/DocsLink';
-import { DocumentTitleSegment } from 'src/components/DocumentTitle';
-import { RegionSelect } from 'src/components/RegionSelect/RegionSelect';
-
-import type { Theme } from '@mui/material';
-
-export const Quotas = () => {
- // @ts-expect-error TODO: this is a placeholder to be replaced with the actual query
- const [lastUpdatedDate, setLastUpdatedDate] = React.useState(Date.now());
-
- return (
- <>
-
- ({
- marginTop: theme.spacing(2),
- })}
- variant="outlined"
- >
- }>
-
-
-
-
-
- Quotas
-
-
- Last updated:{' '}
-
-
-
- {/* TODO: update once link is available */}
-
-
-
-
-
- >
- );
-};
diff --git a/packages/manager/src/features/Account/Quotas/Quotas.test.tsx b/packages/manager/src/features/Account/Quotas/Quotas.test.tsx
new file mode 100644
index 00000000000..df347dfd92d
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/Quotas.test.tsx
@@ -0,0 +1,152 @@
+import { QueryClient } from '@tanstack/react-query';
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import * as React from 'react';
+
+import { regionFactory } from 'src/factories';
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { Quotas } from './Quotas';
+
+const queryMocks = vi.hoisted(() => ({
+ 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,
+}));
+
+vi.mock('src/hooks/useFlags', () => {
+ const actual = vi.importActual('src/hooks/useFlags');
+ return {
+ ...actual,
+ useFlags: queryMocks.useFlags,
+ };
+});
+
+vi.mock('./utils', () => ({
+ getQuotasFilters: queryMocks.getQuotasFilters,
+ useGetLocationsForQuotaService: queryMocks.useGetLocationsForQuotaService,
+}));
+
+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: {
+ retry: false,
+ },
+ },
+ });
+
+ it('renders the component with initial state', () => {
+ const { getByText } = renderWithTheme(, {
+ queryClient,
+ });
+
+ expect(getByText('Quotas')).toBeInTheDocument();
+ expect(getByText('Learn More About Quotas')).toBeInTheDocument();
+ expect(getByText('Select a Service')).toBeInTheDocument();
+ expect(
+ screen.getByPlaceholderText('Select a region for Linodes')
+ ).toBeInTheDocument();
+ });
+
+ it('allows service selection', async () => {
+ const { getByPlaceholderText, getByRole } = renderWithTheme(, {
+ queryClient,
+ });
+
+ const serviceSelect = getByPlaceholderText('Select a service');
+
+ await waitFor(() => {
+ expect(serviceSelect).toHaveValue('Linodes');
+ expect(
+ getByPlaceholderText('Select a region for Linodes')
+ ).toBeInTheDocument();
+ });
+
+ userEvent.click(serviceSelect);
+ await waitFor(() => {
+ const kubernetesOption = getByRole('option', { name: 'Kubernetes' });
+ userEvent.click(kubernetesOption);
+ });
+
+ await waitFor(() => {
+ expect(serviceSelect).toHaveValue('Kubernetes');
+ expect(
+ getByPlaceholderText('Select a region for Kubernetes')
+ ).toBeInTheDocument();
+ });
+
+ userEvent.click(serviceSelect);
+ await waitFor(() => {
+ const objectStorageOption = getByRole('option', {
+ name: 'Object Storage',
+ });
+ userEvent.click(objectStorageOption);
+ });
+
+ await waitFor(() => {
+ expect(serviceSelect).toHaveValue('Object Storage');
+ expect(
+ getByPlaceholderText('Select an Object Storage S3 endpoint')
+ ).toBeInTheDocument();
+ });
+ });
+
+ it('shows loading state when fetching data', () => {
+ queryMocks.useGetLocationsForQuotaService.mockReturnValue({
+ isFetchingRegions: true,
+ regions: [],
+ });
+
+ const { getByPlaceholderText } = renderWithTheme(, {
+ queryClient,
+ });
+
+ expect(
+ getByPlaceholderText('Loading Linodes regions...')
+ ).toBeInTheDocument();
+ });
+
+ it('shows a global option for regions', async () => {
+ const { getByPlaceholderText, getByRole } = renderWithTheme(, {
+ 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)');
+ });
+ });
+});
diff --git a/packages/manager/src/features/Account/Quotas/Quotas.tsx b/packages/manager/src/features/Account/Quotas/Quotas.tsx
new file mode 100644
index 00000000000..60089bd7eba
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/Quotas.tsx
@@ -0,0 +1,155 @@
+import { quotaTypes } from '@linode/api-v4';
+import { Divider, 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 { QuotasTable } from './QuotasTable';
+import { useGetLocationsForQuotaService } from './utils';
+
+import type { Quota, QuotaType } from '@linode/api-v4';
+import type { SelectOption } from '@linode/ui';
+import type { Theme } from '@mui/material';
+
+export const Quotas = () => {
+ const history = useHistory();
+ const [selectedService, setSelectedService] = React.useState<
+ SelectOption
+ >({
+ label: 'Linodes',
+ value: 'linode',
+ });
+ const [selectedLocation, setSelectedLocation] = React.useState | null>(null);
+ const locationData = useGetLocationsForQuotaService(selectedService.value);
+
+ const serviceOptions = Object.entries(quotaTypes).map(([key, value]) => ({
+ label: value,
+ value: key as QuotaType,
+ }));
+
+ const { regions, s3Endpoints } = locationData;
+ const isFetchingLocations =
+ 'isFetchingS3Endpoints' in locationData
+ ? locationData.isFetchingS3Endpoints
+ : locationData.isFetchingRegions;
+
+ // Handlers
+ const onSelectServiceChange = (
+ _event: React.SyntheticEvent,
+ value: SelectOption
+ ) => {
+ setSelectedService(value);
+ setSelectedLocation(null);
+ // remove search params
+ history.push('/account/quotas');
+ };
+
+ return (
+ <>
+
+ ({
+ marginTop: theme.spacing(2),
+ })}
+ variant="outlined"
+ >
+
+
+
+ {selectedService.value === 'object-storage' ? (
+
+
+
+ Quotas
+ ({
+ position: 'relative',
+ top: `-${theme.spacing(2)}`,
+ })}
+ alignItems="center"
+ direction="row"
+ spacing={3}
+ >
+ {/* TODO LIMITS_M1: update once link is available */}
+
+
+
+
+ This table shows quotas and usage. If you need to increase a quota,
+ select Request an Increase from the Actions menu.
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.test.tsx b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.test.tsx
new file mode 100644
index 00000000000..20a8e92988d
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.test.tsx
@@ -0,0 +1,74 @@
+import { act, fireEvent, waitFor } from '@testing-library/react';
+import * as React from 'react';
+
+import { quotaFactory, quotaUsageFactory } from 'src/factories/quotas';
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { QuotasIncreaseForm } from './QuotasIncreaseForm';
+
+describe('QuotasIncreaseForm', () => {
+ it('should render with default values', async () => {
+ const {
+ getByLabelText,
+ getByRole,
+ getByTestId,
+ getByText,
+ } = renderWithTheme(
+ {}}
+ onSuccess={() => {}}
+ open={true}
+ />
+ );
+
+ expect(getByLabelText('Title (required)')).toHaveValue('Increase Quota');
+ expect(getByLabelText('Quantity (required)')).toHaveValue(0);
+ expect(getByText('In us-east (initial limit of 50)')).toBeInTheDocument();
+ expect(getByLabelText('Notes')).toHaveValue('');
+ expect(getByText('Ticket Preview')).toBeInTheDocument();
+ expect(
+ getByTestId('quota-increase-form-preview').firstChild?.firstChild
+ ).toHaveAttribute('aria-expanded', 'false');
+
+ expect(getByRole('button', { name: 'Cancel' })).toBeEnabled();
+ expect(getByRole('button', { name: 'Submit' })).toBeEnabled();
+ });
+
+ it('description should be updated as quantity is changed', async () => {
+ const { getByLabelText, getByTestId } = renderWithTheme(
+ {}}
+ onSuccess={() => {}}
+ open={true}
+ />
+ );
+
+ const quantityInput = getByLabelText('Quantity (required)');
+ const notesInput = getByLabelText('Notes');
+ const preview = getByTestId('quota-increase-form-preview');
+ const previewContent = getByTestId('quota-increase-form-preview-content');
+
+ await waitFor(() => {
+ act(() => {
+ fireEvent.change(quantityInput, { target: { value: 2 } });
+ fireEvent.change(notesInput, { target: { value: 'test!' } });
+ fireEvent.click(preview);
+ });
+ });
+
+ await waitFor(() => {
+ // eslint-disable-next-line xss/no-mixed-html
+ expect(previewContent).toHaveTextContent(
+ 'Increase QuotaUser: mock-user Email: mock-user@linode.com Quota Name: Linode Dedicated vCPUs New Quantity Requested: 2 CPUs Region: us-east test!'
+ );
+ });
+ });
+});
diff --git a/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx
new file mode 100644
index 00000000000..2d30fe2400c
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx
@@ -0,0 +1,212 @@
+import { yupResolver } from '@hookform/resolvers/yup';
+import { useProfile } from '@linode/queries';
+import {
+ Accordion,
+ ActionsPanel,
+ Notice,
+ Stack,
+ TextField,
+ Typography,
+} from '@linode/ui';
+import { scrollErrorIntoViewV2 } from '@linode/utilities';
+import * as React from 'react';
+import { Controller, FormProvider, useForm } from 'react-hook-form';
+
+import { Markdown } from 'src/components/Markdown/Markdown';
+import { useCreateSupportTicketMutation } from 'src/queries/support';
+
+import { getQuotaIncreaseFormSchema, getQuotaIncreaseMessage } from './utils';
+
+import type { APIError, Quota, TicketRequest } from '@linode/api-v4';
+
+interface QuotasIncreaseFormProps {
+ onClose: () => void;
+ onSuccess: (ticketId: number) => void;
+ open: boolean;
+ quota: Quota;
+}
+
+export interface QuotaIncreaseFormFields extends TicketRequest {
+ notes?: string;
+ quantity: string;
+}
+
+export const QuotasIncreaseForm = (props: QuotasIncreaseFormProps) => {
+ const { onClose, quota } = props;
+ const [submitting, setSubmitting] = React.useState(false);
+ const [error, setError] = React.useState(null);
+ const formContainerRef = React.useRef(null);
+ const { data: profile } = useProfile();
+ const { mutateAsync: createSupportTicket } = useCreateSupportTicketMutation();
+
+ const defaultValues = React.useMemo(
+ () => getQuotaIncreaseMessage({ profile, quantity: 0, quota }),
+ [quota, profile]
+ );
+ const form = useForm({
+ defaultValues,
+ mode: 'onBlur',
+ resolver: yupResolver(getQuotaIncreaseFormSchema),
+ });
+
+ const { notes, quantity, summary } = form.watch();
+
+ const quotaIncreaseDescription = getQuotaIncreaseMessage({
+ profile,
+ quantity: Number(quantity),
+ quota,
+ }).description;
+
+ const handleSubmit = form.handleSubmit(async (values) => {
+ const { onSuccess } = props;
+
+ setSubmitting(true);
+
+ const payload: TicketRequest = {
+ description: `${quotaIncreaseDescription}\n\n${values.notes}`,
+ summary: values.summary,
+ };
+
+ createSupportTicket(payload)
+ .then((response) => {
+ return response;
+ })
+ .then((response) => {
+ onSuccess(response.id);
+ })
+ .catch((errResponse: APIError[]) => {
+ setError(errResponse[0].reason);
+ setSubmitting(false);
+ scrollErrorIntoViewV2(formContainerRef);
+ });
+ });
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx b/packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx
new file mode 100644
index 00000000000..d94bf3f98f2
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/QuotasTable.test.tsx
@@ -0,0 +1,125 @@
+import { waitFor } from '@testing-library/react';
+import * as React from 'react';
+
+import { quotaFactory, quotaUsageFactory } from 'src/factories/quotas';
+import { renderWithTheme } from 'src/utilities/testHelpers';
+
+import { QuotasTable } from './QuotasTable';
+
+const queryMocks = vi.hoisted(() => ({
+ quotaQueries: {
+ service: vi.fn().mockReturnValue({
+ _ctx: {
+ usage: vi.fn().mockReturnValue({}),
+ },
+ }),
+ },
+ useQueries: vi.fn().mockReturnValue([]),
+ useQuotaUsageQuery: vi.fn().mockReturnValue({}),
+ useQuotasQuery: vi.fn().mockReturnValue({}),
+}));
+
+vi.mock('src/queries/quotas/quotas', () => {
+ const actual = vi.importActual('src/queries/quotas/quotas');
+ return {
+ ...actual,
+ quotaQueries: queryMocks.quotaQueries,
+ useQuotaUsageQuery: queryMocks.useQuotaUsageQuery,
+ useQuotasQuery: queryMocks.useQuotasQuery,
+ };
+});
+
+vi.mock('@tanstack/react-query', async () => {
+ const actual = await vi.importActual('@tanstack/react-query');
+ return {
+ ...actual,
+ useQueries: queryMocks.useQueries,
+ };
+});
+
+describe('QuotasTable', () => {
+ it('should render', () => {
+ const { getByRole, getByTestId, getByText } = renderWithTheme(
+
+ );
+ expect(
+ getByRole('columnheader', { name: 'Quota Name' })
+ ).toBeInTheDocument();
+ expect(
+ getByRole('columnheader', { name: 'Account Quota Value' })
+ ).toBeInTheDocument();
+ expect(getByRole('columnheader', { name: 'Usage' })).toBeInTheDocument();
+ expect(getByTestId('table-row-empty')).toBeInTheDocument();
+ expect(
+ getByText('Apply filters above to see quotas and current usage.')
+ ).toBeInTheDocument();
+ });
+
+ it('should render a table with the correct data', async () => {
+ const quotas = [
+ quotaFactory.build({
+ description: 'Random Quota Description',
+ quota_limit: 100,
+ quota_name: 'Random Quota',
+ region_applied: 'us-east',
+ }),
+ ];
+ const quotaUsage = quotaUsageFactory.build({
+ quota_limit: 100,
+ used: 10,
+ });
+ queryMocks.useQueries.mockReturnValue([
+ {
+ data: quotaUsage,
+ isLoading: false,
+ },
+ ]);
+ queryMocks.useQuotasQuery.mockReturnValue({
+ data: {
+ data: quotas,
+ page: 1,
+ pages: 1,
+ results: 1,
+ },
+ isFetching: false,
+ });
+ queryMocks.useQuotaUsageQuery.mockReturnValue({
+ data: quotaUsage,
+ isFetching: false,
+ });
+
+ const { getByLabelText, getByTestId, getByText } = renderWithTheme(
+
+ );
+
+ const quota = quotas[0];
+
+ await waitFor(() => {
+ expect(getByText(quota.quota_name)).toBeInTheDocument();
+ expect(getByText(quota.quota_limit)).toBeInTheDocument();
+ expect(getByLabelText(quota.description)).toBeInTheDocument();
+ expect(getByTestId('linear-progress')).toBeInTheDocument();
+ expect(
+ getByText(`${quotaUsage.used} of ${quotaUsage.quota_limit} CPUs used`)
+ ).toBeInTheDocument();
+ expect(
+ getByLabelText(`Action menu for quota ${quota.quota_name}`)
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/manager/src/features/Account/Quotas/QuotasTable.tsx b/packages/manager/src/features/Account/Quotas/QuotasTable.tsx
new file mode 100644
index 00000000000..7008d4cdd9e
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/QuotasTable.tsx
@@ -0,0 +1,182 @@
+import { Dialog, ErrorState } from '@linode/ui';
+import { useQueries } from '@tanstack/react-query';
+import * as React from 'react';
+import { useHistory } from 'react-router-dom';
+
+import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
+import { Table } from 'src/components/Table/Table';
+import { TableBody } from 'src/components/TableBody';
+import { TableCell } from 'src/components/TableCell/TableCell';
+import { TableHead } from 'src/components/TableHead';
+import { TableRow } from 'src/components/TableRow/TableRow';
+import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
+import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
+import { usePagination } from 'src/hooks/usePagination';
+import { useQuotasQuery } from 'src/queries/quotas/quotas';
+import { quotaQueries } from 'src/queries/quotas/quotas';
+
+import { QuotasIncreaseForm } from './QuotasIncreaseForm';
+import { QuotasTableRow } from './QuotasTableRow';
+import { getQuotasFilters } from './utils';
+
+import type { Filter, Quota, QuotaType } from '@linode/api-v4';
+import type { SelectOption } from '@linode/ui';
+import type { AttachmentError } from 'src/features/Support/SupportTicketDetail/SupportTicketDetail';
+
+const quotaRowMinHeight = 58;
+
+interface QuotasTableProps {
+ selectedLocation: SelectOption | null;
+ selectedService: SelectOption;
+}
+
+export const QuotasTable = (props: QuotasTableProps) => {
+ const { selectedLocation, selectedService } = props;
+ const history = useHistory();
+ const pagination = usePagination(1, 'quotas-table');
+ const hasSelectedLocation = Boolean(selectedLocation);
+ const [supportModalOpen, setSupportModalOpen] = React.useState(false);
+ const [selectedQuota, setSelectedQuota] = React.useState();
+
+ const filters: Filter = getQuotasFilters({
+ location: selectedLocation,
+ service: selectedService,
+ });
+
+ const {
+ data: quotas,
+ error: quotasError,
+ isFetching: isFetchingQuotas,
+ } = useQuotasQuery(
+ selectedService.value,
+ {
+ page: pagination.page,
+ page_size: pagination.pageSize,
+ },
+ filters,
+ Boolean(selectedLocation?.value)
+ );
+
+ // Quota Usage Queries
+ // For each quota, fetch the usage in parallel
+ // This will only fetch for the paginated set
+ const quotaIds = quotas?.data.map((quota) => quota.quota_id) ?? [];
+ const quotaUsageQueries = useQueries({
+ queries: quotaIds.map((quotaId) =>
+ quotaQueries.service(selectedService.value)._ctx.usage(quotaId)
+ ),
+ });
+
+ // Combine the quotas with their usage
+ const quotasWithUsage = React.useMemo(
+ () =>
+ quotas?.data.map((quota, index) => ({
+ ...quota,
+ usage: quotaUsageQueries?.[index]?.data,
+ })) ?? [],
+ [quotas, quotaUsageQueries]
+ );
+
+ if (quotasError) {
+ return ;
+ }
+
+ const onIncreaseQuotaTicketCreated = (
+ ticketId: number,
+ attachmentErrors: AttachmentError[] = []
+ ) => {
+ history.push({
+ pathname: `/support/tickets/${ticketId}`,
+ state: { attachmentErrors },
+ });
+ setSupportModalOpen(false);
+ };
+
+ return (
+ <>
+ ({
+ marginTop: theme.spacing(2),
+ minWidth: theme.breakpoints.values.sm,
+ })}
+ >
+
+
+ Quota Name
+ Account Quota Value
+ Usage
+
+
+
+
+ {hasSelectedLocation && isFetchingQuotas ? (
+
+ ) : !selectedLocation ? (
+
+ ) : quotasWithUsage.length === 0 ? (
+
+ ) : (
+ quotasWithUsage.map((quota, index) => {
+ const hasQuotaUsage = quota.usage?.used !== null;
+
+ return (
+
+ );
+ })
+ )}
+
+
+ {selectedLocation && !isFetchingQuotas && (
+
+ )}
+
+
+ >
+ );
+};
diff --git a/packages/manager/src/features/Account/Quotas/QuotasTableRow.tsx b/packages/manager/src/features/Account/Quotas/QuotasTableRow.tsx
new file mode 100644
index 00000000000..fae05e6d098
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/QuotasTableRow.tsx
@@ -0,0 +1,150 @@
+import { Box, CircleProgress, TooltipIcon, Typography } from '@linode/ui';
+import ErrorOutline from '@mui/icons-material/ErrorOutline';
+import { useTheme } from '@mui/material/styles';
+import * as React from 'react';
+
+import { ActionMenu } from 'src/components/ActionMenu/ActionMenu';
+import { BarPercent } from 'src/components/BarPercent/BarPercent';
+import { TableCell } from 'src/components/TableCell/TableCell';
+import { TableRow } from 'src/components/TableRow/TableRow';
+import { useFlags } from 'src/hooks/useFlags';
+import { useIsAkamaiAccount } from 'src/hooks/useIsAkamaiAccount';
+
+import { getQuotaError } from './utils';
+
+import type { Quota, QuotaUsage } from '@linode/api-v4';
+import type { UseQueryResult } from '@tanstack/react-query';
+import type { Action } from 'src/components/ActionMenu/ActionMenu';
+
+interface QuotaWithUsage extends Quota {
+ usage?: QuotaUsage;
+}
+
+interface QuotasTableRowProps {
+ hasQuotaUsage: boolean;
+ index: number;
+ quota: QuotaWithUsage;
+ quotaUsageQueries: UseQueryResult[];
+ setSelectedQuota: (quota: Quota) => void;
+ setSupportModalOpen: (open: boolean) => void;
+}
+
+const quotaRowMinHeight = 58;
+
+export const QuotasTableRow = (props: QuotasTableRowProps) => {
+ const {
+ hasQuotaUsage,
+ index,
+ quota,
+ quotaUsageQueries,
+ setSelectedQuota,
+ setSupportModalOpen,
+ } = props;
+ const theme = useTheme();
+ const flags = useFlags();
+ const { isAkamaiAccount } = useIsAkamaiAccount();
+ // These conditions are meant to achieve a couple things:
+ // 1. Ability to disable the request for increase button for Internal accounts (this will be used for early adopters, and removed eventually).
+ // 2. Ability to disable the request for increase button for All accounts (this is a prevention measure when beta is in GA).
+ const isRequestForQuotaButtonDisabled =
+ flags.limitsEvolution?.requestForIncreaseDisabledForAll ||
+ (flags.limitsEvolution?.requestForIncreaseDisabledForInternalAccountsOnly &&
+ isAkamaiAccount);
+
+ const requestIncreaseAction: Action = {
+ disabled: isRequestForQuotaButtonDisabled,
+ onClick: () => {
+ setSelectedQuota(quota);
+ setSupportModalOpen(true);
+ },
+ title: 'Request an Increase',
+ };
+
+ return (
+
+
+
+
+ {quota.quota_name}
+
+
+
+
+ {quota.quota_limit}
+
+
+ {quotaUsageQueries[index]?.isLoading ? (
+
+ {' '}
+ Fetching Data...
+
+ ) : quotaUsageQueries[index]?.error ? (
+
+
+ {getQuotaError(quotaUsageQueries, index)}
+
+ ) : hasQuotaUsage ? (
+ <>
+
+
+ {`${quota.usage?.used} of ${quota.quota_limit} ${
+ quota.resource_metric
+ }${quota.quota_limit > 1 ? 's' : ''} used`}
+
+ >
+ ) : (
+ Data not available
+ )}
+
+
+ {hasQuotaUsage ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+};
diff --git a/packages/manager/src/features/Account/Quotas/utils.test.tsx b/packages/manager/src/features/Account/Quotas/utils.test.tsx
new file mode 100644
index 00000000000..1defc2fd551
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/utils.test.tsx
@@ -0,0 +1,122 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { renderHook } from '@testing-library/react';
+import * as React from 'react';
+
+import { profileFactory } from 'src/factories/profile';
+import { quotaFactory, quotaUsageFactory } from 'src/factories/quotas';
+
+import {
+ getQuotaError,
+ getQuotaIncreaseMessage,
+ useGetLocationsForQuotaService,
+} from './utils';
+
+import type { QuotaUsage } from '@linode/api-v4';
+import type { UseQueryResult } from '@tanstack/react-query';
+
+const queryMocks = vi.hoisted(() => ({
+ useObjectStorageEndpoints: vi.fn().mockReturnValue({}),
+}));
+
+vi.mock('src/queries/object-storage/queries', () => {
+ const actual = vi.importActual('src/queries/object-storage/queries');
+ return {
+ ...actual,
+ useObjectStorageEndpoints: queryMocks.useObjectStorageEndpoints,
+ };
+});
+
+describe('useGetLocationsForQuotaService', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ return (
+ {children}
+ );
+ };
+
+ it('should handle object storage endpoints with null values', () => {
+ const { result } = renderHook(
+ () => useGetLocationsForQuotaService('object-storage'),
+ {
+ wrapper,
+ }
+ );
+
+ expect(result.current.s3Endpoints).toEqual([
+ { label: 'Global (Account level)', value: 'global' },
+ ]);
+ });
+
+ it('should filter out endpoints with null s3_endpoint values', () => {
+ queryMocks.useObjectStorageEndpoints.mockReturnValue({
+ data: [
+ {
+ endpoint_type: 'E0',
+ s3_endpoint: 'endpoint1',
+ },
+ {
+ endpoint_type: 'E0',
+ s3_endpoint: null,
+ },
+ ],
+ });
+
+ const { result } = renderHook(
+ () => useGetLocationsForQuotaService('object-storage'),
+ {
+ wrapper,
+ }
+ );
+
+ expect(result.current.s3Endpoints).toEqual([
+ { label: 'Global (Account level)', value: 'global' },
+ { label: 'endpoint1 (Standard E0)', value: 'endpoint1' },
+ ]);
+ });
+
+ it('should return the error for a given quota usage query', () => {
+ const quotaUsageQueries = ([
+ { error: [{ reason: 'Error 1' }] },
+ { error: [{ reason: 'Error 2' }] },
+ ] as unknown) as UseQueryResult[];
+ const index = 0;
+
+ const error = getQuotaError(quotaUsageQueries, index);
+
+ expect(error).toEqual('Error 1');
+ });
+
+ it('getQuotaIncreaseFormDefaultValues should return the correct default values', () => {
+ const profile = profileFactory.build();
+ const baseQuota = quotaFactory.build();
+ const quotaUsage = quotaUsageFactory.build();
+ const quota = {
+ ...baseQuota,
+ ...quotaUsage,
+ };
+ const quantity = 1;
+
+ const defaultValues = getQuotaIncreaseMessage({
+ profile,
+ quantity,
+ quota,
+ });
+
+ expect(defaultValues.description).toEqual(
+ `**User**: ${profile.username}
\n**Email**: ${
+ profile.email
+ }
\n**Quota Name**: ${
+ quota.quota_name
+ }
\n**New Quantity Requested**: ${quantity} ${quota.resource_metric}${
+ quantity > 1 ? 's' : ''
+ }
\n**Region**: ${quota.region_applied}`
+ );
+ });
+});
diff --git a/packages/manager/src/features/Account/Quotas/utils.ts b/packages/manager/src/features/Account/Quotas/utils.ts
new file mode 100644
index 00000000000..29c9c97d993
--- /dev/null
+++ b/packages/manager/src/features/Account/Quotas/utils.ts
@@ -0,0 +1,166 @@
+import { useRegionsQuery } from '@linode/queries';
+import { object, string } from 'yup';
+
+import {
+ GLOBAL_QUOTA_LABEL,
+ GLOBAL_QUOTA_VALUE,
+ regionSelectGlobalOption,
+} from 'src/components/RegionSelect/constants';
+import { useObjectStorageEndpoints } from 'src/queries/object-storage/queries';
+
+import type { QuotaIncreaseFormFields } from './QuotasIncreaseForm';
+import type {
+ Filter,
+ Profile,
+ Quota,
+ QuotaType,
+ QuotaUsage,
+ Region,
+} from '@linode/api-v4';
+import type { SelectOption } from '@linode/ui';
+import type { UseQueryResult } from '@tanstack/react-query';
+
+type UseGetLocationsForQuotaService =
+ | {
+ isFetchingRegions: boolean;
+ regions: Region[];
+ s3Endpoints: null;
+ service: Exclude;
+ }
+ | {
+ isFetchingS3Endpoints: boolean;
+ regions: null;
+ s3Endpoints: { label: string; value: string }[];
+ service: Extract;
+ };
+
+/**
+ * Function to get either:
+ * - The region(s) for a given quota service (linode, lke, ...)
+ * - The s3 endpoint(s) (object-storage)
+ */
+export const useGetLocationsForQuotaService = (
+ service: QuotaType
+): UseGetLocationsForQuotaService => {
+ const { data: regions, isFetching: isFetchingRegions } = useRegionsQuery();
+ // In order to get the s3 endpoints, we need to query the object storage service
+ // It will only show quotas for assigned endpoints (endpoints relevant to a region a user ever created a resource in).
+ const {
+ data: s3Endpoints,
+ isFetching: isFetchingS3Endpoints,
+ } = useObjectStorageEndpoints(service === 'object-storage');
+
+ if (service === 'object-storage') {
+ return {
+ isFetchingS3Endpoints,
+ regions: null,
+ s3Endpoints: [
+ ...[{ label: GLOBAL_QUOTA_LABEL, value: GLOBAL_QUOTA_VALUE }],
+ ...(s3Endpoints ?? [])
+ ?.map((s3Endpoint) => {
+ if (!s3Endpoint.s3_endpoint) {
+ return null;
+ }
+
+ return {
+ label: `${s3Endpoint.s3_endpoint} (Standard ${s3Endpoint.endpoint_type})`,
+ value: s3Endpoint.s3_endpoint,
+ };
+ })
+ .filter((item) => item !== null),
+ ],
+ service: 'object-storage',
+ };
+ }
+
+ return {
+ isFetchingRegions,
+ regions: [regionSelectGlobalOption, ...(regions ?? [])],
+ s3Endpoints: null,
+ service,
+ };
+};
+
+interface GetQuotasFiltersProps {
+ location: SelectOption | null;
+ service: SelectOption;
+}
+
+/**
+ * Function to get the filters for the quotas query
+ */
+export const getQuotasFilters = ({
+ location,
+ service,
+}: GetQuotasFiltersProps): Filter => {
+ return {
+ region_applied:
+ service.value !== 'object-storage' ? location?.value : undefined,
+ s3_endpoint:
+ service.value === 'object-storage' ? location?.value : undefined,
+ };
+};
+
+/**
+ * Function to get the error for a given quota usage query
+ */
+export const getQuotaError = (
+ quotaUsageQueries: UseQueryResult[],
+ index: number
+) => {
+ return Array.isArray(quotaUsageQueries[index].error) &&
+ quotaUsageQueries[index].error[0]?.reason
+ ? quotaUsageQueries[index].error[0].reason
+ : 'An unexpected error occurred';
+};
+
+interface GetQuotaIncreaseFormDefaultValuesProps {
+ profile: Profile | undefined;
+ quantity: number;
+ quota: Quota;
+}
+
+/**
+ * Function to get the default values for the quota increase form
+ */
+export const getQuotaIncreaseMessage = ({
+ profile,
+ quantity,
+ quota,
+}: GetQuotaIncreaseFormDefaultValuesProps): QuotaIncreaseFormFields => {
+ const regionAppliedLabel = quota.s3_endpoint ? 'Endpoint' : 'Region';
+ const regionAppliedValue = quota.s3_endpoint ?? quota.region_applied;
+
+ if (!profile) {
+ return {
+ description: '',
+ notes: '',
+ quantity: '0',
+ summary: 'Increase Quota',
+ };
+ }
+
+ return {
+ description: `**User**: ${profile.username}
\n**Email**: ${
+ profile.email
+ }
\n**Quota Name**: ${
+ quota.quota_name
+ }
\n**New Quantity Requested**: ${quantity} ${quota.resource_metric}${
+ quantity > 1 ? 's' : ''
+ }
\n**${regionAppliedLabel}**: ${regionAppliedValue}`,
+ notes: '',
+ quantity: '0',
+ summary: 'Increase Quota',
+ };
+};
+
+export const getQuotaIncreaseFormSchema = object({
+ description: string().required('Description is required.'),
+ notes: string()
+ .optional()
+ .max(255, 'Notes must be less than 255 characters.'),
+ quantity: string()
+ .required('Quantity is required')
+ .matches(/^[1-9]\d*$/, 'Quantity must be a number greater than 0.'),
+ summary: string().required('Summary is required.'),
+});
diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx
index c9b32879308..9de17a38eca 100644
--- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx
+++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx
@@ -1,20 +1,19 @@
-import { Notice, StyledLinkButton, Typography } from '@linode/ui';
+import { Drawer, Notice, StyledLinkButton, Typography } from '@linode/ui';
import React from 'react';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
-import { Drawer } from 'src/components/Drawer';
+import { NotFound } from 'src/components/NotFound';
import { PARENT_USER_SESSION_EXPIRED } from 'src/features/Account/constants';
import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication';
import { setTokenInLocalStorage } from 'src/features/Account/SwitchAccounts/utils';
-import { useCurrentToken } from 'src/hooks/useAuthentication';
import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEventAnalytics';
+import { getAuthToken } from 'src/utilities/authentication';
import { getStorage, setStorage } from 'src/utilities/storage';
import { ChildAccountList } from './SwitchAccounts/ChildAccountList';
import { updateParentTokenInLocalStorage } from './SwitchAccounts/utils';
import type { APIError, UserType } from '@linode/api-v4';
-import type { State as AuthState } from 'src/store/authentication';
interface Props {
onClose: () => void;
@@ -23,7 +22,7 @@ interface Props {
}
interface HandleSwitchToChildAccountProps {
- currentTokenWithBearer?: AuthState['token'];
+ currentTokenWithBearer?: string;
euuid: string;
event: React.MouseEvent;
onClose: (e: React.SyntheticEvent) => void;
@@ -39,9 +38,9 @@ export const SwitchAccountDrawer = (props: Props) => {
const [query, setQuery] = React.useState('');
const isProxyUser = userType === 'proxy';
- const currentParentTokenWithBearer =
+ const currentParentTokenWithBearer: string =
getStorage('authentication/parent_token/token') ?? '';
- const currentTokenWithBearer = useCurrentToken() ?? '';
+ const currentTokenWithBearer = getAuthToken().token;
const {
createToken,
@@ -124,7 +123,12 @@ export const SwitchAccountDrawer = (props: Props) => {
}, [onClose, revokeToken, validateParentToken, updateCurrentToken]);
return (
-
+
{createTokenErrorReason && (
)}
diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx
index ddc14c0a132..0d204a50c2e 100644
--- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx
+++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx
@@ -1,5 +1,5 @@
-import { Typography } from '@linode/ui';
import {
+ Typography,
Box,
Button,
CircleProgress,
@@ -11,7 +11,7 @@ import React, { useState } from 'react';
import { Waypoint } from 'react-waypoint';
import ErrorStateCloud from 'src/assets/icons/error-state-cloud.svg';
-import { useChildAccountsInfiniteQuery } from 'src/queries/account/account';
+import { useChildAccountsInfiniteQuery } from '@linode/queries';
import type { Filter, UserType } from '@linode/api-v4';
diff --git a/packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.tsx b/packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.tsx
index 9bb00b35195..afbaaf2590f 100644
--- a/packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.tsx
+++ b/packages/manager/src/features/Account/SwitchAccounts/SessionExpirationDialog.tsx
@@ -1,16 +1,14 @@
-import { Typography } from '@linode/ui';
+import { ActionsPanel, Typography } from '@linode/ui';
+import { pluralize, useInterval } from '@linode/utilities';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import { sessionExpirationContext as _sessionExpirationContext } from 'src/context/sessionExpirationContext';
import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication';
import { setTokenInLocalStorage } from 'src/features/Account/SwitchAccounts/utils';
-import { useInterval } from 'src/hooks/useInterval';
-import { useAccount } from 'src/queries/account/account';
+import { useAccount } from '@linode/queries';
import { parseAPIDate } from 'src/utilities/date';
-import { pluralize } from 'src/utilities/pluralize';
import { getStorage, setStorage } from 'src/utilities/storage';
interface SessionExpirationDialogProps {
diff --git a/packages/manager/src/features/Account/SwitchAccounts/SwitchAccountSessionDialog.tsx b/packages/manager/src/features/Account/SwitchAccounts/SwitchAccountSessionDialog.tsx
index 524c1561e9a..5ca66a48388 100644
--- a/packages/manager/src/features/Account/SwitchAccounts/SwitchAccountSessionDialog.tsx
+++ b/packages/manager/src/features/Account/SwitchAccounts/SwitchAccountSessionDialog.tsx
@@ -1,8 +1,7 @@
-import { Typography } from '@linode/ui';
+import { ActionsPanel, Typography } from '@linode/ui';
import React from 'react';
import { useHistory } from 'react-router-dom';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
import { sendSwitchAccountSessionExpiryEvent } from 'src/utilities/analytics/customEventAnalytics';
diff --git a/packages/manager/src/features/Account/SwitchAccounts/useParentChildAuthentication.tsx b/packages/manager/src/features/Account/SwitchAccounts/useParentChildAuthentication.tsx
index 5924c18ec8c..fb8939f393f 100644
--- a/packages/manager/src/features/Account/SwitchAccounts/useParentChildAuthentication.tsx
+++ b/packages/manager/src/features/Account/SwitchAccounts/useParentChildAuthentication.tsx
@@ -2,21 +2,21 @@ import {
deletePersonalAccessToken,
getPersonalAccessTokens,
} from '@linode/api-v4';
+import { useCreateChildAccountPersonalAccessTokenMutation } from '@linode/queries';
import { useCallback } from 'react';
-import { getPersonalAccessTokenForRevocation } from 'src/features/Account/SwitchAccounts/utils';
import {
+ getPersonalAccessTokenForRevocation,
isParentTokenValid,
updateCurrentTokenBasedOnUserType,
} from 'src/features/Account/SwitchAccounts/utils';
-import { useCurrentToken } from 'src/hooks/useAuthentication';
-import { useCreateChildAccountPersonalAccessTokenMutation } from 'src/queries/account/account';
+import { getAuthToken } from 'src/utilities/authentication';
import { getStorage } from 'src/utilities/storage';
import type { Token, UserType } from '@linode/api-v4';
export const useParentChildAuthentication = () => {
- const currentTokenWithBearer = useCurrentToken() ?? '';
+ const currentTokenWithBearer = getAuthToken().token;
const {
error: createTokenError,
diff --git a/packages/manager/src/features/Account/SwitchAccounts/utils.ts b/packages/manager/src/features/Account/SwitchAccounts/utils.ts
index 7484f44042c..8775d74c568 100644
--- a/packages/manager/src/features/Account/SwitchAccounts/utils.ts
+++ b/packages/manager/src/features/Account/SwitchAccounts/utils.ts
@@ -1,7 +1,6 @@
import { getStorage, setStorage } from 'src/utilities/storage';
import type { Token, UserType } from '@linode/api-v4';
-import type { State as AuthState } from 'src/store/authentication';
export interface ProxyTokenCreationParams {
/**
@@ -21,7 +20,7 @@ export interface ProxyTokenCreationParams {
export const updateParentTokenInLocalStorage = ({
currentTokenWithBearer,
}: {
- currentTokenWithBearer?: AuthState['token'];
+ currentTokenWithBearer?: string;
}) => {
const parentToken: Token = {
created: getStorage('authentication/created'),
diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts
index 992e72762e5..575c0ccf20c 100644
--- a/packages/manager/src/features/Account/constants.ts
+++ b/packages/manager/src/features/Account/constants.ts
@@ -4,6 +4,7 @@ export const CUSTOMER_SUPPORT = 'customer support';
export const grantTypeMap = {
account: 'Account',
+ bucket: 'Buckets',
database: 'Databases',
domain: 'Domains',
firewall: 'Firewalls',
diff --git a/packages/manager/src/features/Backups/AutoEnroll.tsx b/packages/manager/src/features/Backups/AutoEnroll.tsx
index 5a1fcb0453c..d898234d868 100644
--- a/packages/manager/src/features/Backups/AutoEnroll.tsx
+++ b/packages/manager/src/features/Backups/AutoEnroll.tsx
@@ -2,11 +2,11 @@ import {
FormControlLabel,
Notice,
Paper,
+ Stack,
Toggle,
Typography,
} from '@linode/ui';
-import { styled } from '@mui/material/styles';
-import * as React from 'react';
+import React from 'react';
import { Link } from 'src/components/Link';
@@ -20,58 +20,35 @@ export const AutoEnroll = (props: AutoEnrollProps) => {
const { enabled, error, toggle } = props;
return (
-
+ ({ backgroundColor: theme.palette.background.default })}
+ variant="outlined"
+ >
{error && }
-
-
+
+ ({ font: theme.font.bold })}>
Auto Enroll All New Linodes in Backups
-
+
Enroll all future Linodes in backups. Your account will be billed
the additional hourly rate noted on the{' '}
Backups pricing page
.
-
+
}
- control={}
+ checked={enabled}
+ control={}
+ onChange={toggle}
+ sx={{ gap: 1 }}
/>
-
+
);
};
-
-const StyledPaper = styled(Paper, {
- label: 'StyledPaper',
-})(({ theme }) => ({
- backgroundColor: theme.bg.offWhite,
- padding: theme.spacing(1),
-}));
-
-const StyledFormControlLabel = styled(FormControlLabel, {
- label: 'StyledFormControlLabel',
-})(({ theme }) => ({
- alignItems: 'flex-start',
- display: 'flex',
- marginBottom: theme.spacing(1),
- marginLeft: 0,
-}));
-
-const StyledDiv = styled('div', {
- label: 'StyledDiv',
-})(({ theme }) => ({
- marginTop: theme.spacing(1.5),
-}));
-
-const StyledTypography = styled(Typography, {
- label: 'StyledTypography',
-})(({ theme }) => ({
- fontSize: 17,
- marginBottom: theme.spacing(1),
-}));
diff --git a/packages/manager/src/features/Backups/BackupDrawer.test.tsx b/packages/manager/src/features/Backups/BackupDrawer.test.tsx
index 26eeecebc10..be8bdcde0f7 100644
--- a/packages/manager/src/features/Backups/BackupDrawer.test.tsx
+++ b/packages/manager/src/features/Backups/BackupDrawer.test.tsx
@@ -23,8 +23,8 @@ const queryMocks = vi.hoisted(() => ({
}),
}));
-vi.mock('src/queries/linodes/linodes', async () => {
- const actual = await vi.importActual('src/queries/linodes/linodes');
+vi.mock('@linode/queries', async () => {
+ const actual = await vi.importActual('@linode/queries');
return {
...actual,
useAllLinodesQuery: queryMocks.useAllLinodesQuery,
diff --git a/packages/manager/src/features/Backups/BackupDrawer.tsx b/packages/manager/src/features/Backups/BackupDrawer.tsx
index 7e8dfefa222..fe4e2eb313c 100644
--- a/packages/manager/src/features/Backups/BackupDrawer.tsx
+++ b/packages/manager/src/features/Backups/BackupDrawer.tsx
@@ -1,12 +1,24 @@
-import { Box, Notice, Stack, Typography } from '@linode/ui';
+import {
+ useAccountSettings,
+ useAllLinodesQuery,
+ useMutateAccountSettings,
+} from '@linode/queries';
+import {
+ ActionsPanel,
+ Box,
+ Drawer,
+ Notice,
+ Stack,
+ Typography,
+} from '@linode/ui';
+import { isNumber, pluralize } from '@linode/utilities';
import { styled } from '@mui/material';
import { useSnackbar } from 'notistack';
import * as React from 'react';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { DisplayPrice } from 'src/components/DisplayPrice';
-import { Drawer } from 'src/components/Drawer';
import { Link } from 'src/components/Link';
+import { NotFound } from 'src/components/NotFound';
import { Table } from 'src/components/Table';
import { TableBody } from 'src/components/TableBody';
import { TableCell } from 'src/components/TableCell';
@@ -14,14 +26,7 @@ import { TableHead } from 'src/components/TableHead';
import { TableRow } from 'src/components/TableRow';
import { TableRowError } from 'src/components/TableRowError/TableRowError';
import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading';
-import {
- useAccountSettings,
- useMutateAccountSettings,
-} from 'src/queries/account/settings';
-import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import { useAllTypes } from 'src/queries/types';
-import { isNumber } from 'src/utilities/isNumber';
-import { pluralize } from 'src/utilities/pluralize';
import { getTotalBackupsPrice } from 'src/utilities/pricing/backups';
import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants';
@@ -87,7 +92,7 @@ export const BackupDrawer = (props: Props) => {
const renderBackupsTable = () => {
if (linodesLoading || typesLoading || accountSettingsLoading) {
- return ;
+ return ;
}
if (linodesError) {
return ;
@@ -143,7 +148,13 @@ all new Linodes will automatically be backed up.`
});
return (
-
+
Three backup slots are executed and rotated automatically: a daily
diff --git a/packages/manager/src/features/Backups/BackupLinodeRow.tsx b/packages/manager/src/features/Backups/BackupLinodeRow.tsx
index 9c47812e7c9..98f229c2efa 100644
--- a/packages/manager/src/features/Backups/BackupLinodeRow.tsx
+++ b/packages/manager/src/features/Backups/BackupLinodeRow.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
import { TableCell } from 'src/components/TableCell';
import { TableRow } from 'src/components/TableRow';
-import { useRegionsQuery } from 'src/queries/regions/regions';
+import { useRegionsQuery } from '@linode/queries';
import { useTypeQuery } from 'src/queries/types';
import { getMonthlyBackupsPrice } from 'src/utilities/pricing/backups';
import {
diff --git a/packages/manager/src/features/Backups/BackupsCTA.tsx b/packages/manager/src/features/Backups/BackupsCTA.tsx
index 5aa84cc5aa8..ed964b91a34 100644
--- a/packages/manager/src/features/Backups/BackupsCTA.tsx
+++ b/packages/manager/src/features/Backups/BackupsCTA.tsx
@@ -3,13 +3,13 @@ import Close from '@mui/icons-material/Close';
import React from 'react';
import { LinkButton } from 'src/components/LinkButton';
-import { useAccountSettings } from 'src/queries/account/settings';
-import { useAllLinodesQuery } from 'src/queries/linodes/linodes';
import {
+ useAccountSettings,
+ useAllLinodesQuery,
useMutatePreferences,
usePreferences,
-} from 'src/queries/profile/preferences';
-import { useProfile } from 'src/queries/profile/profile';
+ useProfile,
+} from '@linode/queries';
import { BackupDrawer } from './BackupDrawer';
diff --git a/packages/manager/src/features/Backups/utils.ts b/packages/manager/src/features/Backups/utils.ts
index 85193d82625..e91d11de260 100644
--- a/packages/manager/src/features/Backups/utils.ts
+++ b/packages/manager/src/features/Backups/utils.ts
@@ -1,9 +1,8 @@
import { enableBackups } from '@linode/api-v4';
+import { linodeQueries } from '@linode/queries';
+import { pluralize } from '@linode/utilities';
import { useMutation, useQueryClient } from '@tanstack/react-query';
-import { linodeQueries } from 'src/queries/linodes/linodes';
-import { pluralize } from 'src/utilities/pluralize';
-
import type { APIError, Linode } from '@linode/api-v4';
interface EnableBackupsFufilledResult extends PromiseFulfilledResult<{}> {
diff --git a/packages/manager/src/features/Betas/BetaDetailsList.tsx b/packages/manager/src/features/Betas/BetaDetailsList.tsx
index 3b209df9705..afea748e3a2 100644
--- a/packages/manager/src/features/Betas/BetaDetailsList.tsx
+++ b/packages/manager/src/features/Betas/BetaDetailsList.tsx
@@ -1,8 +1,13 @@
-import { CircleProgress, Divider, Paper, Stack, Typography } from '@linode/ui';
+import {
+ CircleProgress,
+ Divider,
+ ErrorState,
+ Paper,
+ Stack,
+ Typography,
+} from '@linode/ui';
import * as React from 'react';
-import { ErrorState } from 'src/components/ErrorState/ErrorState';
-
import BetaDetails from './BetaDetails';
import type { APIError } from '@linode/api-v4';
diff --git a/packages/manager/src/features/Betas/BetaSignup.tsx b/packages/manager/src/features/Betas/BetaSignup.tsx
index 01e72251575..bd54fcc62bb 100644
--- a/packages/manager/src/features/Betas/BetaSignup.tsx
+++ b/packages/manager/src/features/Betas/BetaSignup.tsx
@@ -1,4 +1,11 @@
-import { Checkbox, CircleProgress, Paper, Stack, Typography } from '@linode/ui';
+import {
+ ActionsPanel,
+ Checkbox,
+ CircleProgress,
+ Paper,
+ Stack,
+ Typography,
+} from '@linode/ui';
import {
createLazyRoute,
useNavigate,
@@ -7,11 +14,10 @@ import {
import { useSnackbar } from 'notistack';
import * as React from 'react';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { LandingHeader } from 'src/components/LandingHeader/LandingHeader';
import { Markdown } from 'src/components/Markdown/Markdown';
import { NotFound } from 'src/components/NotFound';
-import { useCreateAccountBetaMutation } from 'src/queries/account/betas';
+import { useCreateAccountBetaMutation } from '@linode/queries';
import { useBetaQuery } from 'src/queries/betas';
export const BetaSignup = () => {
diff --git a/packages/manager/src/features/Betas/BetasLanding.tsx b/packages/manager/src/features/Betas/BetasLanding.tsx
index 62de9908a22..028fc7dbf8e 100644
--- a/packages/manager/src/features/Betas/BetasLanding.tsx
+++ b/packages/manager/src/features/Betas/BetasLanding.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
import { LandingHeader } from 'src/components/LandingHeader/LandingHeader';
import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner';
import { BetaDetailsList } from 'src/features/Betas/BetaDetailsList';
-import { useAccountBetasQuery } from 'src/queries/account/betas';
+import { useAccountBetasQuery } from '@linode/queries';
import { useBetasQuery } from 'src/queries/betas';
import { categorizeBetasByStatus } from 'src/utilities/betaUtils';
diff --git a/packages/manager/src/features/Billing/BillingDetail.tsx b/packages/manager/src/features/Billing/BillingDetail.tsx
index df85a4e5040..138b31e6ca7 100644
--- a/packages/manager/src/features/Billing/BillingDetail.tsx
+++ b/packages/manager/src/features/Billing/BillingDetail.tsx
@@ -1,16 +1,17 @@
-import { Button, CircleProgress } from '@linode/ui';
+import { Button, CircleProgress, ErrorState } from '@linode/ui';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
-import Grid from '@mui/material/Unstable_Grid2';
+import Grid from '@mui/material/Grid2';
import { PayPalScriptProvider } from '@paypal/react-paypal-js';
import * as React from 'react';
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
-import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { PAYPAL_CLIENT_ID } from 'src/constants';
-import { useAccount } from 'src/queries/account/account';
-import { useAllPaymentMethodsQuery } from 'src/queries/account/payment';
-import { useProfile } from 'src/queries/profile/profile';
+import {
+ useAccount,
+ useAllPaymentMethodsQuery,
+ useProfile,
+} from '@linode/queries';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import { BillingActivityPanel } from './BillingPanels/BillingActivityPanel/BillingActivityPanel';
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx
index b1cd0138ae2..ff13ce46c09 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingActivityPanel/BillingActivityPanel.tsx
@@ -1,8 +1,16 @@
import { getInvoiceItems } from '@linode/api-v4/lib/account';
+import {
+ useAccount,
+ useAllAccountInvoices,
+ useAllAccountPayments,
+ useProfile,
+ useRegionsQuery,
+} from '@linode/queries';
import { Autocomplete, Typography } from '@linode/ui';
+import { getAll, useSet } from '@linode/utilities';
+import Grid from '@mui/material/Grid2';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
-import Grid from '@mui/material/Unstable_Grid2';
import { DateTime } from 'luxon';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';
@@ -32,17 +40,8 @@ import {
import { useFlags } from 'src/hooks/useFlags';
import { useOrder } from 'src/hooks/useOrder';
import { usePagination } from 'src/hooks/usePagination';
-import { useSet } from 'src/hooks/useSet';
-import { useAccount } from 'src/queries/account/account';
-import {
- useAllAccountInvoices,
- useAllAccountPayments,
-} from 'src/queries/account/billing';
-import { useProfile } from 'src/queries/profile/profile';
-import { useRegionsQuery } from 'src/queries/regions/regions';
import { parseAPIDate } from 'src/utilities/date';
import { formatDate } from 'src/utilities/formatDate';
-import { getAll } from 'src/utilities/getAll';
import { getTaxID } from '../../billingUtils';
@@ -385,7 +384,7 @@ export const BillingActivityPanel = React.memo((props: Props) => {
};
return (
-
+
{
};
// The layout changes if there are promotions.
- const gridDimensions: Partial> =
+ const gridDimensions =
promotions && promotions.length > 0 ? { md: 4, xs: 12 } : { sm: 6, xs: 12 };
const balanceJSX =
@@ -156,8 +153,20 @@ export const BillingSummary = (props: BillingSummaryProps) => {
return (
<>
-
-
+
+
Account Balance
@@ -202,7 +211,13 @@ export const BillingSummary = (props: BillingSummaryProps) => {
{promotions && promotions?.length > 0 ? (
-
+
Promotions
@@ -218,7 +233,7 @@ export const BillingSummary = (props: BillingSummaryProps) => {
) : null}
-
+
Accrued Charges
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx
index d95feb817ad..9e76162ff88 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/GooglePayButton.tsx
@@ -1,5 +1,7 @@
+import { useAccount, useClientToken } from '@linode/queries';
import { CircleProgress, Tooltip } from '@linode/ui';
-import Grid from '@mui/material/Unstable_Grid2';
+import { useScript } from '@linode/utilities';
+import Grid from '@mui/material/Grid2';
import { useQueryClient } from '@tanstack/react-query';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';
@@ -10,9 +12,6 @@ import {
gPay,
initGooglePaymentInstance,
} from 'src/features/Billing/GooglePayProvider';
-import { useScript } from 'src/hooks/useScript';
-import { useAccount } from 'src/queries/account/account';
-import { useClientToken } from 'src/queries/account/payment';
import type { SetSuccess } from './types';
import type { APIWarning } from '@linode/api-v4/lib/types';
@@ -146,10 +145,12 @@ export const GooglePayButton = (props: Props) => {
if (isLoading) {
return (
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx
index 9af3f088734..c74bca1d5fa 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PayPalButton.tsx
@@ -1,6 +1,6 @@
import { makePayment } from '@linode/api-v4/lib/account/payments';
import { CircleProgress, Tooltip } from '@linode/ui';
-import Grid from '@mui/material/Unstable_Grid2';
+import Grid from '@mui/material/Grid2';
import {
BraintreePayPalButtons,
FUNDING,
@@ -12,9 +12,7 @@ import { makeStyles } from 'tss-react/mui';
import { reportException } from 'src/exceptionReporting';
import { getPaymentLimits } from 'src/features/Billing/billingUtils';
-import { useAccount } from 'src/queries/account/account';
-import { useClientToken } from 'src/queries/account/payment';
-import { accountQueries } from 'src/queries/account/queries';
+import { useAccount, useClientToken, accountQueries } from '@linode/queries';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
import type { SetSuccess } from './types';
@@ -220,10 +218,12 @@ export const PayPalButton = (props: Props) => {
if (clientTokenLoading || isPending || !options['data-client-token']) {
return (
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx
index bd28c8bb7da..3152be8c4df 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentBits/CreditCardDialog.tsx
@@ -1,7 +1,6 @@
-import { Typography } from '@linode/ui';
+import { ActionsPanel, Typography } from '@linode/ui';
import * as React from 'react';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog';
interface Actions {
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.test.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.test.tsx
index 0133dffcccf..d8c144f7f16 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.test.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.test.tsx
@@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
import * as React from 'react';
import { paymentFactory } from 'src/factories/billing';
-import { http, HttpResponse, server } from 'src/mocks/testServer';
+import { HttpResponse, http, server } from 'src/mocks/testServer';
import { wrapWithTheme } from 'src/utilities/testHelpers';
import PaymentDrawer, { getMinimumPayment } from './PaymentDrawer';
diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx
index 8cd4021fa94..9ebed4c1dcd 100644
--- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentDrawer.tsx
@@ -1,30 +1,29 @@
import { makePayment } from '@linode/api-v4/lib/account';
-import { Typography } from '@linode/ui';
+import { accountQueries, useAccount, useProfile } from '@linode/queries';
import {
Button,
Divider,
+ Drawer,
+ ErrorState,
InputAdornment,
Notice,
Stack,
TextField,
TooltipIcon,
+ Typography,
} from '@linode/ui';
-import Grid from '@mui/material/Unstable_Grid2';
+import Grid from '@mui/material/Grid2';
import { useQueryClient } from '@tanstack/react-query';
import { useSnackbar } from 'notistack';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';
import { Currency } from 'src/components/Currency';
-import { Drawer } from 'src/components/Drawer';
-import { ErrorState } from 'src/components/ErrorState/ErrorState';
import { LinearProgress } from 'src/components/LinearProgress';
+import { NotFound } from 'src/components/NotFound';
import { SupportLink } from 'src/components/SupportLink';
import { getRestrictedResourceText } from 'src/features/Account/utils';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
-import { useAccount } from 'src/queries/account/account';
-import { accountQueries } from 'src/queries/account/queries';
-import { useProfile } from 'src/queries/profile/profile';
import { isCreditCardExpired } from 'src/utilities/creditCard';
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils';
@@ -239,7 +238,12 @@ export const PaymentDrawer = (props: Props) => {
}
return (
-
+
{isReadOnly && (
{
-
+
{
/>
-
+
{
const renderVariant = () => {
return is_default ? (
-
+
) : null;
};
return (
-
+
{
- const actual = await vi.importActual('src/queries/profile/profile');
+vi.mock('@linode/queries', async () => {
+ const actual = await vi.importActual('@linode/queries');
return {
...actual,
useGrants: queryMocks.useGrants,
diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx
index 2c85cd0b195..123bf8f2de8 100644
--- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/ContactInformation.tsx
@@ -1,5 +1,5 @@
import { Box, TooltipIcon, Typography } from '@linode/ui';
-import Grid from '@mui/material/Unstable_Grid2';
+import Grid from '@mui/material/Grid2';
import { allCountries } from 'country-region-data';
import * as React from 'react';
import { useState } from 'react';
@@ -10,8 +10,7 @@ import { getRestrictedResourceText } from 'src/features/Account/utils';
import { EDIT_BILLING_CONTACT } from 'src/features/Billing/constants';
import { StyledAutorenewIcon } from 'src/features/TopMenu/NotificationMenu/NotificationMenu';
import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck';
-import { useNotificationsQuery } from 'src/queries/account/notifications';
-import { usePreferences } from 'src/queries/profile/preferences';
+import { useNotificationsQuery, usePreferences } from '@linode/queries';
import {
BillingActionButton,
@@ -153,7 +152,12 @@ export const ContactInformation = React.memo((props: Props) => {
};
return (
-
+
Billing Contact
diff --git a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/EditBillingContactDrawer.tsx b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/EditBillingContactDrawer.tsx
index fd93ea66293..65537fc3e3d 100644
--- a/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/EditBillingContactDrawer.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/ContactInfoPanel/EditBillingContactDrawer.tsx
@@ -1,7 +1,8 @@
+import { Drawer } from '@linode/ui';
import * as React from 'react';
import { makeStyles } from 'tss-react/mui';
-import { Drawer } from 'src/components/Drawer';
+import { NotFound } from 'src/components/NotFound';
import UpdateContactInformationForm from './UpdateContactInformationForm';
@@ -29,6 +30,7 @@ export const BillingContactDrawer = (props: Props) => {
return (
{
const { data: account } = useAccount();
const { error, isPending, mutateAsync } = useMutateAccount();
+ const queryClient = useQueryClient();
const { data: notifications, refetch } = useNotificationsQuery();
const { mutateAsync: updateAccountAgreements } = useMutateAccountAgreements();
const { classes } = useStyles();
@@ -81,7 +89,38 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
delete clonedValues.company;
}
- await mutateAsync(clonedValues);
+ await mutateAsync(clonedValues, {
+ onSuccess: (account) => {
+ queryClient.setQueryData(
+ accountQueries.account.queryKey,
+ (prevAccount) => {
+ if (!prevAccount) {
+ return account;
+ }
+
+ if (
+ isTaxIdEnabled &&
+ account.tax_id &&
+ account.country !== 'US' &&
+ prevAccount?.tax_id !== account.tax_id
+ ) {
+ enqueueSnackbar(
+ "You edited the Tax Identification Number. It's being verified. You'll get an email with the verification result.",
+ {
+ hideIconVariant: false,
+ variant: 'info',
+ }
+ );
+ queryClient.invalidateQueries({
+ queryKey: accountQueries.notifications.queryKey,
+ });
+ }
+
+ return account;
+ }
+ );
+ },
+ });
if (billingAgreementChecked) {
try {
@@ -223,7 +262,7 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
spacing={0}
>
{isReadOnly && (
-
+
{
)}
{generalError && (
-
+
)}
-
+
{
value={formik.values.email}
/>
-
+
{
value={formik.values.first_name}
/>
-
+
{
value={formik.values.last_name}
/>
-
+
{
value={formik.values.company}
/>
-
+
{
value={formik.values.address_1}
/>
-
+
{
/>
-
+
{
placeholder="Select a Country"
/>
-
+
{formik.values.country === 'US' || formik.values.country == 'CA' ? (
@@ -372,7 +431,12 @@ const UpdateContactInformationForm = ({ focusEmail, onClose }: Props) => {
/>
)}
-
+
{
value={formik.values.city}
/>
-
+
{
value={formik.values.zip}
/>
-
+
{
value={formik.values.phone}
/>
-
+
{
{nonUSCountry && (
theme.tokens.spacing[60]}
- xs={12}
+ sx={{
+ alignItems: 'flex-start',
+ display: 'flex',
+ marginTop: (theme) => theme.tokens.spacing.S16,
+ }}
+ size={12}
>
setBillingAgreementChecked(!billingAgreementChecked)
}
sx={(theme) => ({
- marginRight: theme.tokens.spacing[40],
+ marginRight: theme.tokens.spacing.S8,
padding: 0,
})}
checked={billingAgreementChecked}
diff --git a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx
index 465c01d0380..014869786d3 100644
--- a/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx
+++ b/packages/manager/src/features/Billing/BillingPanels/PaymentInfoPanel/AddPaymentMethodDrawer/AddCreditCardForm.tsx
@@ -1,7 +1,8 @@
import { addPaymentMethod } from '@linode/api-v4/lib';
-import { Notice, TextField } from '@linode/ui';
+import { accountQueries } from '@linode/queries';
+import { ActionsPanel, Notice, TextField } from '@linode/ui';
import { CreditCardSchema } from '@linode/validation';
-import Grid from '@mui/material/Unstable_Grid2';
+import Grid from '@mui/material/Grid2';
import { useQueryClient } from '@tanstack/react-query';
import { useFormik, yupToFormErrors } from 'formik';
import { useSnackbar } from 'notistack';
@@ -9,8 +10,6 @@ import * as React from 'react';
import NumberFormat from 'react-number-format';
import { makeStyles } from 'tss-react/mui';
-import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel';
-import { accountQueries } from 'src/queries/account/queries';
import { parseExpiryYear } from 'src/utilities/creditCard';
import { handleAPIErrors } from 'src/utilities/formikErrorUtils';
@@ -165,12 +164,12 @@ const AddCreditCardForm = (props: Props) => {
return (