From 2d1365115f51d4dcc6fe46619a34f863e22924f3 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Wed, 19 Mar 2025 17:39:19 -0400 Subject: [PATCH 01/84] refactor: [M3-9531] - Nodebalancer routing (Tanstack) (#11858) * Save progress * save mooaaar progress * Route delete modal * cleanup * tests * settings modals * cleanup * moaar cleanup * moaar cleanup * Added changeset: Nodebalancer routing (Tanstack) * Fix test failure * @feedback @dwiley-akamai * @feedback @dwiley-akamai --- .../pr-11858-tech-stories-1742221656603.md | 5 + packages/manager/.eslintrc.cjs | 1 + packages/manager/src/MainContent.tsx | 7 -- .../Devices/RemoveDeviceDialog.tsx | 22 +++- .../NodeBalancers/NodeBalancerCreate.test.tsx | 12 ++ .../NodeBalancers/NodeBalancerCreate.tsx | 17 +-- .../NodeBalancerDeleteDialog.test.tsx | 22 +++- .../NodeBalancerDeleteDialog.tsx | 42 ++++--- .../NodeBalancerConfigurations.test.tsx | 3 + .../NodeBalancerConfigurations.tsx | 72 ++++-------- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 89 +++++++------- .../NodeBalancerFirewalls.test.tsx | 17 +++ .../NodeBalancerFirewalls.tsx | 110 +++++++++++++----- .../NodeBalancerFirewallsActionMenu.tsx | 2 +- .../NodeBalancerFirewallsRow.test.tsx | 3 +- .../NodeBalancerFirewallsRow.tsx | 21 +--- .../NodeBalancerSettings.test.tsx | 14 +++ .../NodeBalancerSettings.tsx | 63 +++++----- .../NodeBalancerSummary.tsx | 18 +-- .../NodeBalancerSummary/SummaryPanel.test.tsx | 11 +- .../NodeBalancerSummary/SummaryPanel.tsx | 19 +-- .../NodeBalancerSummary/TablesPanel.test.tsx | 12 +- .../NodeBalancerSummary/TablesPanel.tsx | 9 +- .../features/NodeBalancers/NodeBalancers.tsx | 37 ------ .../NodeBalancerActionMenu.test.tsx | 17 ++- .../NodeBalancerActionMenu.tsx | 29 +++-- .../NodeBalancerTableRow.test.tsx | 19 ++- .../NodeBalancerTableRow.tsx | 14 +-- .../NodeBalancersLanding.test.tsx | 21 +++- .../NodeBalancersLanding.tsx | 58 ++++----- .../NodeBalancersLandingEmptyState.test.tsx | 12 ++ .../NodeBalancersLandingEmptyState.tsx | 9 +- packages/manager/src/routes/index.tsx | 1 + .../manager/src/routes/nodeBalancers/index.ts | 108 +++++++++++------ .../nodeBalancers/nodeBalancersLazyRoutes.ts | 21 ++++ packages/queries/src/firewalls/firewalls.ts | 19 ++- 36 files changed, 579 insertions(+), 377 deletions(-) create mode 100644 packages/manager/.changeset/pr-11858-tech-stories-1742221656603.md delete mode 100644 packages/manager/src/features/NodeBalancers/NodeBalancers.tsx create mode 100644 packages/manager/src/routes/nodeBalancers/nodeBalancersLazyRoutes.ts diff --git a/packages/manager/.changeset/pr-11858-tech-stories-1742221656603.md b/packages/manager/.changeset/pr-11858-tech-stories-1742221656603.md new file mode 100644 index 00000000000..1299496d790 --- /dev/null +++ b/packages/manager/.changeset/pr-11858-tech-stories-1742221656603.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Nodebalancer routing (Tanstack) ([#11858](https://github.com/linode/manager/pull/11858)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 2ad77920b50..4f128444c4b 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -146,6 +146,7 @@ module.exports = { 'src/features/Firewalls/**/*', 'src/features/Images/**/*', 'src/features/Longview/**/*', + 'src/features/NodeBalancers/**/*', 'src/features/PlacementGroups/**/*', 'src/features/StackScripts/**/*', 'src/features/Volumes/**/*', diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index d078cf5e730..02c79b145d4 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -129,9 +129,6 @@ const Profile = React.lazy(() => default: module.Profile, })) ); -const NodeBalancers = React.lazy( - () => import('src/features/NodeBalancers/NodeBalancers') -); const SupportTickets = React.lazy( () => import('src/features/Support/SupportTickets') ); @@ -368,10 +365,6 @@ export const MainContent = () => { }> - void; onService: boolean | undefined; open: boolean; } export const RemoveDeviceDialog = React.memo((props: Props) => { - const { device, firewallId, firewallLabel, onClose, onService, open } = props; + const { + device, + firewallId, + firewallLabel, + isFetching, + onClose, + onService, + open, + } = props; const { enqueueSnackbar } = useSnackbar(); const deviceType = device?.entity.type; @@ -105,6 +114,7 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { /> } error={error?.[0]?.reason} + isFetching={isFetching} onClose={onClose} open={open} title={dialogTitle} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx index 3a2f0a171aa..8c2ee800cd0 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.test.tsx @@ -4,6 +4,18 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import NodeBalancerCreate from './NodeBalancerCreate'; +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(() => vi.fn()), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + }; +}); + // Note: see nodeblaancers-create-in-complex-form.spec.ts for an e2e test of this flow describe('NodeBalancerCreate', () => { it('renders all parts of the NodeBalancerCreate page', () => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index 6f897919c98..fe520fdb23c 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -20,10 +20,9 @@ import { import { scrollErrorIntoView } from '@linode/utilities'; import { useTheme } from '@mui/material'; import useMediaQuery from '@mui/material/useMediaQuery'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useNavigate } from '@tanstack/react-router'; import { append, clone, compose, defaultTo, lensPath, over } from 'ramda'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { CheckoutSummary } from 'src/components/CheckoutSummary/CheckoutSummary'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; @@ -98,6 +97,7 @@ const defaultFieldsStates = { }; const NodeBalancerCreate = () => { + const navigate = useNavigate(); const { data: agreements } = useAccountAgreements(); const { data: profile } = useProfile(); const { data: regions } = useRegionsQuery(); @@ -109,8 +109,6 @@ const NodeBalancerCreate = () => { mutateAsync: createNodeBalancer, } = useNodebalancerCreateMutation(); - const history = useHistory(); - const [ nodeBalancerFields, setNodeBalancerFields, @@ -303,7 +301,10 @@ const NodeBalancerCreate = () => { createNodeBalancer(nodeBalancerRequestData) .then((nodeBalancer) => { - history.push(`/nodebalancers/${nodeBalancer.id}/summary`); + navigate({ + params: { id: String(nodeBalancer.id) }, + to: '/nodebalancers/$id/summary', + }); // Analytics Event sendCreateNodeBalancerEvent(`Region: ${nodeBalancer.region}`); }) @@ -807,10 +808,4 @@ export const fieldErrorsToNodePathErrors = (errors: APIError[]) => { }, []); }; -export const nodeBalancerCreateLazyRoute = createLazyRoute( - '/nodebalancers/create' -)({ - component: NodeBalancerCreate, -}); - export default NodeBalancerCreate; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx index 765997408f1..fc5c22b3744 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx @@ -1,6 +1,7 @@ import userEvent from '@testing-library/user-event'; import * as React from 'react'; +import { nodeBalancerFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancerDeleteDialog } from './NodeBalancerDeleteDialog'; @@ -8,15 +9,17 @@ import { NodeBalancerDeleteDialog } from './NodeBalancerDeleteDialog'; import type { ManagerPreferences } from '@linode/utilities'; const props = { - id: 1, - label: 'nb-1', - onClose: vi.fn(), + isFetching: false, open: true, + selectedNodeBalancer: nodeBalancerFactory.build(), }; const preference: ManagerPreferences['type_to_confirm'] = true; +const navigate = vi.fn(); const queryMocks = vi.hoisted(() => ({ + useMatch: vi.fn(() => ({})), + useNavigate: vi.fn(() => navigate), usePreferences: vi.fn().mockReturnValue({}), })); @@ -28,6 +31,15 @@ vi.mock('@linode/queries', async () => { }; }); +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useMatch: queryMocks.useMatch, + useNavigate: queryMocks.useNavigate, + }; +}); + queryMocks.usePreferences.mockReturnValue({ data: preference, }); @@ -50,7 +62,7 @@ describe('NodeBalancerDeleteDialog', () => { 'Traffic will no longer be routed through this NodeBalancer. Please check your DNS settings and either provide the IP address of another active NodeBalancer, or route traffic directly to your Linode.' ) ).toBeVisible(); - expect(getByText('Delete nb-1?')).toBeVisible(); + expect(getByText('Delete nodebalancer-id-1?')).toBeVisible(); expect(getByText('NodeBalancer Label')).toBeVisible(); expect(getByText('Cancel')).toBeVisible(); expect(getByText('Delete')).toBeVisible(); @@ -62,6 +74,6 @@ describe('NodeBalancerDeleteDialog', () => { ); await userEvent.click(getByText('Cancel')); - expect(props.onClose).toHaveBeenCalled(); + expect(navigate).toHaveBeenCalled(); }); }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx index 42afe967835..1ac0f721fa4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.tsx @@ -1,30 +1,36 @@ +import { useNodebalancerDeleteMutation } from '@linode/queries'; import { Notice, Typography } from '@linode/ui'; +import { useMatch, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { useNodebalancerDeleteMutation } from '@linode/queries'; + +import type { NodeBalancer } from '@linode/api-v4'; interface Props { - id: number; - label: string; - onClose: () => void; + isFetching: boolean; open: boolean; + selectedNodeBalancer: NodeBalancer | undefined; } export const NodeBalancerDeleteDialog = ({ - id, - label, - onClose, + isFetching, open, + selectedNodeBalancer, }: Props) => { - const { error, isPending, mutateAsync } = useNodebalancerDeleteMutation(id); - const { push } = useHistory(); + const navigate = useNavigate(); + const match = useMatch({ + strict: false, + }); + const { error, isPending, mutateAsync } = useNodebalancerDeleteMutation( + selectedNodeBalancer?.id ?? -1 + ); + + const label = selectedNodeBalancer?.label; const onDelete = async () => { await mutateAsync(); - onClose(); - push('/nodebalancers'); + navigate({ to: '/nodebalancers' }); }; return ( @@ -35,12 +41,20 @@ export const NodeBalancerDeleteDialog = ({ primaryBtnText: 'Delete', type: 'NodeBalancer', }} + onClose={ + match.routeId === '/nodebalancers/$id/settings/delete' + ? () => + navigate({ + params: { id: String(selectedNodeBalancer?.id) }, + to: '/nodebalancers/$id/settings', + }) + : () => navigate({ to: '/nodebalancers' }) + } errors={error ?? undefined} expand label={'NodeBalancer Label'} - loading={isPending} + loading={isPending || isFetching} onClick={onDelete} - onClose={onClose} open={open} title={`Delete ${label}?`} typographyStyle={{ marginTop: '20px' }} diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx index 037fea00ea3..38f94168524 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx @@ -16,6 +16,9 @@ const props = { grants: undefined, nodeBalancerLabel: 'nb-1', nodeBalancerRegion: 'us-east', + params: { + nodeBalancerId: '1', + }, }; const loadingTestId = 'circle-progress'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx index fe5cc8986a4..e94811cd831 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.tsx @@ -23,7 +23,6 @@ import { view, } from 'ramda'; import * as React from 'react'; -import { withRouter } from 'react-router-dom'; import { compose as composeC } from 'recompose'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; @@ -54,7 +53,6 @@ import type { NodeBalancerConfigNode, } from '@linode/api-v4'; import type { Lens } from 'ramda'; -import type { RouteComponentProps } from 'react-router-dom'; import type { PromiseLoaderResponse } from 'src/components/PromiseLoader/PromiseLoader'; import type { WithQueryClientProps } from 'src/containers/withQueryClient.container'; @@ -81,17 +79,18 @@ const StyledConfigsButton = styled(Button, { }, })); -interface Props { +export interface NodeBalancerConfigurationsBaseProps { grants: Grants | undefined; nodeBalancerLabel: string; nodeBalancerRegion: string; } -interface MatchProps { - configId?: string; - nodeBalancerId?: string; +interface Params { + params: { + configId?: string; + id: string; + }; } -type RouteProps = RouteComponentProps; interface PreloadedProps { configs: PromiseLoaderResponse; @@ -122,8 +121,8 @@ interface NodeBalancerConfigWithNodes extends NodeBalancerConfig { } interface NodeBalancerConfigurationsProps - extends Props, - RouteProps, + extends NodeBalancerConfigurationsBaseProps, + Params, PreloadedProps, WithQueryClientProps {} @@ -268,11 +267,7 @@ class NodeBalancerConfigurations extends React.Component< .join(','); createNode = (configIdx: number, nodeIdx: number) => { - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; const config = this.state.configs[configIdx]; const node = this.state.configs[configIdx].nodes[nodeIdx]; @@ -330,11 +325,7 @@ class NodeBalancerConfigurations extends React.Component< }, }); - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; if (!nodeBalancerId) { return; @@ -377,11 +368,7 @@ class NodeBalancerConfigurations extends React.Component< }; deleteNode = (configIdx: number, nodeIdx: number) => { - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; if (!nodeBalancerId) { return; @@ -491,7 +478,7 @@ class NodeBalancerConfigurations extends React.Component< isNodeBalancerReadOnly = () => { const { grants } = this.props; - const { nodeBalancerId } = this.props.match.params; + const { id: nodeBalancerId } = this.props.params; return Boolean( grants?.nodebalancer?.some( (grant) => @@ -579,7 +566,7 @@ class NodeBalancerConfigurations extends React.Component< const lensTo = lensFrom(['configs', idx]); // Check whether config is expended based on the URL - const expandedConfigId = this.props.match.params.configId; + const expandedConfigId = this.props.params.configId; const isExpanded = expandedConfigId ? parseInt(expandedConfigId, 10) === config.id : false; @@ -734,11 +721,7 @@ class NodeBalancerConfigurations extends React.Component< * subsequent saves. */ - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; if (!nodeBalancerId) { return; @@ -839,11 +822,7 @@ class NodeBalancerConfigurations extends React.Component< configPayload: NodeBalancerConfigFieldsWithStatus ) => { /* Update a config and its nodes simultaneously */ - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; if (!nodeBalancerId) { return; @@ -1021,11 +1000,7 @@ class NodeBalancerConfigurations extends React.Component< }; updateNode = (configIdx: number, nodeIdx: number) => { - const { - match: { - params: { nodeBalancerId }, - }, - } = this.props; + const { id: nodeBalancerId } = this.props.params; const config = this.state.configs[configIdx]; const node = this.state.configs[configIdx].nodes[nodeIdx]; @@ -1152,19 +1127,14 @@ class NodeBalancerConfigurations extends React.Component< const preloaded = PromiseLoader({ configs: (props) => { - const { - match: { - params: { nodeBalancerId }, - }, - } = props; + const { id: nodeBalancerId } = props.params; return getConfigsWithNodes(+nodeBalancerId!); }, }); -const enhanced = composeC( - withRouter, - preloaded, - withQueryClient -); +const enhanced = composeC< + NodeBalancerConfigurationsProps, + NodeBalancerConfigurationsBaseProps +>(preloaded, withQueryClient); export default enhanced(NodeBalancerConfigurations); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 8bf4e870eb6..3a5564785d0 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -4,42 +4,41 @@ import { useNodebalancerUpdateMutation, } from '@linode/queries'; import { CircleProgress, ErrorState, Notice } from '@linode/ui'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useMatch, useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { - matchPath, - useHistory, - useLocation, - useParams, -} from 'react-router-dom'; import { LandingHeader } from 'src/components/LandingHeader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; -import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import { useTabs } from 'src/hooks/useTabs'; import { getErrorMap } from 'src/utilities/errorUtils'; import NodeBalancerConfigurations from './NodeBalancerConfigurations'; import { NodeBalancerSettings } from './NodeBalancerSettings'; import { NodeBalancerSummary } from './NodeBalancerSummary/NodeBalancerSummary'; +import type { NodeBalancerConfigurationsBaseProps } from './NodeBalancerConfigurations'; + export const NodeBalancerDetail = () => { - const history = useHistory(); - const location = useLocation(); - const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); - const id = Number(nodeBalancerId); + const { id } = useParams({ + strict: false, + }); const [label, setLabel] = React.useState(); const { data: grants } = useGrants(); const { error: updateError, mutateAsync: updateNodeBalancer, - } = useNodebalancerUpdateMutation(id); + } = useNodebalancerUpdateMutation(Number(id)); - const { data: nodebalancer, error, isLoading } = useNodeBalancerQuery(id); + const { data: nodebalancer, error, isLoading } = useNodeBalancerQuery( + Number(id), + Boolean(id) + ); const isNodeBalancerReadOnly = useIsResourceRestricted({ grantLevel: 'read_only', @@ -57,23 +56,20 @@ export const NodeBalancerDetail = () => { setLabel(nodebalancer?.label); }; - const tabs = [ + const { handleTabChange, tabIndex, tabs } = useTabs([ { - routeName: `/nodebalancers/${id}/summary`, title: 'Summary', + to: '/nodebalancers/$id/summary', }, { - routeName: `/nodebalancers/${id}/configurations`, title: 'Configurations', + to: '/nodebalancers/$id/configurations', }, { - routeName: `/nodebalancers/${id}/settings`, title: 'Settings', + to: '/nodebalancers/$id/settings', }, - ]; - - const matches = (pathName: string) => - Boolean(matchPath(location.pathname, { path: pathName })); + ]); if (isLoading) { return ; @@ -94,10 +90,6 @@ export const NodeBalancerDetail = () => { const nodeBalancerLabel = label !== undefined ? label : nodebalancer?.label; - const navToURL = (index: number) => { - history.push(tabs[index].routeName); - }; - return ( { variant="warning" /> )} - matches(tab.routeName)), - 0 - )} - onChange={navToURL} - > - + + - { ); }; -export const nodeBalancerDetailLazyRoute = createLazyRoute( - '/nodebalancers/$nodeBalancerId' -)({ - component: NodeBalancerDetail, -}); +const NodeBalancerConfigurationWrapper = ( + props: NodeBalancerConfigurationsBaseProps +) => { + const { configId, id: nodeBalancerId } = useParams({ + strict: false, + }); + const match = useMatch({ + strict: false, + }); + + if ( + (match.routeId === '/nodebalancers/$id/configurations' && + !nodeBalancerId) || + (!configId && + match.routeId === '/nodebalancers/$id/configurations/$configId') + ) { + return null; + } + + const matchProps = { + params: { + configId, + id: nodeBalancerId, + }, + }; + + return ; +}; export default NodeBalancerDetail; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.test.tsx index 1f980b22484..b8e5292c811 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.test.tsx @@ -9,8 +9,12 @@ const firewall = firewallFactory.build({ label: 'mock-firewall-1' }); // Set up various mocks for tests +const navigate = vi.fn(); const queryMocks = vi.hoisted(() => ({ + useMatch: vi.fn(() => ({})), + useNavigate: vi.fn(() => navigate), useNodeBalancersFirewallsQuery: vi.fn().mockReturnValue({ data: undefined }), + useParams: vi.fn(() => ({})), })); vi.mock('@linode/queries', async () => { @@ -21,6 +25,16 @@ vi.mock('@linode/queries', async () => { }; }); +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useMatch: queryMocks.useMatch, + useNavigate: queryMocks.useNavigate, + useParams: queryMocks.useParams, + }; +}); + const props = { displayFirewallInfoText: false, nodeBalancerId: 1, @@ -32,6 +46,9 @@ describe('NodeBalancerFirewalls', () => { data: { data: [firewall] }, isLoading: false, }); + queryMocks.useParams.mockReturnValue({ + id: '1', + }); }); afterEach(() => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.tsx index 05a8cb0e4e6..b49871ade3b 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerFirewalls.tsx @@ -1,5 +1,10 @@ -import { useNodeBalancersFirewallsQuery } from '@linode/queries'; +import { + useAllFirewallDevicesQuery, + useFirewallQuery, + useNodeBalancersFirewallsQuery, +} from '@linode/queries'; import { Box, Button, Drawer, Stack, Typography } from '@linode/ui'; +import { useMatch, useNavigate } from '@tanstack/react-router'; import React from 'react'; import { Link } from 'src/components/Link'; @@ -14,10 +19,11 @@ import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; import { RemoveDeviceDialog } from 'src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog'; import { AddFirewallForm } from 'src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm'; +import { useDialogData } from 'src/hooks/useDialogData'; import { NodeBalancerFirewallsRow } from './NodeBalancerFirewallsRow'; -import type { Firewall, FirewallDevice } from '@linode/api-v4'; +import type { Firewall } from '@linode/api-v4'; interface Props { nodeBalancerId: number; @@ -25,7 +31,10 @@ interface Props { export const NodeBalancerFirewalls = (props: Props) => { const { nodeBalancerId } = props; - + const navigate = useNavigate(); + const match = useMatch({ + strict: false, + }); const { data: attachedFirewallData, error, @@ -34,27 +43,35 @@ export const NodeBalancerFirewalls = (props: Props) => { const attachedFirewalls = attachedFirewallData?.data; - const [selectedFirewall, setSelectedFirewall] = React.useState(); - - const [ - deviceToBeRemoved, - setDeviceToBeRemoved, - ] = React.useState(); - - const [ - isRemoveDeviceDialogOpen, - setIsRemoveDeviceDialogOpen, - ] = React.useState(false); + const isUnassignFirewallRoute = + match.routeId === + '/nodebalancers/$id/settings/unassign-firewall/$firewallId'; - const [ - isAddFirewallDrawerOpen, - setIsAddFirewalDrawerOpen, - ] = React.useState(false); - - const handleClickUnassign = (device: FirewallDevice, firewall: Firewall) => { - setDeviceToBeRemoved(device); - setSelectedFirewall(firewall); - setIsRemoveDeviceDialogOpen(true); + const { + data: selectedFirewall, + isFetching: isFetchingSelectedFirewall, + } = useDialogData({ + enabled: isUnassignFirewallRoute, + paramKey: 'firewallId', + queryHook: useFirewallQuery, + redirectToOnNotFound: '/nodebalancers/$id/settings', + }); + + const { data: devices, isFetching: isFetchingDevices } = useDialogData({ + enabled: isUnassignFirewallRoute, + paramKey: 'firewallId', + queryHook: useAllFirewallDevicesQuery, + redirectToOnNotFound: '/nodebalancers/$id/settings', + }); + + const handleClickUnassign = (firewall: Firewall) => { + navigate({ + params: { + firewallId: String(firewall.id), + id: String(nodeBalancerId), + }, + to: '/nodebalancers/$id/settings/unassign-firewall/$firewallId', + }); }; const renderTableContent = () => { @@ -72,10 +89,11 @@ export const NodeBalancerFirewalls = (props: Props) => { return attachedFirewalls.map((attachedFirewall) => ( handleClickUnassign(attachedFirewall)} /> )); }; @@ -94,9 +112,14 @@ export const NodeBalancerFirewalls = (props: Props) => { to your NodeBalancer. Only inbound rules are applied to NodeBalancers. - + { setIsDeleteDialogOpen(false)} - open={isDeleteDialogOpen} + isFetching={isFetchingNodeBalancer} + open={match.routeId === '/nodebalancers/$id/settings/delete'} + selectedNodeBalancer={selectedNodeBalancer} /> ); }; - -export const nodeBalancerSettingsLazyRoute = createLazyRoute( - '/nodebalancers/$nodeBalancerId/settings' -)({ - component: NodeBalancerSettings, -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx index a15d5ef286e..e5efa06488c 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary.tsx @@ -1,18 +1,18 @@ +import { useNodeBalancerQuery } from '@linode/queries'; import Grid from '@mui/material/Grid2'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { useNodeBalancerQuery } from '@linode/queries'; import { SummaryPanel } from './SummaryPanel'; import { TablesPanel } from './TablesPanel'; export const NodeBalancerSummary = () => { - const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); - const id = Number(nodeBalancerId); - const { data: nodebalancer } = useNodeBalancerQuery(id); + const { id } = useParams({ + from: '/nodebalancers/$id/summary', + }); + const { data: nodebalancer } = useNodeBalancerQuery(Number(id), Boolean(id)); return (
@@ -28,9 +28,3 @@ export const NodeBalancerSummary = () => {
); }; - -export const nodeBalancerSummaryLazyRoute = createLazyRoute( - '/nodebalancers/$nodeBalancerId' -)({ - component: NodeBalancerSummary, -}); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx index 81ce99abf20..c4f8ba712b5 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx @@ -11,13 +11,21 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { SummaryPanel } from './SummaryPanel'; -// Set up various mocks for tests const queryMocks = vi.hoisted(() => ({ useAllNodeBalancerConfigsQuery: vi.fn().mockReturnValue({ data: undefined }), useNodeBalancerQuery: vi.fn().mockReturnValue({ data: undefined }), useNodeBalancersFirewallsQuery: vi.fn().mockReturnValue({ data: undefined }), + useParams: vi.fn().mockReturnValue({}), })); +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useParams: queryMocks.useParams, + }; +}); + vi.mock('@linode/queries', async () => { const actual = await vi.importActual('@linode/queries'); return { @@ -41,6 +49,7 @@ describe('SummaryPanel', () => { queryMocks.useNodeBalancersFirewallsQuery.mockReturnValue({ data: { data: [firewallFactory.build({ label: 'mock-firewall-1' })] }, }); + queryMocks.useParams.mockReturnValue({ id: 1 }); }); afterEach(() => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index 2a3dad1b501..f6f8b158285 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -8,8 +8,8 @@ import { import { Paper, Typography } from '@linode/ui'; import { convertMegabytesTo } from '@linode/utilities'; import { styled } from '@mui/material/styles'; +import { useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import { Link } from 'src/components/Link'; import { TagCell } from 'src/components/TagCell/TagCell'; @@ -18,16 +18,21 @@ import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useKubernetesClusterQuery } from 'src/queries/kubernetes'; export const SummaryPanel = () => { - const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); - const id = Number(nodeBalancerId); - const { data: nodebalancer } = useNodeBalancerQuery(id); - const { data: configs } = useAllNodeBalancerConfigsQuery(id); + const { id } = useParams({ + from: '/nodebalancers/$id/summary', + }); + const { data: nodebalancer } = useNodeBalancerQuery(Number(id), Boolean(id)); + const { data: configs } = useAllNodeBalancerConfigsQuery(Number(id)); const { data: regions } = useRegionsQuery(); - const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery(id); + const { data: attachedFirewallData } = useNodeBalancersFirewallsQuery( + Number(id) + ); const linkText = attachedFirewallData?.data[0]?.label; const linkID = attachedFirewallData?.data[0]?.id; const region = regions?.find((r) => r.id === nodebalancer?.region); - const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation(id); + const { mutateAsync: updateNodeBalancer } = useNodebalancerUpdateMutation( + Number(id) + ); const displayFirewallLink = !!attachedFirewallData?.data?.length; const isNodeBalancerReadOnly = useIsResourceRestricted({ diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx index 67878e537d5..5b266838373 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import * as React from 'react'; import { nodeBalancerFactory } from 'src/factories'; @@ -6,12 +5,20 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { TablesPanel } from './TablesPanel'; -// Set up various mocks for tests const queryMocks = vi.hoisted(() => ({ useNodeBalancerQuery: vi.fn().mockReturnValue({ data: undefined }), useNodeBalancerStatsQuery: vi.fn().mockReturnValue({ data: undefined }), + useParams: vi.fn().mockReturnValue({}), })); +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useParams: queryMocks.useParams, + }; +}); + vi.mock('@linode/queries', async () => { const actual = await vi.importActual('@linode/queries'); return { @@ -35,6 +42,7 @@ describe('TablesPanel', () => { queryMocks.useNodeBalancerQuery.mockReturnValue({ data: nodeBalancerFactory.build(), }); + queryMocks.useParams.mockReturnValue({ id: 1 }); }); afterEach(() => { diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx index ddac1fff05c..38efeaafe54 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.tsx @@ -6,8 +6,8 @@ import { import { Box, CircleProgress, ErrorState, Paper, Typography } from '@linode/ui'; import { formatNumber, getMetrics } from '@linode/utilities'; import { styled, useTheme } from '@mui/material/styles'; +import { useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useParams } from 'react-router-dom'; import PendingIcon from 'src/assets/icons/pending.svg'; import { AreaChart } from 'src/components/AreaChart/AreaChart'; @@ -30,9 +30,10 @@ export const TablesPanel = () => { const theme = useTheme(); const { data: profile } = useProfile(); const timezone = getUserTimezone(profile?.timezone); - const { nodeBalancerId } = useParams<{ nodeBalancerId: string }>(); - const id = Number(nodeBalancerId); - const { data: nodebalancer } = useNodeBalancerQuery(id); + const { id } = useParams({ + from: '/nodebalancers/$id/summary', + }); + const { data: nodebalancer } = useNodeBalancerQuery(Number(id), Boolean(id)); const { data: stats, error, isLoading } = useNodeBalancerStatsQuery( nodebalancer?.id ?? -1 diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx deleted file mode 100644 index 66084266b3f..00000000000 --- a/packages/manager/src/features/NodeBalancers/NodeBalancers.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { CircleProgress } from '@linode/ui'; -import * as React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; - -const NodeBalancerDetail = React.lazy(() => - import('./NodeBalancerDetail/NodeBalancerDetail').then((module) => ({ - default: module.NodeBalancerDetail, - })) -); -const NodeBalancersLanding = React.lazy( - () => import('./NodeBalancersLanding/NodeBalancersLanding') -); -const NodeBalancerCreate = React.lazy(() => import('./NodeBalancerCreate')); - -const NodeBalancers = () => { - return ( - }> - - - - - - - - ); -}; - -export default NodeBalancers; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx index e950688d3d4..0de7d906e43 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.test.tsx @@ -5,6 +5,21 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; +const navigate = vi.fn(); +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(() => navigate), + useRouter: vi.fn(() => vi.fn()), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + useRouter: queryMocks.useRouter, + }; +}); + const props = { label: 'nodebalancer-1', nodeBalancerId: 1, @@ -33,6 +48,6 @@ describe('NodeBalancerActionMenu', () => { const deleteButton = getByText('Delete'); await userEvent.click(deleteButton); - expect(props.toggleDialog).toHaveBeenCalled(); + expect(navigate).toHaveBeenCalled(); }); }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx index f24b602548d..db2c41178bd 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerActionMenu.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { Hidden } from 'src/components/Hidden'; @@ -13,17 +13,15 @@ import type { Theme } from '@mui/material/styles'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface Props { - label: string; nodeBalancerId: number; - toggleDialog: (nodeBalancerId: number, label: string) => void; } export const NodeBalancerActionMenu = (props: Props) => { + const navigate = useNavigate(); const theme = useTheme(); const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); - const history = useHistory(); - const { label, nodeBalancerId, toggleDialog } = props; + const { nodeBalancerId } = props; const isNodeBalancerReadOnly = useIsResourceRestricted({ grantLevel: 'read_only', @@ -34,20 +32,35 @@ export const NodeBalancerActionMenu = (props: Props) => { const actions: Action[] = [ { onClick: () => { - history.push(`/nodebalancers/${nodeBalancerId}/configurations`); + navigate({ + params: { + id: String(nodeBalancerId), + }, + to: `/nodebalancers/$id/configurations`, + }); }, title: 'Configurations', }, { onClick: () => { - history.push(`/nodebalancers/${nodeBalancerId}/settings`); + navigate({ + params: { + id: String(nodeBalancerId), + }, + to: `/nodebalancers/$id/settings`, + }); }, title: 'Settings', }, { disabled: isNodeBalancerReadOnly, onClick: () => { - toggleDialog(nodeBalancerId, label); + navigate({ + params: { + id: String(nodeBalancerId), + }, + to: `/nodebalancers/$id/delete`, + }); }, title: 'Delete', tooltip: isNodeBalancerReadOnly diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx index 21b88f7e9a6..0eb8e8793af 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx @@ -8,6 +8,19 @@ import { renderWithTheme, resizeScreenSize } from 'src/utilities/testHelpers'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; +const navigate = vi.fn(); +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(() => navigate), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + }; +}); + vi.mock('src/hooks/useIsResourceRestricted'); const props = { @@ -47,7 +60,7 @@ describe('NodeBalancerTableRow', () => { const deleteButton = getByText('Delete'); await userEvent.click(deleteButton); - expect(props.onDelete).toHaveBeenCalled(); + expect(navigate).toHaveBeenCalled(); }); it('does not delete the NodeBalancer if the delete button is disabled', async () => { @@ -55,7 +68,9 @@ describe('NodeBalancerTableRow', () => { const { getByText } = renderWithTheme(); const deleteButton = getByText('Delete'); + expect(deleteButton).toBeDisabled(); // Add this assertion + await userEvent.click(deleteButton); - expect(props.onDelete).not.toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); }); }); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx index 35a8a16cdf0..65bd78280e2 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.tsx @@ -14,12 +14,8 @@ import { NodeBalancerActionMenu } from './NodeBalancerActionMenu'; import type { NodeBalancer } from '@linode/api-v4/lib/nodebalancers'; -interface Props extends NodeBalancer { - onDelete: () => void; -} - -export const NodeBalancerTableRow = (props: Props) => { - const { id, ipv4, label, onDelete, region, transfer } = props; +export const NodeBalancerTableRow = (props: NodeBalancer) => { + const { id, ipv4, label, region, transfer } = props; const { data: configs } = useAllNodeBalancerConfigsQuery(id); @@ -73,11 +69,7 @@ export const NodeBalancerTableRow = (props: Props) => { - + ); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx index e320fd915ab..e3771063f44 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx @@ -8,7 +8,26 @@ import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancersLanding } from './NodeBalancersLanding'; -beforeAll(() => mockMatchMedia()); +const queryMocks = vi.hoisted(() => ({ + useMatch: vi.fn().mockReturnValue({}), + useNavigate: vi.fn(() => vi.fn()), + useParams: vi.fn().mockReturnValue({}), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useMatch: queryMocks.useMatch, + useNavigate: queryMocks.useNavigate, + useParams: queryMocks.useParams, + }; +}); + +beforeAll(() => { + mockMatchMedia(); + queryMocks.useParams.mockReturnValue({ id: 1 }); +}); const loadingTestId = 'circle-progress'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx index 840495a67a1..80f2b6c7bfb 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.tsx @@ -1,7 +1,7 @@ +import { useNodeBalancerQuery, useNodeBalancersQuery } from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useMatch, useNavigate, useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Hidden } from 'src/components/Hidden'; @@ -15,26 +15,21 @@ import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell/TableSortCell'; import { TransferDisplay } from 'src/components/TransferDisplay/TransferDisplay'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { useDialogData } from 'src/hooks/useDialogData'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useNodeBalancersQuery } from '@linode/queries'; import { NodeBalancerDeleteDialog } from '../NodeBalancerDeleteDialog'; import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; import { NodeBalancerTableRow } from './NodeBalancerTableRow'; + const preferenceKey = 'nodebalancers'; export const NodeBalancersLanding = () => { - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState( - false - ); - const [ - selectedNodeBalancerId, - setSelectedNodeBalancerId, - ] = React.useState(-1); - - const history = useHistory(); + const navigate = useNavigate(); + const match = useMatch({ strict: false }); + const params = useParams({ strict: false }); const pagination = usePagination(1, preferenceKey); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_nodebalancers', @@ -61,14 +56,15 @@ export const NodeBalancersLanding = () => { filter ); - const selectedNodeBalancer = data?.data.find( - (nodebalancer) => nodebalancer.id === selectedNodeBalancerId - ); - - const onDelete = (nodeBalancerId: number) => { - setSelectedNodeBalancerId(nodeBalancerId); - setIsDeleteDialogOpen(true); - }; + const { + data: selectedNodeBalancer, + isFetching: isFetchingNodeBalancer, + } = useDialogData({ + enabled: !!params.id, + paramKey: 'id', + queryHook: useNodeBalancerQuery, + redirectToOnNotFound: '/nodebalancers', + }); if (error) { return ( @@ -90,6 +86,9 @@ export const NodeBalancersLanding = () => { <> { disabledCreateButton={isRestricted} docsLink="https://techdocs.akamai.com/cloud-computing/docs/getting-started-with-nodebalancers" entity="NodeBalancer" - onButtonClick={() => history.push('/nodebalancers/create')} + onButtonClick={() => navigate({ to: '/nodebalancers/create' })} title="NodeBalancers" /> @@ -137,11 +136,7 @@ export const NodeBalancersLanding = () => { {data?.data.map((nodebalancer) => ( - onDelete(nodebalancer.id)} - {...nodebalancer} - /> + ))}
@@ -155,17 +150,12 @@ export const NodeBalancersLanding = () => { /> setIsDeleteDialogOpen(false)} - open={isDeleteDialogOpen} + isFetching={isFetchingNodeBalancer} + open={match.routeId === '/nodebalancers/$id/delete'} + selectedNodeBalancer={selectedNodeBalancer} /> ); }; -export const nodeBalancersLandingLazyRoute = createLazyRoute('/nodebalancers')({ - component: NodeBalancersLanding, -}); - export default NodeBalancersLanding; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx index b3277aceb8b..b04b3f64eaf 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.test.tsx @@ -6,6 +6,18 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancerLandingEmptyState } from './NodeBalancersLandingEmptyState'; +const queryMocks = vi.hoisted(() => ({ + useNavigate: vi.fn(() => vi.fn()), +})); + +vi.mock('@tanstack/react-router', async () => { + const actual = await vi.importActual('@tanstack/react-router'); + return { + ...actual, + useNavigate: queryMocks.useNavigate, + }; +}); + vi.mock('src/hooks/useRestrictedGlobalGrantCheck'); // Note: An integration test confirming the helper text and enabled Create NodeBalancer button already exists, so we're just checking for a disabled create button here diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx index 805981e2b7e..1f22b671b48 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLandingEmptyState.tsx @@ -1,5 +1,5 @@ +import { useNavigate } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import NetworkIcon from 'src/assets/icons/entityIcons/networking.svg'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; @@ -16,8 +16,7 @@ import { } from './NodeBalancersLandingEmptyStateData'; export const NodeBalancerLandingEmptyState = () => { - const { push } = useHistory(); - + const navigate = useNavigate(); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_nodebalancers', }); @@ -36,7 +35,9 @@ export const NodeBalancerLandingEmptyState = () => { category: linkAnalyticsEvent.category, label: 'Create NodeBalancer', }); - push('/nodebalancers/create'); + navigate({ + to: '/nodebalancers/create', + }); }, tooltipText: getRestrictedResourceText({ action: 'create', diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index 186b87554f1..5fd7ada5b49 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -92,6 +92,7 @@ export const migrationRouteTree = migrationRootRoute.addChildren([ firewallsRouteTree, imagesRouteTree, longviewRouteTree, + nodeBalancersRouteTree, placementGroupsRouteTree, stackScriptsRouteTree, volumesRouteTree, diff --git a/packages/manager/src/routes/nodeBalancers/index.ts b/packages/manager/src/routes/nodeBalancers/index.ts index 7dae79cedcd..855fcf1481a 100644 --- a/packages/manager/src/routes/nodeBalancers/index.ts +++ b/packages/manager/src/routes/nodeBalancers/index.ts @@ -1,4 +1,4 @@ -import { createRoute, lazyRouteComponent } from '@tanstack/react-router'; +import { createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { NodeBalancersRoute } from './NodeBalancersRoute'; @@ -13,60 +13,89 @@ const nodeBalancersIndexRoute = createRoute({ getParentRoute: () => nodeBalancersRoute, path: '/', }).lazy(() => - import( - 'src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding' - ).then((m) => m.nodeBalancersLandingLazyRoute) + import('./nodeBalancersLazyRoutes').then( + (m) => m.nodeBalancersLandingLazyRoute + ) ); const nodeBalancersCreateRoute = createRoute({ getParentRoute: () => nodeBalancersRoute, path: 'create', }).lazy(() => - import('src/features/NodeBalancers/NodeBalancerCreate').then( - (m) => m.nodeBalancerCreateLazyRoute - ) + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerCreateLazyRoute) ); const nodeBalancerDetailRoute = createRoute({ + beforeLoad: async ({ params }) => { + throw redirect({ + params: { + id: params.id, + }, + to: '/nodebalancers/$id/summary', + }); + }, getParentRoute: () => nodeBalancersRoute, - parseParams: (params) => ({ - nodeBalancerId: Number(params.nodeBalancerId), - }), - path: '$nodeBalancerId', + path: '$id', }).lazy(() => - import( - 'src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail' - ).then((m) => m.nodeBalancerDetailLazyRoute) + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) ); const nodeBalancerDetailSummaryRoute = createRoute({ - getParentRoute: () => nodeBalancerDetailRoute, - path: 'summary', + getParentRoute: () => nodeBalancersRoute, + path: '$id/summary', }).lazy(() => - import( - 'src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/NodeBalancerSummary' - ).then((m) => m.nodeBalancerSummaryLazyRoute) + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) ); -// TODO TanStack Router - figure proper way of lazy loading class components const nodeBalancerDetailConfigurationsRoute = createRoute({ - component: lazyRouteComponent( - () => - import( - 'src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations' - ) - ), - getParentRoute: () => nodeBalancerDetailRoute, - path: 'configurations', -}); + getParentRoute: () => nodeBalancersRoute, + path: '$id/configurations', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); + +const nodeBalancerDetailConfigurationRoute = createRoute({ + getParentRoute: () => nodeBalancersRoute, + path: '$id/configurations/$configId', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); const nodeBalancerDetailSettingsRoute = createRoute({ - getParentRoute: () => nodeBalancerDetailRoute, - path: 'settings', + getParentRoute: () => nodeBalancersRoute, + path: '$id/settings', }).lazy(() => - import( - 'src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings' - ).then((m) => m.nodeBalancerSettingsLazyRoute) + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); + +const nodeBalancerDetailSettingsDeleteRoute = createRoute({ + getParentRoute: () => nodeBalancersRoute, + path: '$id/settings/delete', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); + +const nodeBalancerDetailSettingsAddFirewallRoute = createRoute({ + getParentRoute: () => nodeBalancersRoute, + path: '$id/settings/add-firewall', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); + +const nodeBalancerDetailSettingsUnassignFirewallRoute = createRoute({ + getParentRoute: () => nodeBalancersRoute, + path: '$id/settings/unassign-firewall/$firewallId', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then((m) => m.nodeBalancerDetailLazyRoute) +); + +const nodeBalancerDeleteRoute = createRoute({ + getParentRoute: () => nodeBalancersRoute, + path: '$id/delete', +}).lazy(() => + import('./nodeBalancersLazyRoutes').then( + (m) => m.nodeBalancersLandingLazyRoute + ) ); export const nodeBalancersRouteTree = nodeBalancersRoute.addChildren([ @@ -74,7 +103,14 @@ export const nodeBalancersRouteTree = nodeBalancersRoute.addChildren([ nodeBalancersCreateRoute, nodeBalancerDetailRoute.addChildren([ nodeBalancerDetailSummaryRoute, - nodeBalancerDetailConfigurationsRoute, - nodeBalancerDetailSettingsRoute, + nodeBalancerDetailConfigurationsRoute.addChildren([ + nodeBalancerDetailConfigurationRoute, + ]), + nodeBalancerDetailSettingsRoute.addChildren([ + nodeBalancerDetailSettingsDeleteRoute, + nodeBalancerDetailSettingsAddFirewallRoute, + nodeBalancerDetailSettingsUnassignFirewallRoute, + ]), ]), + nodeBalancerDeleteRoute, ]); diff --git a/packages/manager/src/routes/nodeBalancers/nodeBalancersLazyRoutes.ts b/packages/manager/src/routes/nodeBalancers/nodeBalancersLazyRoutes.ts new file mode 100644 index 00000000000..6951896a185 --- /dev/null +++ b/packages/manager/src/routes/nodeBalancers/nodeBalancersLazyRoutes.ts @@ -0,0 +1,21 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import NodeBalancerCreate from 'src/features/NodeBalancers/NodeBalancerCreate'; +import { NodeBalancerDetail } from 'src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail'; +import { NodeBalancersLanding } from 'src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding'; + +export const nodeBalancersLandingLazyRoute = createLazyRoute('/nodebalancers')({ + component: NodeBalancersLanding, +}); + +export const nodeBalancerDetailLazyRoute = createLazyRoute( + '/nodebalancers/$id' +)({ + component: NodeBalancerDetail, +}); + +export const nodeBalancerCreateLazyRoute = createLazyRoute( + '/nodebalancers/create' +)({ + component: NodeBalancerCreate, +}); diff --git a/packages/queries/src/firewalls/firewalls.ts b/packages/queries/src/firewalls/firewalls.ts index 54ac1a0826c..1044375c9bd 100644 --- a/packages/queries/src/firewalls/firewalls.ts +++ b/packages/queries/src/firewalls/firewalls.ts @@ -101,10 +101,14 @@ export const firewallQueries = createQueryKeys('firewalls', { }, }); -export const useAllFirewallDevicesQuery = (id: number) => - useQuery( - firewallQueries.firewall(id)._ctx.devices - ); +export const useAllFirewallDevicesQuery = ( + id: number, + enabled: boolean = true +) => + useQuery({ + ...firewallQueries.firewall(id)._ctx.devices, + enabled, + }); export const useFirewallsInfiniteQuery = (filter: Filter, enabled: boolean) => { return useInfiniteQuery, APIError[]>({ @@ -279,8 +283,11 @@ export const useFirewallTemplatesQuery = () => { }); }; -export const useFirewallQuery = (id: number) => - useQuery(firewallQueries.firewall(id)); +export const useFirewallQuery = (id: number, enabled: boolean = true) => + useQuery({ + ...firewallQueries.firewall(id), + enabled, + }); export const useAllFirewallsQuery = (enabled: boolean = true) => { return useQuery({ From 6d7a586bae6a2e3cdfa83fa938a8336f68aef0a9 Mon Sep 17 00:00:00 2001 From: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com> Date: Thu, 20 Mar 2025 06:19:05 +0530 Subject: [PATCH 02/84] change: [DI-24111] - Updated hovering color for dashboard icons (#11883) * change: [DI-24111] - Added color on hover of icons * change: [DI-24111] - Updated label * change: [DI-24111] - Moved dupilcate code to function * Added changeset * change: [DI-24111] - Removed styled components --- .../pr-11883-changed-1742377578472.md | 5 ++ packages/manager/src/assets/icons/refresh.svg | 4 +- packages/manager/src/assets/icons/zoomin.svg | 34 ++++++------ packages/manager/src/assets/icons/zoomout.svg | 18 +++---- .../CloudPulse/Overview/GlobalFilters.tsx | 15 +----- .../CloudPulse/Widget/components/Zoomer.tsx | 54 +++++++++---------- 6 files changed, 59 insertions(+), 71 deletions(-) create mode 100644 packages/manager/.changeset/pr-11883-changed-1742377578472.md diff --git a/packages/manager/.changeset/pr-11883-changed-1742377578472.md b/packages/manager/.changeset/pr-11883-changed-1742377578472.md new file mode 100644 index 00000000000..a5a1d846ea1 --- /dev/null +++ b/packages/manager/.changeset/pr-11883-changed-1742377578472.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) diff --git a/packages/manager/src/assets/icons/refresh.svg b/packages/manager/src/assets/icons/refresh.svg index 4864b6402c3..118f068f75c 100644 --- a/packages/manager/src/assets/icons/refresh.svg +++ b/packages/manager/src/assets/icons/refresh.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/manager/src/assets/icons/zoomin.svg b/packages/manager/src/assets/icons/zoomin.svg index 1a9d0e6873d..389ea95c5da 100644 --- a/packages/manager/src/assets/icons/zoomin.svg +++ b/packages/manager/src/assets/icons/zoomin.svg @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/packages/manager/src/assets/icons/zoomout.svg b/packages/manager/src/assets/icons/zoomout.svg index 173bcb9b058..30f229b8083 100644 --- a/packages/manager/src/assets/icons/zoomout.svg +++ b/packages/manager/src/assets/icons/zoomout.svg @@ -1,10 +1,10 @@ - - - - - - - - - + + + + + + + + + diff --git a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx index e245f598678..85f8ea6c2ab 100644 --- a/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx +++ b/packages/manager/src/features/CloudPulse/Overview/GlobalFilters.tsx @@ -1,7 +1,6 @@ import { Box, Divider } from '@linode/ui'; import { IconButton } from '@mui/material'; import { Grid } from '@mui/material'; -import { styled } from '@mui/material/styles'; import * as React from 'react'; import Reload from 'src/assets/icons/refresh.svg'; @@ -125,12 +124,13 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { marginTop: { md: theme.spacing(3.5) }, })} aria-label="Refresh Dashboard Metrics" + color="inherit" data-testid="global-refresh" disabled={!selectedDashboard} onClick={handleGlobalRefresh} size="small" > - + @@ -159,14 +159,3 @@ export const GlobalFilters = React.memo((props: GlobalFilterProperties) => { ); }); - -const StyledReload = styled(Reload, { label: 'StyledReload' })(({ theme }) => ({ - '&:active': { - color: `${theme.palette.success}`, - }, - '&:hover': { - cursor: 'pointer', - }, - height: '24px', - width: '24px', -})); diff --git a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx index a04204db3eb..c19af395c50 100644 --- a/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx +++ b/packages/manager/src/features/CloudPulse/Widget/components/Zoomer.tsx @@ -13,45 +13,39 @@ export interface ZoomIconProperties { } export const ZoomIcon = React.memo((props: ZoomIconProperties) => { - const handleClick = (needZoomIn: boolean) => { - props.handleZoomToggle(needZoomIn); - }; - - const ToggleZoomer = () => { - if (props.zoomIn) { - return ( - - handleClick(false)} - > - - - - ); - } + const { handleZoomToggle } = props; + if (props.zoomIn) { return ( - + handleClick(true)} + aria-label="Zoom Out" + color="inherit" + data-testid="zoom-out" + onClick={() => handleZoomToggle(false)} > - + ); - }; + } - return ; + return ( + + handleZoomToggle(true)} + > + + + + ); }); From 71a3bb41f04b7fa90140a743d3d19a86fba0162a Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 20 Mar 2025 09:27:09 -0400 Subject: [PATCH 03/84] upcoming: [M3-9105] - Add Account Default Firewalls setting (#11828) * save progress save progress * default firewalls paper * i think this fixes the weird initial loading state of all undefined??? * update types * changesets * update schema / cleanup * add toasts for success * design feedback pt1: update placeholder text * UX feedback (for now) --- packages/api-v4/src/firewalls/types.ts | 8 +- ...r-11828-upcoming-features-1741811633687.md | 5 + .../src/features/Account/AccountLanding.tsx | 2 +- .../src/features/Account/AccountLogins.tsx | 4 +- .../Account/AccountLoginsTableRow.tsx | 2 +- .../features/Account/CloseAccountDialog.tsx | 2 +- .../features/Account/CloseAccountSetting.tsx | 3 +- .../Account/DefaultFirewalls.test.tsx | 45 ++++ .../src/features/Account/DefaultFirewalls.tsx | 210 ++++++++++++++++++ .../src/features/Account/GlobalSettings.tsx | 12 +- .../features/Account/NetworkInterfaceType.tsx | 7 +- .../Account/ObjectStorageSettings.tsx | 2 +- ...r-11828-upcoming-features-1741811672300.md | 5 + packages/queries/src/firewalls/firewalls.ts | 25 +++ packages/validation/src/firewalls.schema.ts | 8 +- 15 files changed, 318 insertions(+), 22 deletions(-) create mode 100644 packages/manager/.changeset/pr-11828-upcoming-features-1741811633687.md create mode 100644 packages/manager/src/features/Account/DefaultFirewalls.test.tsx create mode 100644 packages/manager/src/features/Account/DefaultFirewalls.tsx create mode 100644 packages/queries/.changeset/pr-11828-upcoming-features-1741811672300.md diff --git a/packages/api-v4/src/firewalls/types.ts b/packages/api-v4/src/firewalls/types.ts index 4f5eca2dbce..2f21b3a5022 100644 --- a/packages/api-v4/src/firewalls/types.ts +++ b/packages/api-v4/src/firewalls/types.ts @@ -89,10 +89,10 @@ export interface FirewallDevicePayload { } export interface DefaultFirewallIDs { - public_interface: number; - vpc_interface: number; - linode: number; - nodebalancer: number; + public_interface: number | null; + vpc_interface: number | null; + linode: number | null; + nodebalancer: number | null; } export interface FirewallSettings { diff --git a/packages/manager/.changeset/pr-11828-upcoming-features-1741811633687.md b/packages/manager/.changeset/pr-11828-upcoming-features-1741811633687.md new file mode 100644 index 00000000000..092b096a33c --- /dev/null +++ b/packages/manager/.changeset/pr-11828-upcoming-features-1741811633687.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Default Firewalls paper to Account Settings ([#11828](https://github.com/linode/manager/pull/11828)) diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index c95d4881b7e..b011cd3dcae 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -1,3 +1,4 @@ +import { useAccount, useProfile } from '@linode/queries'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; @@ -14,7 +15,6 @@ import { useIsParentTokenExpired } from 'src/features/Account/SwitchAccounts/use import { getRestrictedResourceText } from 'src/features/Account/utils'; import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useAccount, useProfile } from '@linode/queries'; import { sendSwitchAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; import AccountLogins from './AccountLogins'; diff --git a/packages/manager/src/features/Account/AccountLogins.tsx b/packages/manager/src/features/Account/AccountLogins.tsx index 3c3d4739c9c..2c06eb241bb 100644 --- a/packages/manager/src/features/Account/AccountLogins.tsx +++ b/packages/manager/src/features/Account/AccountLogins.tsx @@ -1,7 +1,9 @@ +import { useAccountLoginsQuery, useProfile } from '@linode/queries'; import { Notice, Typography } from '@linode/ui'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Hidden } from 'src/components/Hidden'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; @@ -15,14 +17,12 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' import { TableSortCell } from 'src/components/TableSortCell'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; -import { useAccountLoginsQuery, useProfile } from '@linode/queries'; import AccountLoginsTableRow from './AccountLoginsTableRow'; import { getRestrictedResourceText } from './utils'; import type { AccountLogin } from '@linode/api-v4/lib/account/types'; import type { Theme } from '@mui/material/styles'; -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; const preferenceKey = 'account-logins'; diff --git a/packages/manager/src/features/Account/AccountLoginsTableRow.tsx b/packages/manager/src/features/Account/AccountLoginsTableRow.tsx index 837725aa1ef..cd76d5b8411 100644 --- a/packages/manager/src/features/Account/AccountLoginsTableRow.tsx +++ b/packages/manager/src/features/Account/AccountLoginsTableRow.tsx @@ -1,3 +1,4 @@ +import { useProfile } from '@linode/queries'; import { capitalize } from '@linode/utilities'; import * as React from 'react'; @@ -7,7 +8,6 @@ import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { useProfile } from '@linode/queries'; import { formatDate } from 'src/utilities/formatDate'; import type { diff --git a/packages/manager/src/features/Account/CloseAccountDialog.tsx b/packages/manager/src/features/Account/CloseAccountDialog.tsx index 32ca167673f..3428969433e 100644 --- a/packages/manager/src/features/Account/CloseAccountDialog.tsx +++ b/packages/manager/src/features/Account/CloseAccountDialog.tsx @@ -1,4 +1,5 @@ import { cancelAccount } from '@linode/api-v4/lib/account'; +import { useProfile } from '@linode/queries'; import { Notice, TextField, Typography } from '@linode/ui'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -9,7 +10,6 @@ import { CANCELLATION_DATA_LOSS_WARNING, CANCELLATION_DIALOG_TITLE, } from 'src/features/Account/constants'; -import { useProfile } from '@linode/queries'; import type { APIError } from '@linode/api-v4/lib/types'; diff --git a/packages/manager/src/features/Account/CloseAccountSetting.tsx b/packages/manager/src/features/Account/CloseAccountSetting.tsx index 2c5a03dc20d..cd7ba68b31a 100644 --- a/packages/manager/src/features/Account/CloseAccountSetting.tsx +++ b/packages/manager/src/features/Account/CloseAccountSetting.tsx @@ -1,9 +1,8 @@ +import { useProfile } from '@linode/queries'; import { Accordion, Button } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; -import { useProfile } from '@linode/queries'; - import CloseAccountDialog from './CloseAccountDialog'; import { CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, diff --git a/packages/manager/src/features/Account/DefaultFirewalls.test.tsx b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx new file mode 100644 index 00000000000..6043c5765c0 --- /dev/null +++ b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx @@ -0,0 +1,45 @@ +import { waitForElementToBeRemoved } from '@testing-library/react'; +import * as React from 'react'; + +import { firewallFactory, firewallSettingsFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { DefaultFirewalls } from './DefaultFirewalls'; + +const loadingTestId = 'circle-progress'; + +describe('NetworkInterfaces', () => { + it('renders the NetworkInterfaces accordion', async () => { + server.use( + http.get('*/v4beta/networking/firewalls/settings', () => + HttpResponse.json(firewallSettingsFactory.build()) + ), + http.get('*/v4beta/networking/firewalls', () => + HttpResponse.json(makeResourcePage(firewallFactory.buildList(1))) + ) + ); + const { getByTestId, getByText } = renderWithTheme(); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); + + await waitForElementToBeRemoved(getByTestId(loadingTestId)); + + expect(getByText('Default Firewalls')).toBeVisible(); + expect(getByText('Linodes')).toBeVisible(); + expect( + getByText('Configuration Profile Interfaces Firewall') + ).toBeVisible(); + expect( + getByText('Linode Interfaces - Public Interface Firewall') + ).toBeVisible(); + expect( + getByText('Linode Interfaces - VPC Interface Firewall') + ).toBeVisible(); + expect(getByText('NodeBalancers')).toBeVisible(); + expect(getByText('NodeBalancers Firewall')).toBeVisible(); + expect(getByText('Save')).toBeVisible(); + }); +}); diff --git a/packages/manager/src/features/Account/DefaultFirewalls.tsx b/packages/manager/src/features/Account/DefaultFirewalls.tsx new file mode 100644 index 00000000000..6d8fecc4716 --- /dev/null +++ b/packages/manager/src/features/Account/DefaultFirewalls.tsx @@ -0,0 +1,210 @@ +import { yupResolver } from '@hookform/resolvers/yup'; +import { + useAllFirewallsQuery, + useFirewallSettingsQuery, + useMutateFirewallSettings, +} from '@linode/queries'; +import { + Accordion, + Box, + Button, + CircleProgress, + Divider, + ErrorState, + Notice, + Select, + Stack, + Typography, +} from '@linode/ui'; +import { UpdateFirewallSettingsSchema } from '@linode/validation'; +import { useSnackbar } from 'notistack'; +import * as React from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import type { UpdateFirewallSettings } from '@linode/api-v4'; + +const DEFAULT_FIREWALL_PLACEHOLDER = 'None'; + +export const DefaultFirewalls = () => { + const { enqueueSnackbar } = useSnackbar(); + + const { + data: firewallSettings, + error: firewallSettingsError, + isLoading: isLoadingFirewallSettings, + } = useFirewallSettingsQuery(); + + const { mutateAsync: updateFirewallSettings } = useMutateFirewallSettings(); + + const { + data: firewalls, + error: firewallsError, + isLoading: isLoadingfirewalls, + } = useAllFirewallsQuery(); + const firewallOptions = + firewalls?.map((firewall) => { + return { label: firewall.label, value: firewall.id }; + }) ?? []; + + const values = { + default_firewall_ids: { ...firewallSettings?.default_firewall_ids }, + }; + + const { + control, + formState: { errors, isDirty, isSubmitting }, + handleSubmit, + setError, + } = useForm({ + defaultValues: { ...values }, + mode: 'onBlur', + resolver: yupResolver(UpdateFirewallSettingsSchema), + values, + }); + + const onSubmit = async (values: UpdateFirewallSettings) => { + try { + await updateFirewallSettings(values); + enqueueSnackbar('Default firewall settings updated.', { + variant: 'success', + }); + } catch (error) { + setError(error.field ?? 'root', { message: error[0].reason }); + } + }; + + if (isLoadingFirewallSettings || isLoadingfirewalls) { + return ( + + + + ); + } + + if (firewallSettingsError || firewallsError) { + return ( + + + + ); + } + + return ( + +
+ {errors.root?.message && ( + {errors.root.message} + )} + + + Set the default firewall that is assigned to each network interface + type when creating a Linode. The same firewall (new or existing) can + be assigned to each type of interface/connection. + + ({ marginTop: theme.spacing(2) })} + variant="h3" + > + Linodes + + ( + +
+
+ ); +}; diff --git a/packages/manager/src/features/Account/GlobalSettings.tsx b/packages/manager/src/features/Account/GlobalSettings.tsx index 6c222089266..4a84bf86a3d 100644 --- a/packages/manager/src/features/Account/GlobalSettings.tsx +++ b/packages/manager/src/features/Account/GlobalSettings.tsx @@ -1,19 +1,20 @@ +import { + useAccountSettings, + useAllLinodesQuery, + useMutateAccountSettings, +} from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { - useAccountSettings, - useMutateAccountSettings, - useAllLinodesQuery, -} from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { BackupDrawer } from '../Backups'; import AutoBackups from './AutoBackups'; import CloseAccountSetting from './CloseAccountSetting'; +import { DefaultFirewalls } from './DefaultFirewalls'; import { EnableManaged } from './EnableManaged'; import NetworkHelper from './NetworkHelper'; import { NetworkInterfaceType } from './NetworkInterfaceType'; @@ -84,6 +85,7 @@ const GlobalSettings = () => {
{isLinodeInterfacesEnabled && } + {isLinodeInterfacesEnabled && } { + const { enqueueSnackbar } = useSnackbar(); const { data: accountSettings } = useAccountSettings(); const { mutateAsync: updateAccountSettings } = useMutateAccountSettings(); @@ -67,6 +69,9 @@ export const NetworkInterfaceType = () => { const onSubmit = async (values: InterfaceSettingValues) => { try { await updateAccountSettings(values); + enqueueSnackbar('Network Interface type settings updated.', { + variant: 'success', + }); } catch (error) { setError('interfaces_for_new_linodes', { message: error[0].reason }); } diff --git a/packages/manager/src/features/Account/ObjectStorageSettings.tsx b/packages/manager/src/features/Account/ObjectStorageSettings.tsx index db2e0a7522a..ee26d882c75 100644 --- a/packages/manager/src/features/Account/ObjectStorageSettings.tsx +++ b/packages/manager/src/features/Account/ObjectStorageSettings.tsx @@ -1,3 +1,4 @@ +import { useAccountSettings, useProfile } from '@linode/queries'; import { Accordion, Box, @@ -12,7 +13,6 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { useAccountSettings, useProfile } from '@linode/queries'; import { useCancelObjectStorageMutation } from 'src/queries/object-storage/queries'; export const ObjectStorageSettings = () => { diff --git a/packages/queries/.changeset/pr-11828-upcoming-features-1741811672300.md b/packages/queries/.changeset/pr-11828-upcoming-features-1741811672300.md new file mode 100644 index 00000000000..3582b889ee1 --- /dev/null +++ b/packages/queries/.changeset/pr-11828-upcoming-features-1741811672300.md @@ -0,0 +1,5 @@ +--- +"@linode/queries": Upcoming Features +--- + +Add Firewall Settings query ([#11828](https://github.com/linode/manager/pull/11828)) diff --git a/packages/queries/src/firewalls/firewalls.ts b/packages/queries/src/firewalls/firewalls.ts index 1044375c9bd..b6f1b0dd58d 100644 --- a/packages/queries/src/firewalls/firewalls.ts +++ b/packages/queries/src/firewalls/firewalls.ts @@ -10,6 +10,8 @@ import { getTemplates, updateFirewall, updateFirewallRules, + getFirewallSettings, + updateFirewallSettings, } from '@linode/api-v4/lib/firewalls'; import { getAll } from '@linode/utilities'; import { createQueryKeys } from '@lukemorales/query-key-factory'; @@ -39,6 +41,8 @@ import type { Params, ResourcePage, UpdateFirewallRules, + FirewallSettings, + UpdateFirewallSettings } from '@linode/api-v4'; const getAllFirewallDevices = ( @@ -91,6 +95,10 @@ export const firewallQueries = createQueryKeys('firewalls', { }, queryKey: null, }, + settings: { + queryFn: getFirewallSettings, + queryKey: null, + }, template: (slug: FirewallTemplateSlug) => ({ queryFn: () => getTemplate(slug), queryKey: [slug], @@ -277,6 +285,10 @@ export const useFirewallsQuery = (params?: Params, filter?: Filter) => { }); }; +export const useFirewallSettingsQuery = () => { + return useQuery(firewallQueries.settings); +}; + export const useFirewallTemplatesQuery = () => { return useQuery({ ...firewallQueries.templates, @@ -296,6 +308,19 @@ export const useAllFirewallsQuery = (enabled: boolean = true) => { }); }; +export const useMutateFirewallSettings = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data) => updateFirewallSettings(data), + onSuccess(firewallSettings) { + queryClient.setQueryData( + firewallQueries.settings.queryKey, + firewallSettings + ); + }, + }); +}; + export const useMutateFirewall = (id: number) => { const queryClient = useQueryClient(); return useMutation>({ diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index dbda3097726..6c149f675bb 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -212,9 +212,9 @@ export const FirewallDeviceSchema = object({ export const UpdateFirewallSettingsSchema = object({ default_firewall_ids: object({ - interface_public: number(), - interface_vpc: number(), - linode: number(), - nodebalancer: number(), + interface_public: number().nullable(), + interface_vpc: number().nullable(), + linode: number().nullable(), + nodebalancer: number().nullable(), }), }); From 9a757f01eb398ed33b67a9bea3777b6fc8e46d55 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:24:46 -0400 Subject: [PATCH 04/84] chore: [M3-9600] - ESLint warning for theme.spacing (#11889) * chore: [M3-9600] ESLint warning for theme.spacing * Added changeset: Add eslint rule for deprecating mui theme.spacing --------- Co-authored-by: Jaalah Ramos --- .../pr-11889-tech-stories-1742437360421.md | 5 +++++ packages/manager/.eslintrc.cjs | 1 + packages/manager/package.json | 2 +- packages/queries/package.json | 2 +- packages/ui/package.json | 2 +- packages/utilities/package.json | 2 +- pnpm-lock.yaml | 22 +++++++++---------- 7 files changed, 21 insertions(+), 15 deletions(-) create mode 100644 packages/manager/.changeset/pr-11889-tech-stories-1742437360421.md diff --git a/packages/manager/.changeset/pr-11889-tech-stories-1742437360421.md b/packages/manager/.changeset/pr-11889-tech-stories-1742437360421.md new file mode 100644 index 00000000000..a5e7e379ba0 --- /dev/null +++ b/packages/manager/.changeset/pr-11889-tech-stories-1742437360421.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Add eslint rule for deprecating mui theme.spacing ([#11889](https://github.com/linode/manager/pull/11889)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 4f128444c4b..fd5c9ac21e0 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -242,6 +242,7 @@ module.exports = { rules: { '@linode/cloud-manager/deprecate-formik': 'warn', '@linode/cloud-manager/no-createLinode': 'off', + '@linode/cloud-manager/no-mui-theme-spacing': 'warn', '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/packages/manager/package.json b/packages/manager/package.json index 959ce9ff8c2..ce419ed307f 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -124,7 +124,7 @@ }, "devDependencies": { "@4tw/cypress-drag-drop": "^2.3.0", - "@linode/eslint-plugin-cloud-manager": "^0.0.7", + "@linode/eslint-plugin-cloud-manager": "^0.0.10", "@storybook/addon-a11y": "^8.6.7", "@storybook/addon-actions": "^8.6.7", "@storybook/addon-controls": "^8.6.7", diff --git a/packages/queries/package.json b/packages/queries/package.json index b1e9b0e75ab..1aad9cf4167 100644 --- a/packages/queries/package.json +++ b/packages/queries/package.json @@ -35,7 +35,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@linode/eslint-plugin-cloud-manager": "^0.0.7", + "@linode/eslint-plugin-cloud-manager": "^0.0.10", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", diff --git a/packages/ui/package.json b/packages/ui/package.json index a7ad31f5b46..ddfab3346e8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -45,7 +45,7 @@ ] }, "devDependencies": { - "@linode/eslint-plugin-cloud-manager": "^0.0.7", + "@linode/eslint-plugin-cloud-manager": "^0.0.10", "@storybook/addon-actions": "^8.6.7", "@storybook/preview-api": "^8.6.7", "@storybook/react": "^8.6.7", diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 57ff635151b..0fd577f49d7 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -37,7 +37,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@linode/eslint-plugin-cloud-manager": "^0.0.7", + "@linode/eslint-plugin-cloud-manager": "^0.0.10", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f9c01f025f..c1e7aa49326 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -309,8 +309,8 @@ importers: specifier: ^2.3.0 version: 2.3.0(cypress@14.0.1) '@linode/eslint-plugin-cloud-manager': - specifier: ^0.0.7 - version: 0.0.7(eslint@7.32.0) + specifier: ^0.0.10 + version: 0.0.10(eslint@7.32.0) '@storybook/addon-a11y': specifier: ^8.6.7 version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) @@ -592,8 +592,8 @@ importers: version: 18.3.1(react@18.3.1) devDependencies: '@linode/eslint-plugin-cloud-manager': - specifier: ^0.0.7 - version: 0.0.7(eslint@7.32.0) + specifier: ^0.0.10 + version: 0.0.10(eslint@7.32.0) '@testing-library/dom': specifier: ^10.1.0 version: 10.4.0 @@ -713,8 +713,8 @@ importers: version: 4.9.13(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) devDependencies: '@linode/eslint-plugin-cloud-manager': - specifier: ^0.0.7 - version: 0.0.7(eslint@7.32.0) + specifier: ^0.0.10 + version: 0.0.10(eslint@7.32.0) '@storybook/addon-actions': specifier: ^8.6.7 version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) @@ -786,8 +786,8 @@ importers: version: 18.3.1(react@18.3.1) devDependencies: '@linode/eslint-plugin-cloud-manager': - specifier: ^0.0.7 - version: 0.0.7(eslint@7.32.0) + specifier: ^0.0.10 + version: 0.0.10(eslint@7.32.0) '@testing-library/dom': specifier: ^10.1.0 version: 10.4.0 @@ -1640,8 +1640,8 @@ packages: '@linode/design-language-system@4.0.0': resolution: {integrity: sha512-SKM4AG0GpFjgirKI+7bG3RT6ai3VU7MJJLUvaZsHf0OgmEJ25qWH7DqGOx5FWSTtzX0YemJSrwnKMpL+3CLawg==} - '@linode/eslint-plugin-cloud-manager@0.0.7': - resolution: {integrity: sha512-83ZDbDQGsXCKxagX6CWszFZbsuX/fHSFn/i+P1FGYDm/0qnIo2XypB/lTdJhHJwvq1j2z+0VDZIMP7YLF5U6Sg==} + '@linode/eslint-plugin-cloud-manager@0.0.10': + resolution: {integrity: sha512-zaZZ8QHd89e3lSr6ZwKhqwKEzOASiJJksPdbyti68W2M3iC76CBuG6akAmC1/e/Z6KUNq1mHKNAxTUCBgkOmCg==} peerDependencies: eslint: ^6.8.0 @@ -7541,7 +7541,7 @@ snapshots: '@linode/design-language-system@4.0.0': {} - '@linode/eslint-plugin-cloud-manager@0.0.7(eslint@7.32.0)': + '@linode/eslint-plugin-cloud-manager@0.0.10(eslint@7.32.0)': dependencies: eslint: 7.32.0 From 1a0f0e4788dd76c6c0aa901e72e54b81532133fc Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 20 Mar 2025 11:27:35 -0400 Subject: [PATCH 05/84] upcoming: Enable Linode Interface query only when Delete Interface dialog is open and update Dialog title (#11881) * prevent queries when dialog not open * update copy to match firewall interface device row * Added changeset: Diable query to get Linode Interface when Interface Delete dialog is closed * address feedback --- .../.changeset/pr-11881-upcoming-features-1742334131588.md | 5 +++++ .../.changeset/pr-11881-upcoming-features-1742478234772.md | 5 +++++ .../LinodeInterfaces/DeleteInterfaceDialog.tsx | 4 ++-- packages/queries/src/linodes/interfaces.ts | 7 ++++--- 4 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 packages/manager/.changeset/pr-11881-upcoming-features-1742334131588.md create mode 100644 packages/manager/.changeset/pr-11881-upcoming-features-1742478234772.md diff --git a/packages/manager/.changeset/pr-11881-upcoming-features-1742334131588.md b/packages/manager/.changeset/pr-11881-upcoming-features-1742334131588.md new file mode 100644 index 00000000000..869a2527bf7 --- /dev/null +++ b/packages/manager/.changeset/pr-11881-upcoming-features-1742334131588.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Diable query to get Linode Interface when Interface Delete dialog is closed ([#11881](https://github.com/linode/manager/pull/11881)) diff --git a/packages/manager/.changeset/pr-11881-upcoming-features-1742478234772.md b/packages/manager/.changeset/pr-11881-upcoming-features-1742478234772.md new file mode 100644 index 00000000000..cafcca9055a --- /dev/null +++ b/packages/manager/.changeset/pr-11881-upcoming-features-1742478234772.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update title for Delete Interface dialog ([#11881](https://github.com/linode/manager/pull/11881)) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/DeleteInterfaceDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/DeleteInterfaceDialog.tsx index 48a77a540dd..e621d5ec828 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/DeleteInterfaceDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/DeleteInterfaceDialog.tsx @@ -26,7 +26,7 @@ export const DeleteInterfaceDialog = (props: Props) => { data: linodeInterface, error: interfaceError, isLoading, - } = useLinodeInterfaceQuery(linodeId, interfaceId ?? -1); + } = useLinodeInterfaceQuery(linodeId, interfaceId, open); const { error, isPending, mutate } = useDeleteLinodeInterfaceMutation( linodeId, @@ -63,7 +63,7 @@ export const DeleteInterfaceDialog = (props: Props) => { isFetching={isLoading} onClose={onClose} open={open} - title={`Delete ${type} Interface?`} + title={`Delete ${type} Interface (ID: ${interfaceId})?`} > Are you sure you want to delete this {type} interface? diff --git a/packages/queries/src/linodes/interfaces.ts b/packages/queries/src/linodes/interfaces.ts index 8c87f98b45b..56120fd33f0 100644 --- a/packages/queries/src/linodes/interfaces.ts +++ b/packages/queries/src/linodes/interfaces.ts @@ -27,13 +27,14 @@ export const useLinodeInterfacesQuery = (linodeId: number) => { export const useLinodeInterfaceQuery = ( linodeId: number, - interfaceId: number + interfaceId: number | undefined, + enabled: boolean = true ) => { return useQuery({ ...linodeQueries .linode(linodeId) - ._ctx.interfaces._ctx.interface(interfaceId), - enabled: Boolean(interfaceId), + ._ctx.interfaces._ctx.interface(interfaceId ?? -1), + enabled: enabled && interfaceId !== undefined, }); }; From 7f702453400962dcb67aa62e73be6aded462443c Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:16:20 -0400 Subject: [PATCH 06/84] test: [M3-9618] - Temporarily skip Firewall end-to-end tests (#11898) * Temporarily skip Firewall end-to-end tests * Added changeset: Temporarily skip Firewall end-to-end tests --- packages/manager/.changeset/pr-11898-tests-1742493507045.md | 5 +++++ .../cypress/e2e/core/firewalls/create-firewall.spec.ts | 2 +- .../cypress/e2e/core/firewalls/delete-firewall.spec.ts | 2 +- .../e2e/core/firewalls/migrate-linode-with-firewall.spec.ts | 2 +- .../cypress/e2e/core/firewalls/update-firewall.spec.ts | 2 +- 5 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-11898-tests-1742493507045.md diff --git a/packages/manager/.changeset/pr-11898-tests-1742493507045.md b/packages/manager/.changeset/pr-11898-tests-1742493507045.md new file mode 100644 index 00000000000..27a80eaac13 --- /dev/null +++ b/packages/manager/.changeset/pr-11898-tests-1742493507045.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Temporarily skip Firewall end-to-end tests ([#11898](https://github.com/linode/manager/pull/11898)) diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 314bc91aebf..26e8b9f1bdf 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -9,7 +9,7 @@ import { chooseRegion } from 'support/util/regions'; import { createLinodeRequestFactory } from 'src/factories/linodes'; authenticate(); -describe('create firewall', () => { +describe.skip('create firewall', () => { before(() => { cleanUp(['lke-clusters', 'linodes', 'firewalls']); }); diff --git a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts index 99bbabeb1a2..ce59d0b311c 100644 --- a/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/delete-firewall.spec.ts @@ -9,7 +9,7 @@ import { firewallFactory } from 'src/factories/firewalls'; import type { Firewall } from '@linode/api-v4'; authenticate(); -describe('delete firewall', () => { +describe.skip('delete firewall', () => { before(() => { cleanUp('firewalls'); }); diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index 7ad3575f76e..241e0652c41 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -144,7 +144,7 @@ describe('Migrate Linode With Firewall', () => { /* * - Uses real API data to create a Firewall, attach a Linode to it, then migrate the Linode. */ - it('migrates linode with firewall - real data', () => { + it.skip('migrates linode with firewall - real data', () => { cy.tag('method:e2e', 'purpose:dcTesting'); const [migrationRegionStart, migrationRegionEnd] = chooseRegions(2); const firewallLabel = randomLabel(); diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index 930a90bf216..e3422517b7e 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -167,7 +167,7 @@ const createLinodeAndFirewall = async ( }; authenticate(); -describe('update firewall', () => { +describe.skip('update firewall', () => { before(() => { cleanUp('firewalls'); }); From 3b9c13c754c7b0d6da3c1b46511e6d81476bb99e Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:18:14 -0400 Subject: [PATCH 07/84] security: [M3-9620] Remove unused utils with security vulnerabilities (#11899) * Remove reliance on regex for utils with security vulnerabilities * remove unused utils with security vulnerabilities * Changeset --- .../pr-11899-removed-1742563209985.md | 5 ++ .../utilities/src/helpers/stringUtils.test.ts | 49 +------------------ packages/utilities/src/helpers/stringUtils.ts | 44 ----------------- 3 files changed, 6 insertions(+), 92 deletions(-) create mode 100644 packages/utilities/.changeset/pr-11899-removed-1742563209985.md diff --git a/packages/utilities/.changeset/pr-11899-removed-1742563209985.md b/packages/utilities/.changeset/pr-11899-removed-1742563209985.md new file mode 100644 index 00000000000..ed8fcff927f --- /dev/null +++ b/packages/utilities/.changeset/pr-11899-removed-1742563209985.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Removed +--- + +Unused utils with security vulnerabilities ([#11899](https://github.com/linode/manager/pull/11899)) diff --git a/packages/utilities/src/helpers/stringUtils.test.ts b/packages/utilities/src/helpers/stringUtils.test.ts index 7456ac93783..35f6f4e8a27 100644 --- a/packages/utilities/src/helpers/stringUtils.test.ts +++ b/packages/utilities/src/helpers/stringUtils.test.ts @@ -1,12 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - getNextLabel, - getNumberAtEnd, - isNumeric, - removeNumberAtEnd, - truncateAndJoinList, -} from './stringUtils'; +import { isNumeric, truncateAndJoinList } from './stringUtils'; describe('truncateAndJoinList', () => { const strList = ['a', 'b', 'c']; @@ -57,44 +51,3 @@ describe('isNumeric', () => { expect(isNumeric('my-linode')).toBe(false); }); }); - -describe('getNumberAtEnd', () => { - it('should return 1 when given test-1', () => { - expect(getNumberAtEnd('test-1')).toBe(1); - }); - it('should return null if there is no number in the string', () => { - expect(getNumberAtEnd('test')).toBe(null); - }); - it('should get the last number in the string', () => { - expect(getNumberAtEnd('test-1-2-3')).toBe(3); - }); - it('should handle a string that only contains numbers', () => { - expect(getNumberAtEnd('123')).toBe(123); - }); -}); - -describe('removeNumberAtEnd', () => { - it('should return 1 in "test-1"', () => { - expect(removeNumberAtEnd('test-1')).toBe('test-'); - }); - it('should return the same string if there is no number at the end', () => { - expect(removeNumberAtEnd('test')).toBe('test'); - }); - it('should return an empty string if the input is just a number', () => { - expect(removeNumberAtEnd('123')).toBe(''); - }); - it('should not remove the first number', () => { - expect(removeNumberAtEnd('1-2-3')).toBe('1-2-'); - }); -}); - -describe('getNextLabel', () => { - it('should append a number to get the next label', () => { - expect(getNextLabel({ label: 'test' }, [{ label: 'test' }])).toBe('test-1'); - }); - it('should not duplicate labels so that the returned label is unique', () => { - expect(getNextLabel({ label: 'test' }, [{ label: 'test-1' }])).toBe( - 'test-2' - ); - }); -}); diff --git a/packages/utilities/src/helpers/stringUtils.ts b/packages/utilities/src/helpers/stringUtils.ts index e6a886355e3..c183b3af87d 100644 --- a/packages/utilities/src/helpers/stringUtils.ts +++ b/packages/utilities/src/helpers/stringUtils.ts @@ -28,47 +28,3 @@ export const truncateAndJoinList = ( export const wrapInQuotes = (s: string) => '"' + s + '"'; export const isNumeric = (s: string) => /^\d+$/.test(s); - -export function getNumberAtEnd(str: string) { - // Use a regular expression to match one or more digits at the end of the string - const match = str.match(/\d+$/); - - // If there is a match, return the matched number; otherwise, return null - return match ? parseInt(match[0], 10) : null; -} - -export function removeNumberAtEnd(str: string) { - // Use a regular expression to match one or more digits at the end of the string - const regex = /\d+$/; - - // Use the replace() method to remove the matched portion - return str.replace(regex, ''); -} - -/** - * Gets the next available unique entity label - */ -export function getNextLabel( - selectedEntity: T, - allEntities: T[] -): string { - const numberAtEnd = getNumberAtEnd(selectedEntity.label); - - let labelToReturn = ''; - - if (numberAtEnd === null) { - labelToReturn = `${selectedEntity.label}-1`; - } else { - labelToReturn = `${removeNumberAtEnd(selectedEntity.label)}${ - numberAtEnd + 1 - }`; - } - - if (allEntities.some((r) => r.label === labelToReturn)) { - return getNextLabel( - { ...selectedEntity, label: labelToReturn }, - allEntities - ); - } - return labelToReturn; -} From 52af395ffdb44db6312789537f2369ba49e2f154 Mon Sep 17 00:00:00 2001 From: bill-akamai Date: Fri, 21 Mar 2025 10:48:19 -0500 Subject: [PATCH 08/84] refactor: [M3-9435] [Akamai Design System] Breadcrumb Component (#11841) * Add final breadcrumb styling * Tokenize previous breadcrumb * Adjust breadcrumb bar height * Add dark mode Breadcrumb token * Update Breadcrumb stories * Added changeset: Updated Breadcrumb component to conform to Akamai Design System specs * Use cx utility for cleaner class names in EditableText.tsx, Improve class name handling in Crumbs * Fix conflicts * Fix spacing * Fix overall height * Use existing PreContainerDiv style --- .../pr-11841-changed-1741884096366.md | 5 ++ .../Breadcrumb/Breadcrumb.stories.tsx | 46 +++++++++++++++++++ .../components/Breadcrumb/Crumbs.styles.tsx | 11 +++-- .../src/components/Breadcrumb/Crumbs.tsx | 2 +- .../Breadcrumb/FinalCrumb.styles.tsx | 8 ++-- .../src/components/Breadcrumb/FinalCrumb.tsx | 1 + .../components/EditableText/EditableText.tsx | 26 ++++++++--- 7 files changed, 82 insertions(+), 17 deletions(-) create mode 100644 packages/manager/.changeset/pr-11841-changed-1741884096366.md diff --git a/packages/manager/.changeset/pr-11841-changed-1741884096366.md b/packages/manager/.changeset/pr-11841-changed-1741884096366.md new file mode 100644 index 00000000000..f40405718e9 --- /dev/null +++ b/packages/manager/.changeset/pr-11841-changed-1741884096366.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Updated Breadcrumb component to conform to Akamai Design System specs ([#11841](https://github.com/linode/manager/pull/11841)) diff --git a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx index a50d3a4206f..4edad9d654b 100644 --- a/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx +++ b/packages/manager/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -1,12 +1,57 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import React from 'react'; +import { Chip } from '@linode/ui'; import { Breadcrumb } from './Breadcrumb'; +const withBadgeCrumbs = [ + { + position: 3, + label: ( + <> + test + + + + + ), + }, +]; + +const noBadgeCrumbs = [ + { + position: 3, + label: test, + }, +]; + const meta: Meta = { component: Breadcrumb, title: 'Foundations/Breadcrumb', + argTypes: { + crumbOverrides: { + options: ['With Badge', 'No Badge'], + mapping: { + 'With Badge': withBadgeCrumbs, + 'No Badge': noBadgeCrumbs, + }, + control: { + type: 'radio', + labels: { + 'With Badge': 'Show Beta Badge', + 'No Badge': 'Hide Beta Badge', + }, + }, + defaultValue: 'No Badge', + }, + }, }; type Story = StoryObj; @@ -20,6 +65,7 @@ export const Default: Story = { onEdit: async () => action('onEdit'), }, pathname: '/linodes/9872893679817/test/lastcrumb', + crumbOverrides: noBadgeCrumbs, }, render: (args) => , }; diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx index 241f6705b90..8536c2dffd8 100644 --- a/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/Crumbs.styles.tsx @@ -3,23 +3,24 @@ import { styled } from '@mui/material'; export const StyledTypography = styled(Typography, { label: 'StyledTypography', -})(({}) => ({ +})(({ theme }) => ({ '&:hover': { textDecoration: 'underline', }, - fontSize: '1.125rem', + fontSize: '1rem', lineHeight: 'normal', textTransform: 'capitalize', whiteSpace: 'nowrap', + color: theme.tokens.component.Breadcrumb.LastItem.Text, })); export const StyledSlashTypography = styled(Typography, { label: 'StyledSlashTypography', })(({ theme }) => ({ color: theme.textColors.tableHeader, - fontSize: 20, - marginLeft: 2, - marginRight: 2, + fontSize: 16, + marginLeft: 4, + marginRight: 4, })); export const StyledDiv = styled('div', { label: 'StyledDiv' })({ diff --git a/packages/manager/src/components/Breadcrumb/Crumbs.tsx b/packages/manager/src/components/Breadcrumb/Crumbs.tsx index 174d86166c4..bd8e6585f87 100644 --- a/packages/manager/src/components/Breadcrumb/Crumbs.tsx +++ b/packages/manager/src/components/Breadcrumb/Crumbs.tsx @@ -14,7 +14,7 @@ import type { EditableProps, LabelProps } from './types'; import type { LinkProps } from 'react-router-dom'; export interface CrumbOverridesProps { - label?: string; + label?: string | React.ReactNode; linkTo?: LinkProps['to']; noCap?: boolean; position: number; diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx index af43289b40f..0fd695b3110 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.styles.tsx @@ -14,16 +14,16 @@ export const StyledEditableText = styled(EditableText, { '& > div': { width: 250, }, - marginLeft: `-${theme.spacing()}`, })); export const StyledH1Header = styled(H1Header, { label: 'StyledH1Header' })( ({ theme }) => ({ - color: theme.textColors.tableStatic, - fontSize: '1.125rem', + color: theme.tokens.component.Breadcrumb.Normal.Text.Default, + fontSize: '1rem', + paddingLeft: 0, textTransform: 'capitalize', [theme.breakpoints.up('lg')]: { - fontSize: '1.125rem', + fontSize: '1rem', }, }) ); diff --git a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx index 729f1222230..f5f011ff4fd 100644 --- a/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx +++ b/packages/manager/src/components/Breadcrumb/FinalCrumb.tsx @@ -38,6 +38,7 @@ export const FinalCrumb = React.memo((props: Props) => { disabledBreadcrumbEditButton={disabledBreadcrumbEditButton} errorText={onEditHandlers.errorText} handleAnalyticsEvent={onEditHandlers.handleAnalyticsEvent} + isBreadcrumb onCancel={onEditHandlers.onCancel} onEdit={onEditHandlers.onEdit} text={onEditHandlers.editableTextTitle} diff --git a/packages/ui/src/components/EditableText/EditableText.tsx b/packages/ui/src/components/EditableText/EditableText.tsx index a19323bc83f..541bab797c9 100644 --- a/packages/ui/src/components/EditableText/EditableText.tsx +++ b/packages/ui/src/components/EditableText/EditableText.tsx @@ -13,7 +13,7 @@ import type { TextFieldProps } from '../TextField'; import type { Theme } from '@mui/material/styles'; import type { PropsWithChildren } from 'react'; -const useStyles = makeStyles()( +const useStyles = makeStyles()( (theme: Theme, _params, classes) => ({ button: { '&[aria-label="Save"]': { @@ -99,6 +99,11 @@ const useStyles = makeStyles()( textDecoration: 'underline !important', }, }, + breadcrumbText: { + color: theme.tokens.component.Breadcrumb.Normal.Text.Default, + fontSize: '1rem !important', + paddingLeft: 0, + }, }) ); @@ -135,6 +140,10 @@ interface BaseProps extends Omit { * Optional suffix to append to the text when it is not in editing mode */ textSuffix?: string; + /** + * Whether this EditableText is used as a breadcrumb + */ + isBreadcrumb?: boolean; } interface PropsWithoutLink extends BaseProps { @@ -163,7 +172,7 @@ interface PropsWithLink extends BaseProps { export type EditableTextProps = PropsWithLink | PropsWithoutLink; export const EditableText = (props: EditableTextProps) => { - const { classes } = useStyles(); + const { classes, cx } = useStyles(); const [isEditing, setIsEditing] = React.useState(Boolean(props.errorText)); const [text, setText] = React.useState(props.text); @@ -173,6 +182,7 @@ export const EditableText = (props: EditableTextProps) => { disabledBreadcrumbEditButton, errorText, handleAnalyticsEvent, + isBreadcrumb, labelLink, onCancel, onEdit, @@ -237,7 +247,7 @@ export const EditableText = (props: EditableTextProps) => { }; const labelText = ( @@ -245,7 +255,7 @@ export const EditableText = (props: EditableTextProps) => { return !isEditing && !errorText ? (
{!!labelLink ? ( @@ -258,7 +268,7 @@ export const EditableText = (props: EditableTextProps) => { {/** pencil icon */}
) : ( -
+
Date: Fri, 21 Mar 2025 16:39:01 -0400 Subject: [PATCH 09/84] upcoming: [M3-9512] - Update types and validation for VPC subnet (#11896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Update types and validation for VPC Subnet ## Changes 🔄 - Updated `ipv6` type for `CreateSubnetPayload` - Separated `createSubnetSchema` into `createSubnetSchemaIPv4` and `createSubnetSchemaWithIPv6` - Added ipv6 prefix length validation for subnets in `vpcsValidateIP` - Updated `ipv6 subnet` schema validation in `vpcs.schema.ts` - Minor variable refactoring ## How to test 🧪 ### Verification steps Cross-reference VPC IPv6 API spec (linked in parent ticket) and updated types for: - [ ] POST /v4/vpcs/{vpcId}/subnets - [ ] GET /v4/vpcs/{vpcId}/subnets - [ ] GET v4/vpcs/{vpcId}/subnets/{vpcSubnetId} --- ...r-11896-upcoming-features-1742491246746.md | 5 + packages/api-v4/src/vpcs/types.ts | 10 +- packages/api-v4/src/vpcs/vpcs.ts | 4 +- .../VPCs/VPCDetail/SubnetCreateDrawer.tsx | 8 +- ...r-11896-upcoming-features-1742491352504.md | 5 + packages/validation/src/vpcs.schema.ts | 170 +++++++++++------- 6 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11896-upcoming-features-1742491246746.md create mode 100644 packages/validation/.changeset/pr-11896-upcoming-features-1742491352504.md diff --git a/packages/api-v4/.changeset/pr-11896-upcoming-features-1742491246746.md b/packages/api-v4/.changeset/pr-11896-upcoming-features-1742491246746.md new file mode 100644 index 00000000000..5e37a888bd7 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11896-upcoming-features-1742491246746.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Update `ipv6` type in `CreateSubnetPayload` and rename `createSubnetSchema` to `createSubnetSchemaIPv4` ([#11896](https://github.com/linode/manager/pull/11896)) diff --git a/packages/api-v4/src/vpcs/types.ts b/packages/api-v4/src/vpcs/types.ts index 28ce606fdc8..025ffd80a6b 100644 --- a/packages/api-v4/src/vpcs/types.ts +++ b/packages/api-v4/src/vpcs/types.ts @@ -2,7 +2,7 @@ interface VPCIPv6 { range?: string; } -interface CreateVPCIPV6 extends VPCIPv6 { +interface CreateVPCIPv6 extends VPCIPv6 { allocation_class?: string; } @@ -21,7 +21,7 @@ export interface CreateVPCPayload { label: string; description?: string; region: string; - ipv6?: CreateVPCIPV6[]; + ipv6?: CreateVPCIPv6[]; subnets?: CreateSubnetPayload[]; } @@ -30,10 +30,14 @@ export interface UpdateVPCPayload { description?: string; } +interface VPCIPv6Subnet { + range: string; +} + export interface CreateSubnetPayload { label: string; ipv4?: string; - ipv6?: string; + ipv6?: VPCIPv6Subnet[]; } export interface Subnet extends CreateSubnetPayload { diff --git a/packages/api-v4/src/vpcs/vpcs.ts b/packages/api-v4/src/vpcs/vpcs.ts index 30254700f6e..88daa6015bf 100644 --- a/packages/api-v4/src/vpcs/vpcs.ts +++ b/packages/api-v4/src/vpcs/vpcs.ts @@ -1,5 +1,5 @@ import { - createSubnetSchema, + createSubnetSchemaIPv4, createVPCSchema, modifySubnetSchema, updateVPCSchema, @@ -129,7 +129,7 @@ export const createSubnet = (vpcID: number, data: CreateSubnetPayload) => Request( setURL(`${API_ROOT}/vpcs/${encodeURIComponent(vpcID)}/subnets`), setMethod('POST'), - setData(data, createSubnetSchema) + setData(data, createSubnetSchemaIPv4) ); /** diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx index 83b39cda728..256b17e5d2c 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx @@ -13,7 +13,7 @@ import { Stack, TextField, } from '@linode/ui'; -import { createSubnetSchema } from '@linode/validation'; +import { createSubnetSchemaIPv4 } from '@linode/validation'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -25,7 +25,7 @@ import { getRecommendedSubnetIPv4, } from 'src/utilities/subnets'; -import type { CreateSubnetPayload } from '@linode/api-v4'; +import type { CreateSubnetPayload, Subnet } from '@linode/api-v4'; interface Props { onClose: () => void; @@ -44,7 +44,7 @@ export const SubnetCreateDrawer = (props: Props) => { const recommendedIPv4 = getRecommendedSubnetIPv4( DEFAULT_SUBNET_IPV4_VALUE, - vpc?.subnets?.map((subnet) => subnet.ipv4 ?? '') ?? [] + vpc?.subnets?.map((subnet: Subnet) => subnet.ipv4 ?? '') ?? [] ); const { @@ -66,7 +66,7 @@ export const SubnetCreateDrawer = (props: Props) => { label: '', }, mode: 'onBlur', - resolver: yupResolver(createSubnetSchema), + resolver: yupResolver(createSubnetSchemaIPv4), }); const ipv4 = watch('ipv4'); diff --git a/packages/validation/.changeset/pr-11896-upcoming-features-1742491352504.md b/packages/validation/.changeset/pr-11896-upcoming-features-1742491352504.md new file mode 100644 index 00000000000..afa9a26dc9f --- /dev/null +++ b/packages/validation/.changeset/pr-11896-upcoming-features-1742491352504.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Upcoming Features +--- + +Update `ipv6` vpc schema validation for subnets, separate `createSubnetSchema` into `createSubnetSchemaIPv4` and `createSubnetSchemaWithIPv6` ([#11896](https://github.com/linode/manager/pull/11896)) diff --git a/packages/validation/src/vpcs.schema.ts b/packages/validation/src/vpcs.schema.ts index 076eaa8ca81..13263afc0f4 100644 --- a/packages/validation/src/vpcs.schema.ts +++ b/packages/validation/src/vpcs.schema.ts @@ -13,7 +13,7 @@ const labelTestDetails = { const IP_EITHER_BOTH_NOT_NEITHER = 'A subnet must have either IPv4 or IPv6, or both, but not neither.'; -// @TODO VPC - remove below constant when IPv6 is added +// @TODO VPC IPv6 - remove below constant when IPv6 is in GA const TEMPORARY_IPV4_REQUIRED_MESSAGE = 'A subnet must have an IPv4 range.'; export const determineIPType = (ip: string) => { @@ -38,16 +38,19 @@ export const determineIPType = (ip: string) => { * @param { value } - the IP address string to be validated * @param { shouldHaveIPMask } - a boolean indicating whether the value should have a mask (e.g., /32) or not * @param { mustBeIPMask } - a boolean indicating whether the value MUST be an IP mask/prefix length or not + * @param { isIPv6Subnet } - a boolean indicating whether the IPv6 value is for a subnet */ export const vpcsValidateIP = ({ value, shouldHaveIPMask, mustBeIPMask, + isIPv6Subnet, }: { value: string | undefined | null; shouldHaveIPMask: boolean; mustBeIPMask: boolean; + isIPv6Subnet?: boolean; }): boolean => { if (!value) { return false; @@ -95,9 +98,17 @@ export const vpcsValidateIP = ({ if (isIPv6) { // VPCs must be assigned an IPv6 prefix of /52, /48, or /44 - if (!['52', '48', '44'].includes(mask)) { + const invalidVPCIPv6Prefix = !['52', '48', '44'].includes(mask); + if (!isIPv6Subnet && invalidVPCIPv6Prefix) { return false; } + + // VPC subnets must be assigned an IPv6 prefix of 52-62 + const invalidVPCIPv6SubnetPrefix = +mask < 52 || +mask > 62; + if (isIPv6Subnet && invalidVPCIPv6SubnetPrefix) { + return false; + } + if (shouldHaveIPMask) { ipaddr.IPv6.parseCIDR(value); } else { @@ -127,16 +138,93 @@ export const updateVPCSchema = object({ description: string(), }); -export const createSubnetSchema = object().shape( +const VPCIPv6Schema = object({ + range: string() + .optional() + .test({ + name: 'IPv6 prefix length', + message: 'Must be the prefix length 52, 48, or 44 of the IP, e.g. /52', + test: (value) => { + if (value && value.length > 0) { + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, + }); + } + }, + }), +}); + +const VPCIPv6SubnetSchema = object({ + range: string() + .required() + .test({ + name: 'IPv6 prefix length', + message: 'Must be the prefix length (52-62) of the IP, e.g. /52', + test: (value) => { + if (value && value !== 'auto' && value.length > 0) { + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, + isIPv6Subnet: true, + }); + } + }, + }), +}); + +// @TODO VPC IPv6: Delete this when IPv6 is in GA +export const createSubnetSchemaIPv4 = object({ + label: labelValidation.required(LABEL_REQUIRED), + ipv4: string().when('ipv6', { + is: (value: unknown) => + value === '' || value === null || value === undefined, + then: (schema) => + schema.required(TEMPORARY_IPV4_REQUIRED_MESSAGE).test({ + name: 'IPv4 CIDR format', + message: 'The IPv4 range must be in CIDR format.', + test: (value) => + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, + }), + }), + otherwise: (schema) => + lazy((value: string | undefined) => { + switch (typeof value) { + case 'undefined': + return schema.notRequired().nullable(); + + case 'string': + return schema.notRequired().test({ + name: 'IPv4 CIDR format', + message: 'The IPv4 range must be in CIDR format.', + test: (value) => + vpcsValidateIP({ + value, + shouldHaveIPMask: true, + mustBeIPMask: false, + }), + }); + + default: + return schema.notRequired().nullable(); + } + }), + }), +}); + +export const createSubnetSchemaWithIPv6 = object().shape( { label: labelValidation.required(LABEL_REQUIRED), ipv4: string().when('ipv6', { is: (value: unknown) => value === '' || value === null || value === undefined, then: (schema) => - // @TODO VPC - change required message back to IP_EITHER_BOTH_NOT_NEITHER when IPv6 is supported - // Since only IPv4 is currently supported, subnets must have an IPv4 - schema.required(TEMPORARY_IPV4_REQUIRED_MESSAGE).test({ + schema.required(IP_EITHER_BOTH_NOT_NEITHER).test({ name: 'IPv4 CIDR format', message: 'The IPv4 range must be in CIDR format.', test: (value) => @@ -169,44 +257,13 @@ export const createSubnetSchema = object().shape( } }), }), - ipv6: string().when('ipv4', { - is: (value: unknown) => - value === '' || value === null || value === undefined, - then: (schema) => - schema.required(IP_EITHER_BOTH_NOT_NEITHER).test({ - name: 'IPv6 prefix length', - message: 'Must be the prefix length (64-125) of the IP, e.g. /64', - test: (value) => - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: true, - }), - }), - otherwise: (schema) => - lazy((value: string | undefined) => { - switch (typeof value) { - case 'undefined': - return schema.notRequired().nullable(); - - case 'string': - return schema.notRequired().test({ - name: 'IPv6 prefix length', - message: - 'Must be the prefix length (64-125) of the IP, e.g. /64', - test: (value) => - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: true, - }), - }); - - default: - return schema.notRequired().nullable(); - } - }), - }), + ipv6: array() + .of(VPCIPv6SubnetSchema) + .when('ipv4', { + is: (value: unknown) => + value === '' || value === null || value === undefined, + then: (schema) => schema.required(IP_EITHER_BOTH_NOT_NEITHER), + }), }, [ ['ipv6', 'ipv4'], @@ -214,30 +271,17 @@ export const createSubnetSchema = object().shape( ] ); -const createVPCIPv6Schema = object({ - range: string() - .optional() - .test({ - name: 'IPv6 prefix length', - message: 'Must be the prefix length 52, 48, or 44 of the IP, e.g. /52', - test: (value) => { - if (value && value !== 'auto' && value.length > 0) { - vpcsValidateIP({ - value, - shouldHaveIPMask: true, - mustBeIPMask: false, - }); - } - }, - }), - allocation_class: string().optional(), -}); +const createVPCIPv6Schema = VPCIPv6Schema.concat( + object({ + allocation_class: string().optional(), + }) +); export const createVPCSchema = object({ label: labelValidation.required(LABEL_REQUIRED), description: string(), region: string().required('Region is required'), - subnets: array().of(createSubnetSchema), + subnets: array().of(createSubnetSchemaIPv4), ipv6: array().of(createVPCIPv6Schema).max(1).optional(), }); From 3b2ee2238c675b5e71e8076e5d2050ac3505744c Mon Sep 17 00:00:00 2001 From: bill-akamai Date: Mon, 24 Mar 2025 11:24:01 -0500 Subject: [PATCH 10/84] refactor: [M3-9431] [Akamai Design System] Radio Button Component (#11878) * Tokenize radio button styles * Add RadioButtonTypes * Fix viewbox size * Add dark Radio Button types and object * Remove hardcoded color fill for disabled button * Added changeset: Updated Radio Button component to conform to Akamai Design System specs * Fix Radio component test to match updated SVG structure * Use new tokens accessors * Add correct font-size to dark mode * Create consistent padding b/t light and dark themes * Share fontSize prop b/t light and dark --- .../pr-11878-changed-1742325899299.md | 5 +++ packages/ui/src/assets/icons/radio.svg | 7 ++-- packages/ui/src/assets/icons/radioRadioed.svg | 17 ++------- .../ui/src/components/Radio/Radio.test.tsx | 7 ++-- packages/ui/src/components/Radio/Radio.tsx | 4 +-- packages/ui/src/foundations/themes/dark.ts | 1 + packages/ui/src/foundations/themes/light.ts | 36 ++++++++++++------- 7 files changed, 40 insertions(+), 37 deletions(-) create mode 100644 packages/manager/.changeset/pr-11878-changed-1742325899299.md diff --git a/packages/manager/.changeset/pr-11878-changed-1742325899299.md b/packages/manager/.changeset/pr-11878-changed-1742325899299.md new file mode 100644 index 00000000000..ab398ad9321 --- /dev/null +++ b/packages/manager/.changeset/pr-11878-changed-1742325899299.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Updated Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) diff --git a/packages/ui/src/assets/icons/radio.svg b/packages/ui/src/assets/icons/radio.svg index 22bc08e92d2..ee0ce1fcfcb 100644 --- a/packages/ui/src/assets/icons/radio.svg +++ b/packages/ui/src/assets/icons/radio.svg @@ -1,6 +1,3 @@ - - - - - + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/radioRadioed.svg b/packages/ui/src/assets/icons/radioRadioed.svg index 76031bbcb4a..0b99e9ec03d 100644 --- a/packages/ui/src/assets/icons/radioRadioed.svg +++ b/packages/ui/src/assets/icons/radioRadioed.svg @@ -1,16 +1,3 @@ - - - - - - - - - - - - - - - + + \ No newline at end of file diff --git a/packages/ui/src/components/Radio/Radio.test.tsx b/packages/ui/src/components/Radio/Radio.test.tsx index 27f7aebecf1..6e7af47fe5f 100644 --- a/packages/ui/src/components/Radio/Radio.test.tsx +++ b/packages/ui/src/components/Radio/Radio.test.tsx @@ -12,10 +12,13 @@ describe('Radio', () => { const radio = screen.getByRole('radio'); expect(radio).toBeInTheDocument(); - const notFilled = screen.container.querySelector('[id="Oval-2"]'); + + const notFilled = screen.container.querySelector('#radio-inner'); expect(notFilled).not.toBeInTheDocument(); + fireEvent.click(radio); - const filled = screen.container.querySelector('[id="Oval-2"]'); + + const filled = screen.container.querySelector('#radio-inner'); expect(filled).toBeInTheDocument(); }); diff --git a/packages/ui/src/components/Radio/Radio.tsx b/packages/ui/src/components/Radio/Radio.tsx index 7beaab125f9..77f331c49c0 100644 --- a/packages/ui/src/components/Radio/Radio.tsx +++ b/packages/ui/src/components/Radio/Radio.tsx @@ -31,14 +31,14 @@ export const Radio = (props: RadioProps) => { } icon={ } data-qa-radio={props.checked || false} diff --git a/packages/ui/src/foundations/themes/dark.ts b/packages/ui/src/foundations/themes/dark.ts index 843623a573d..f2ed61e17cd 100644 --- a/packages/ui/src/foundations/themes/dark.ts +++ b/packages/ui/src/foundations/themes/dark.ts @@ -757,6 +757,7 @@ export const darkTheme: ThemeOptions = { '&:hover': { color: theme.palette.primary.main, }, + padding: '10px 10px', }), }, }, diff --git a/packages/ui/src/foundations/themes/light.ts b/packages/ui/src/foundations/themes/light.ts index c21153eab78..d3a82697eae 100644 --- a/packages/ui/src/foundations/themes/light.ts +++ b/packages/ui/src/foundations/themes/light.ts @@ -1124,25 +1124,35 @@ export const lightTheme: ThemeOptions = { color: primaryColors.main, }, root: ({ theme }) => ({ - '& $checked': { - color: primaryColors.main, + '&:active': { + color: theme.tokens.component.RadioButton.Active.Active.Border, + }, + '&.Mui-checked': { + color: theme.tokens.component.RadioButton.Active.Default.Border, + '&:active': { + color: theme.tokens.component.RadioButton.Active.Active.Border, + }, }, '& .defaultFill': { fill: theme.color.white, transition: theme.transitions.create(['fill']), }, + '& svg circle': { + fill: Color.Neutrals.White, + }, '&.Mui-disabled': { '& .defaultFill': { fill: Color.Neutrals[5], }, - color: `${Color.Neutrals[40]} !important`, - fill: `${Color.Neutrals[5]} !important`, - pointerEvents: 'none', - }, - '&.MuiRadio-root': { - '.MuiSvgIcon-fontSizeMedium': { - fontSize: '20px', + '&:not(.Mui-checked) svg circle': { + fill: Color.Neutrals[20], + }, + '&:not(.Mui-checked)': { + color: + theme.tokens.component.RadioButton.Inactive.Disabled.Border, }, + color: theme.tokens.component.RadioButton.Active.Disabled.Border, + pointerEvents: 'none', }, '&.MuiRadio-sizeSmall': { '.MuiSvgIcon-fontSizeSmall': { @@ -1153,10 +1163,10 @@ export const lightTheme: ThemeOptions = { '& .defaultFill': { fill: theme.color.white, }, - color: theme.palette.primary.main, - fill: theme.color.white, + color: theme.tokens.component.RadioButton.Active.Hover.Border, + fill: theme.tokens.component.RadioButton.Active.Hover.Background, }, - color: Color.Neutrals[40], + color: theme.tokens.alias.Action.Neutral, padding: '10px 10px', transition: theme.transitions.create(['color']), }), @@ -1208,7 +1218,7 @@ export const lightTheme: ThemeOptions = { MuiSvgIcon: { styleOverrides: { root: { - fontSize: 24, + fontSize: 20, }, }, }, From 4d1476e0ea5645dfc07edd5e07c407c029714c59 Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:16:17 -0400 Subject: [PATCH 11/84] test: [M3-8668] - Disallow legacy regions from being selected via `chooseRegion` (#11892) * Disallow legacy regions from being selected via `chooseRegion` * Added changeset: Prevent legacy regions from being used by Cypress tests --- .../pr-11892-tests-1742564481277.md | 5 ++++ .../manager/cypress/support/util/regions.ts | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 packages/manager/.changeset/pr-11892-tests-1742564481277.md diff --git a/packages/manager/.changeset/pr-11892-tests-1742564481277.md b/packages/manager/.changeset/pr-11892-tests-1742564481277.md new file mode 100644 index 00000000000..09b9073190d --- /dev/null +++ b/packages/manager/.changeset/pr-11892-tests-1742564481277.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Prevent legacy regions from being used by Cypress tests ([#11892](https://github.com/linode/manager/pull/11892)) diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index 1df135b8ffb..bda74ea62d3 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -100,6 +100,36 @@ const disallowedRegionIds = [ // Washington, DC 'us-iad', + + // Atlanta, GA + 'us-southeast', + + // Dallas, TX + 'us-central', + + // Frankfurt, DE + 'eu-central', + + // Fremont, CA + 'us-west', + + // London, GB + 'eu-west', + + // Mumbai, IN + 'ap-west', + + // Newark, NJ + 'us-east', + + // Singapore, SG + 'ap-south', + + // Sydney, AU + 'ap-southeast', + + // Toronto, CA + 'ca-central', ]; /** From 7ea6ebf1f407b805d66241404a110081d7b44fba Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:03:45 -0400 Subject: [PATCH 12/84] upcoming: [M3-9109] - Update Firewall devices Linode landing table to account for new interfaces (#11842) * update firewall device Linodes table * update labeling * useAllLinodesQuery instead of individually querying each Linode * update comments * update comment again.. * Added changeset: Update Firewall Devices Linode landing table to account for new interface devices * hide more stuff behind feature flag * remove no longer needed changes * add unit tests for rows * update test details * address feedback --- packages/api-v4/src/firewalls/types.ts | 2 +- ...r-11842-upcoming-features-1742225152205.md | 5 + .../Devices/FirewallDeviceRow.test.tsx | 100 ++++++++++++++++++ .../Devices/FirewallDeviceRow.tsx | 60 ++++++++--- .../Devices/FirewallDeviceTable.tsx | 59 ++++++++++- .../Devices/RemoveDeviceDialog.tsx | 15 ++- .../FirewallDetail/Devices/constants.ts | 2 +- .../Firewalls/FirewallDetail/index.tsx | 23 +++- .../FirewallLanding/FirewallRow.test.tsx | 2 +- .../features/TopMenu/SearchBar/SearchBar.tsx | 44 +++++--- packages/manager/src/mocks/types.ts | 2 +- packages/queries/src/firewalls/firewalls.ts | 2 +- packages/queries/src/linodes/linodes.ts | 14 +++ 13 files changed, 288 insertions(+), 42 deletions(-) create mode 100644 packages/manager/.changeset/pr-11842-upcoming-features-1742225152205.md create mode 100644 packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.test.tsx diff --git a/packages/api-v4/src/firewalls/types.ts b/packages/api-v4/src/firewalls/types.ts index 2f21b3a5022..ec3ce8dea40 100644 --- a/packages/api-v4/src/firewalls/types.ts +++ b/packages/api-v4/src/firewalls/types.ts @@ -48,7 +48,7 @@ export interface FirewallRuleType { export interface FirewallDeviceEntity { id: number; type: FirewallDeviceEntityType; - label: string; + label: string | null; url: string; } diff --git a/packages/manager/.changeset/pr-11842-upcoming-features-1742225152205.md b/packages/manager/.changeset/pr-11842-upcoming-features-1742225152205.md new file mode 100644 index 00000000000..9152404dae5 --- /dev/null +++ b/packages/manager/.changeset/pr-11842-upcoming-features-1742225152205.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Firewall Devices Linode landing table to account for new interface devices ([#11842](https://github.com/linode/manager/pull/11842)) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.test.tsx new file mode 100644 index 00000000000..4ed709c6a99 --- /dev/null +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.test.tsx @@ -0,0 +1,100 @@ +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; + +import { firewallDeviceFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { FirewallDeviceRow } from './FirewallDeviceRow'; + +import type { FirewallDeviceEntityType } from '@linode/api-v4'; + +const props = { + device: firewallDeviceFactory.build(), + disabled: false, + handleRemoveDevice: vi.fn(), + isLinodeRelatedDevice: true, +}; + +const INTERFACE_TEXT = 'Configuration Profile Interface'; + +describe('FirewallDeviceRow', () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it('shows the network interface type if the linodeInterfaces feature flag is enabled for Linode related devices', () => { + const { getAllByRole, getByText } = renderWithTheme( + , + { flags: { linodeInterfaces: { enabled: true } } } + ); + + expect(getByText('entity')).toBeVisible(); + expect(getByText(INTERFACE_TEXT)).toBeVisible(); + expect(getAllByRole('cell')).toHaveLength(3); + expect(getByText('Remove')).toBeVisible(); + }); + + it('does not show the network interface type if the linodeInterfaces feature flag is not enabled for Linode related devices', () => { + const { getAllByRole, getByText, queryByText } = renderWithTheme( + , + { + flags: { linodeInterfaces: { enabled: false } }, + } + ); + + expect(getByText('entity')).toBeVisible(); + expect(queryByText(INTERFACE_TEXT)).not.toBeInTheDocument(); + expect(getAllByRole('cell')).toHaveLength(2); + expect(getByText('Remove')).toBeVisible(); + }); + + it('does not show the network interface type for nodebalancer devices', () => { + const nodeBalancerEntity = firewallDeviceFactory.build({ + entity: { + id: 10, + label: 'entity', + type: 'nodebalancer' as FirewallDeviceEntityType, + url: '/linodes/1', + }, + }); + + const { + getAllByRole, + getByText, + queryByText, + } = renderWithTheme( + , + { flags: { linodeInterfaces: { enabled: true } } } + ); + + expect(getByText('entity')).toBeVisible(); + expect(queryByText(INTERFACE_TEXT)).not.toBeInTheDocument(); + expect(getAllByRole('cell')).toHaveLength(2); + expect(getByText('Remove')).toBeVisible(); + }); + + it('can remove a device with an enabled Remove button', async () => { + const { getByText } = renderWithTheme(, { + flags: { linodeInterfaces: { enabled: true } }, + }); + + const removeButton = getByText('Remove'); + await userEvent.click(removeButton); + expect(props.handleRemoveDevice).toHaveBeenCalledTimes(1); + }); + + it('cannot remove a device with a disabled Remove button', async () => { + const { getByText } = renderWithTheme( + , + { flags: { linodeInterfaces: { enabled: true } } } + ); + + const removeButton = getByText('Remove'); + await userEvent.click(removeButton); + expect(props.handleRemoveDevice).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx index 91d47cbbb80..08250082cac 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx @@ -1,29 +1,59 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; +import { Skeleton } from 'src/components/Skeleton'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { FirewallDeviceActionMenu } from './FirewallDeviceActionMenu'; import type { FirewallDeviceActionMenuProps } from './FirewallDeviceActionMenu'; -export const FirewallDeviceRow = React.memo( - (props: FirewallDeviceActionMenuProps) => { - const { device } = props; - const { id, label, type } = device.entity; +interface FirewallDeviceRowProps extends FirewallDeviceActionMenuProps { + isLinodeRelatedDevice: boolean; +} - return ( - - - +export const FirewallDeviceRow = React.memo((props: FirewallDeviceRowProps) => { + const { device, isLinodeRelatedDevice } = props; + const { id, label, type, url } = device.entity; + + const isInterfaceDevice = type === 'interface'; + // for Linode Interfaces, the url comes in as '/v4/linode/instances/:linodeId/interfaces/:interfaceId + // we need the Linode ID to create a link + const entityId = isInterfaceDevice ? Number(url.split('/')[4]) : id; + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + return ( + + + {/* The only time a firewall device's label comes in as null is for Linode Interface devices. This label won't stay null - we do some + processing to give the interface device its associated Linode's label. However, processing may take time, so we show a loading indicator first */} + {isInterfaceDevice && !label ? ( + + ) : ( + // @TODO Linode Interfaces - perhaps link to the interface's details later + {label} + )} + + {isLinodeInterfacesEnabled && isLinodeRelatedDevice && ( + + {isInterfaceDevice + ? `Linode Interface (ID: ${id})` + : 'Configuration Profile Interface'} - - - - - ); - } -); + )} + + + + + ); +}); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx index 90f784bc354..903bd4d3259 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx @@ -1,9 +1,13 @@ -import { useAllFirewallDevicesQuery } from '@linode/queries'; +import { + useAllFirewallDevicesQuery, + useAllLinodesQuery, +} from '@linode/queries'; import * as React from 'react'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; +import { TableCell } from 'src/components/TableCell'; import { TableContentWrapper } from 'src/components/TableContentWrapper/TableContentWrapper'; import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; @@ -11,6 +15,7 @@ import { TableSortCell } from 'src/components/TableSortCell'; import { useOrderV2 } from 'src/hooks/useOrderV2'; import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { formattedTypes } from './constants'; import { FirewallDeviceRow } from './FirewallDeviceRow'; @@ -35,11 +40,52 @@ export const FirewallDeviceTable = React.memo( type, } = props; + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + const { data: allDevices, error, isLoading } = useAllFirewallDevicesQuery( firewallId ); const devices = - allDevices?.filter((device) => device.entity.type === type) || []; + allDevices?.filter((device) => + type === 'linode' && isLinodeInterfacesEnabled + ? device.entity.type !== 'nodebalancer' // include entities with type 'interface' in Linode table + : device.entity.type === type + ) || []; + + const linodeInterfaceDevices = + type === 'linode' + ? allDevices?.filter((device) => device.entity.type === 'interface') + : []; + + // only fire this query if we have linode interface devices. We fetch the Linodes those devices are attached to + // so that we can add a label to the devices for sorting and display purposes + const { data: linodesWithInterfaces } = useAllLinodesQuery( + {}, + {}, + isLinodeInterfacesEnabled && + linodeInterfaceDevices && + linodeInterfaceDevices.length > 0 + ); + + const updatedDevices = devices.map((device) => { + if (device.entity.type === 'interface') { + const linodeId = Number(device.entity.url.split('/')[4]); + const associatedLinode = linodesWithInterfaces?.find( + (linode) => linode.id === linodeId + ); + return { + ...device, + entity: { + ...device.entity, + label: associatedLinode?.label ?? null, + }, + }; + } else { + return device; + } + }); + + const isLinodeRelatedDevice = type === 'linode'; const _error = error ? getAPIErrorOrDefault( @@ -56,7 +102,7 @@ export const FirewallDeviceTable = React.memo( orderBy, sortedData: sortedDevices, } = useOrderV2({ - data: devices, + data: updatedDevices, initialRoute: { defaultOrder: { order: 'asc', @@ -85,14 +131,18 @@ export const FirewallDeviceTable = React.memo( {formattedTypes[deviceType]} + {isLinodeInterfacesEnabled && isLinodeRelatedDevice && ( + Network Interface + )} + @@ -106,6 +156,7 @@ export const FirewallDeviceTable = React.memo( device={thisDevice} disabled={disabled} handleRemoveDevice={handleRemoveDevice} + isLinodeRelatedDevice={isLinodeRelatedDevice} key={`device-row-${thisDevice.id}`} /> ))} diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx index e16712d50c1..65beddd7d70 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/RemoveDeviceDialog.tsx @@ -10,6 +10,8 @@ import * as React from 'react'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { formattedTypes } from './constants'; + import type { FirewallDevice } from '@linode/api-v4'; export interface Props { @@ -36,6 +38,11 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { const { enqueueSnackbar } = useSnackbar(); const deviceType = device?.entity.type; + const entityLabelToUse = + deviceType === 'interface' + ? `(ID: ${device?.entity.id})` + : device?.entity.label; + const { error, isPending, mutateAsync } = useRemoveFirewallDeviceMutation( firewallId, device?.id ?? -1 @@ -43,7 +50,7 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { const queryClient = useQueryClient(); - const deviceDialog = deviceType === 'linode' ? 'Linode' : 'NodeBalancer'; + const deviceDialog = formattedTypes[deviceType ?? 'linode']; const onDelete = async () => { if (!device) { @@ -54,7 +61,7 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { const toastMessage = onService ? `Firewall ${firewallLabel} successfully unassigned` - : `${deviceDialog} ${device.entity.label} successfully removed`; + : `${deviceDialog} ${entityLabelToUse} successfully removed`; enqueueSnackbar(toastMessage, { variant: 'success', @@ -84,14 +91,14 @@ export const RemoveDeviceDialog = React.memo((props: Props) => { const dialogTitle = onService ? `Unassign Firewall ${firewallLabel}?` - : `Remove ${deviceDialog} ${device?.entity.label}?`; + : `Remove ${deviceDialog} ${entityLabelToUse}?`; const confirmationText = ( Are you sure you want to{' '} {onService ? `unassign Firewall ${firewallLabel} from ${deviceDialog} ${device?.entity.label}?` - : `remove ${deviceDialog} ${device?.entity.label} from Firewall ${firewallLabel}?`} + : `remove ${deviceDialog} ${entityLabelToUse} from Firewall ${firewallLabel}?`} ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts index 9234255a29c..87178d2420a 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/constants.ts @@ -1,7 +1,7 @@ import type { FirewallDeviceEntityType } from '@linode/api-v4'; export const formattedTypes: Record = { - interface: 'Interface', // @TODO Linode Interface: double check this when working on UI tickets + interface: 'Linode Interface', linode: 'Linode', nodebalancer: 'NodeBalancer', }; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 55ef3363546..619dceaaba9 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -23,6 +23,7 @@ import { useFlags } from 'src/hooks/useFlags'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; import { useTabs } from 'src/hooks/useTabs'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { checkIfUserCanModifyFirewall } from '../shared'; @@ -48,6 +49,8 @@ export const FirewallDetail = () => { const flags = useFlags(); const [isGenerateDialogOpen, setIsGenerateDialogOpen] = React.useState(false); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + const secureVMFirewallBanner = (secureVMNoticesEnabled && flags.secureVmCopy) ?? false; @@ -67,11 +70,27 @@ export const FirewallDetail = () => { acc.linodeCount += 1; } else if (device.entity.type === 'nodebalancer') { acc.nodebalancerCount += 1; + } else if ( + isLinodeInterfacesEnabled && + device.entity.type === 'interface' + ) { + const linodeId = device.entity.url.split('/')[4]; + if (!acc.seenLinodeIdsForInterfaces.has(linodeId)) { + acc.linodeCount += 1; + } + acc.seenLinodeIdsForInterfaces.add(linodeId); } return acc; }, - { linodeCount: 0, nodebalancerCount: 0 } - ) || { linodeCount: 0, nodebalancerCount: 0 }; + { + linodeCount: 0, + nodebalancerCount: 0, + seenLinodeIdsForInterfaces: new Set(), + } + ) || { + linodeCount: 0, + nodebalancerCount: 0, + }; const { handleTabChange, tabIndex, tabs } = useTabs([ { diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index edd7962269f..6dd12b34133 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -70,7 +70,7 @@ describe('FirewallRow', () => { const device = firewallDeviceFactory.build(); const links = getDeviceLinks([device.entity]); const { getByText } = renderWithTheme(links); - expect(getByText(device.entity.label)); + expect(getByText(device.entity.label ?? '')); }); it('should render up to three comma-separated links', () => { diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 8433189c58c..ec272fbef40 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -208,7 +208,9 @@ export const SearchBar = () => { ...params.inputProps, sx: { '&::placeholder': { - color: theme.tokens.component.GlobalHeader.Search.Text.Placeholder, + color: + theme.tokens.component.GlobalHeader.Search.Text + .Placeholder, }, }, }, @@ -220,9 +222,13 @@ export const SearchBar = () => { sx={{ '> svg': { '&:hover': { - color: theme.tokens.component.GlobalHeader.Search.Icon.Hover, + color: + theme.tokens.component.GlobalHeader.Search + .Icon.Hover, }, - color: theme.tokens.component.GlobalHeader.Search.Icon.Default, + color: + theme.tokens.component.GlobalHeader.Search.Icon + .Default, }, padding: 0, [theme.breakpoints.up('sm')]: { @@ -245,18 +251,32 @@ export const SearchBar = () => { ), sx: { '&:active, &:focus, &.Mui-focused, &.Mui-focused:hover': { - backgroundColor: theme.tokens.component.GlobalHeader.Search.Background, - borderColor: theme.tokens.component.GlobalHeader.Search.Border.Active, - color: theme.tokens.component.GlobalHeader.Search.Text.Filled, + backgroundColor: + theme.tokens.component.GlobalHeader.Search.Background, + borderColor: + theme.tokens.component.GlobalHeader.Search.Border + .Active, + color: + theme.tokens.component.GlobalHeader.Search.Text + .Filled, }, '&:hover': { - backgroundColor: theme.tokens.component.GlobalHeader.Search.Background, - borderColor: theme.tokens.component.GlobalHeader.Search.Border.Hover, - color: theme.tokens.component.GlobalHeader.Search.Text.Filled, + backgroundColor: + theme.tokens.component.GlobalHeader.Search.Background, + borderColor: + theme.tokens.component.GlobalHeader.Search.Border + .Hover, + color: + theme.tokens.component.GlobalHeader.Search.Text + .Filled, }, - backgroundColor: theme.tokens.component.GlobalHeader.Search.Background, - borderColor: theme.tokens.component.GlobalHeader.Search.Border.Default, - color: theme.tokens.component.GlobalHeader.Search.Text.Filled, + backgroundColor: + theme.tokens.component.GlobalHeader.Search.Background, + borderColor: + theme.tokens.component.GlobalHeader.Search.Border + .Default, + color: + theme.tokens.component.GlobalHeader.Search.Text.Filled, maxWidth: '100%', }, }, diff --git a/packages/manager/src/mocks/types.ts b/packages/manager/src/mocks/types.ts index 4b5c1050b63..9a63ace5b05 100644 --- a/packages/manager/src/mocks/types.ts +++ b/packages/manager/src/mocks/types.ts @@ -126,7 +126,7 @@ export interface MockState { firewalls: Firewall[]; ipAddresses: IPAddress[]; linodeConfigs: [number, Config][]; - linodeInterfaces: [number, LinodeInterface][], + linodeInterfaces: [number, LinodeInterface][]; linodes: Linode[]; notificationQueue: Notification[]; placementGroups: PlacementGroup[]; diff --git a/packages/queries/src/firewalls/firewalls.ts b/packages/queries/src/firewalls/firewalls.ts index b6f1b0dd58d..0e7b89a8247 100644 --- a/packages/queries/src/firewalls/firewalls.ts +++ b/packages/queries/src/firewalls/firewalls.ts @@ -42,7 +42,7 @@ import type { ResourcePage, UpdateFirewallRules, FirewallSettings, - UpdateFirewallSettings + UpdateFirewallSettings, } from '@linode/api-v4'; const getAllFirewallDevices = ( diff --git a/packages/queries/src/linodes/linodes.ts b/packages/queries/src/linodes/linodes.ts index 5f3309d9dd6..3b254020c5f 100644 --- a/packages/queries/src/linodes/linodes.ts +++ b/packages/queries/src/linodes/linodes.ts @@ -335,6 +335,20 @@ export const useCreateLinodeMutation = () => { queryKey: vpcQueries.vpc(vpcId).queryKey, }); } + } else { + // invalidate firewall queries if a new Linode interface is assigned to a firewall + if (variables.interfaces?.some((iface) => iface.firewall_id)) { + queryClient.invalidateQueries({ + queryKey: firewallQueries.firewalls.queryKey, + }); + } + for (const iface of variables.interfaces ?? []) { + if (iface.firewall_id) { + queryClient.invalidateQueries({ + queryKey: firewallQueries.firewall(iface.firewall_id).queryKey, + }); + } + } } // If the Linode is assigned to a placement group on creation, From 6dbce749c3dd69f8f8e89356e51709967a45c464 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:41:10 -0400 Subject: [PATCH 13/84] fix: [M3-9607] - Tabs keyboard navigation on Tanstack rerouted features (#11894) * initial commit - save work * Finalize fix * cleanup * Add component test * fix units * Added changeset: Tabs keyboard navigation on some Tanstack rerouted features --- .../pr-11894-fixed-1742495460894.md | 5 + .../component/components/tabs.spec.tsx | 155 ++++++++++++++++++ .../cypress/support/component/setup.tsx | 10 +- .../cypress/support/util/components.ts | 27 ++- packages/manager/package.json | 2 +- .../src/components/Tabs/SafeTabPanel.test.tsx | 28 ++-- .../manager/src/components/Tabs/Tab.test.tsx | 21 ++- .../Alerts/AlertsLanding/AlertsLanding.tsx | 6 +- .../Firewalls/FirewallDetail/index.tsx | 55 ++++--- .../IntegrationsTabPanel.test.tsx | 19 ++- .../ApiAwarenessModal/SDKTabPanel.test.tsx | 19 ++- .../NodeBalancerDetail/NodeBalancerDetail.tsx | 34 ++-- packages/manager/src/hooks/useTabs.ts | 16 +- pnpm-lock.yaml | 83 +++++----- 14 files changed, 354 insertions(+), 126 deletions(-) create mode 100644 packages/manager/.changeset/pr-11894-fixed-1742495460894.md create mode 100644 packages/manager/cypress/component/components/tabs.spec.tsx diff --git a/packages/manager/.changeset/pr-11894-fixed-1742495460894.md b/packages/manager/.changeset/pr-11894-fixed-1742495460894.md new file mode 100644 index 00000000000..da30bf01036 --- /dev/null +++ b/packages/manager/.changeset/pr-11894-fixed-1742495460894.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Tabs keyboard navigation on some Tanstack rerouted features ([#11894](https://github.com/linode/manager/pull/11894)) diff --git a/packages/manager/cypress/component/components/tabs.spec.tsx b/packages/manager/cypress/component/components/tabs.spec.tsx new file mode 100644 index 00000000000..130b8072555 --- /dev/null +++ b/packages/manager/cypress/component/components/tabs.spec.tsx @@ -0,0 +1,155 @@ +import { createRoute } from '@tanstack/react-router'; +import * as React from 'react'; +import { ui } from 'support/ui'; +import { checkComponentA11y } from 'support/util/accessibility'; +import { componentTests, visualTests } from 'support/util/components'; + +import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { TanStackTabLinkList } from 'src/components/Tabs/TanStackTabLinkList'; +import { useTabs } from 'src/hooks/useTabs'; + +const CustomTabs = () => { + const { handleTabChange, tabIndex, tabs } = useTabs([ + { + title: 'Tab 1', + to: '/tab-1', + }, + { + title: 'Tab 2', + to: '/tab-2', + }, + { + title: 'Tab 3', + to: '/tab-3', + }, + ]); + + return ( + + + }> + + +
Tab 1 content
+ + +
Tab 2 content
+
+ +
Tab 3 content
+
+ + + + ); +}; + +componentTests( + 'Tabs', + (mount) => { + describe('Tabs', () => { + it('should render all tabs and default to the first tab', () => { + mount(); + ui.tabList + .findTabByTitle('Tab 1') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + ui.tabList + .findTabByTitle('Tab 2') + .should('exist') + .should('have.attr', 'aria-selected', 'false'); + ui.tabList + .findTabByTitle('Tab 3') + .should('exist') + .should('have.attr', 'aria-selected', 'false'); + + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 1 content'); + }); + + it('should render the correct tab content when a tab is clicked', () => { + mount(); + + ui.tabList.findTabByTitle('Tab 2').click(); + ui.tabList + .findTabByTitle('Tab 2') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); + + ui.tabList.findTabByTitle('Tab 3').click(); + ui.tabList + .findTabByTitle('Tab 3') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 3 content'); + }); + + it('should handle keyboard navigation', () => { + mount(); + + ui.tabList.findTabByTitle('Tab 1').focus(); + cy.get('body').type('{rightArrow}'); + ui.tabList + .findTabByTitle('Tab 2') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); + + cy.get('body').type('{rightArrow}'); + ui.tabList + .findTabByTitle('Tab 3') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 3 content'); + + cy.get('body').type('{leftArrow}'); + ui.tabList + .findTabByTitle('Tab 2') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 2 content'); + + cy.get('body').type('{leftArrow}'); + ui.tabList + .findTabByTitle('Tab 1') + .should('exist') + .should('have.attr', 'aria-selected', 'true'); + cy.get('[data-reach-tab-panels]').should('have.text', 'Tab 1 content'); + }); + }); + }, + { + routeTree: (parentRoute) => [ + createRoute({ + getParentRoute: () => parentRoute, + path: '/tab-1', + }), + createRoute({ + getParentRoute: () => parentRoute, + path: '/tab-2', + }), + createRoute({ + getParentRoute: () => parentRoute, + path: '/tab-3', + }), + ], + useTanstackRouter: true, + } +); + +visualTests( + (mount) => { + describe('Accessibility checks', () => { + it('passes aXe check when menu is closed without an item selected', () => { + mount(); + checkComponentA11y(); + }); + }); + }, + { + useTanstackRouter: true, + } +); diff --git a/packages/manager/cypress/support/component/setup.tsx b/packages/manager/cypress/support/component/setup.tsx index 1fc6e7ece96..87fdadf9db1 100644 --- a/packages/manager/cypress/support/component/setup.tsx +++ b/packages/manager/cypress/support/component/setup.tsx @@ -35,7 +35,7 @@ import { LinodeThemeWrapper } from 'src/LinodeThemeWrapper'; import { storeFactory } from 'src/store'; import type { ThemeName } from '@linode/ui'; -import type { AnyRouter } from '@tanstack/react-router'; +import type { AnyRoute, AnyRouter } from '@tanstack/react-router'; import type { Flags } from 'src/featureFlags'; /** @@ -48,7 +48,8 @@ export const mountWithTheme = ( jsx: React.ReactNode, theme: ThemeName = 'light', flags: Partial = {}, - useTanstackRouter: boolean = false + useTanstackRouter: boolean = false, + routeTree?: (parentRoute: AnyRoute) => AnyRoute[] ) => { const queryClient = queryClientFactory(); const store = storeFactory(); @@ -59,10 +60,13 @@ export const mountWithTheme = ( path: '/', }); const router: AnyRouter = createRouter({ + defaultNotFoundComponent: () =>
Not Found
, history: createMemoryHistory({ initialEntries: ['/'], }), - routeTree: rootRoute.addChildren([indexRoute]), + routeTree: routeTree + ? rootRoute.addChildren([indexRoute, ...routeTree(indexRoute)]) + : rootRoute.addChildren([indexRoute]), }); return mount( diff --git a/packages/manager/cypress/support/util/components.ts b/packages/manager/cypress/support/util/components.ts index 7eca479968c..607c3c019c3 100644 --- a/packages/manager/cypress/support/util/components.ts +++ b/packages/manager/cypress/support/util/components.ts @@ -3,9 +3,9 @@ */ import type { ThemeName } from '@linode/ui'; +import type { AnyRoute } from '@tanstack/react-router'; import type { MountReturn } from 'cypress/react'; import type { Flags } from 'src/featureFlags'; - /** * Array of themes for which to test components. */ @@ -49,11 +49,18 @@ export const componentTests = ( componentName: string, callback: (mountCommand: MountCommand) => void, options: { + routeTree?: (parentRoute: AnyRoute) => AnyRoute[]; useTanstackRouter?: boolean; } = {} ) => { const mountCommand = (jsx: React.ReactNode, flags?: Flags) => - cy.mountWithTheme(jsx, defaultTheme, flags, options.useTanstackRouter); + cy.mountWithTheme( + jsx, + defaultTheme, + flags, + options.useTanstackRouter, + options.routeTree + ); describe(`${componentName} component tests`, () => { callback(mountCommand); }); @@ -71,11 +78,23 @@ export const componentTests = ( * * @param callback - Test scope callback. */ -export const visualTests = (callback: (mountCommand: MountCommand) => void) => { +export const visualTests = ( + callback: (mountCommand: MountCommand) => void, + options: { + routeTree?: (parentRoute: AnyRoute) => AnyRoute[]; + useTanstackRouter?: boolean; + } = {} +) => { describe('Visual tests', () => { componentThemes.forEach((themeName: ThemeName) => { const mountCommand = (jsx: React.ReactNode, flags?: any) => - cy.mountWithTheme(jsx, themeName, flags); + cy.mountWithTheme( + jsx, + themeName, + flags, + options.useTanstackRouter, + options.routeTree + ); describe(`${capitalize(themeName)} theme`, () => { callback(mountCommand); }); diff --git a/packages/manager/package.json b/packages/manager/package.json index ce419ed307f..a1e1cf1beeb 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -36,7 +36,7 @@ "@mui/utils": "^6.4.3", "@mui/x-date-pickers": "^7.27.0", "@paypal/react-paypal-js": "^7.8.3", - "@reach/tabs": "^0.10.5", + "@reach/tabs": "^0.18.0", "@sentry/react": "^7.119.1", "@shikijs/langs": "^3.1.0", "@shikijs/themes": "^3.1.0", diff --git a/packages/manager/src/components/Tabs/SafeTabPanel.test.tsx b/packages/manager/src/components/Tabs/SafeTabPanel.test.tsx index f884961b017..74dc8bc275a 100644 --- a/packages/manager/src/components/Tabs/SafeTabPanel.test.tsx +++ b/packages/manager/src/components/Tabs/SafeTabPanel.test.tsx @@ -1,6 +1,8 @@ import { render } from '@testing-library/react'; import * as React from 'react'; +import { Tabs } from 'src/components/Tabs/Tabs'; + import { SafeTabPanel } from './SafeTabPanel'; vi.mock('@reach/tabs', async () => { @@ -14,9 +16,11 @@ vi.mock('@reach/tabs', async () => { describe('SafeTabPanel', () => { it('renders children when the tab is selected', () => { const { getByText } = render( - -
Child Content
-
+ + +
Child Content
+
+
); expect(getByText('Child Content')).toBeInTheDocument(); @@ -24,9 +28,11 @@ describe('SafeTabPanel', () => { it('does not render children when the tab is not selected', () => { const { queryByText } = render( - -
Child Content
-
+ + +
Child Content
+
+
); expect(queryByText('Child Content')).toBeNull(); @@ -34,11 +40,13 @@ describe('SafeTabPanel', () => { it('renders empty when the index is null', () => { const { container } = render( - -
Child Content
-
+ + +
Child Content
+
+
); - expect(container.firstChild).toBeEmptyDOMElement(); + expect(container.firstChild?.firstChild).toBeEmptyDOMElement(); }); }); diff --git a/packages/manager/src/components/Tabs/Tab.test.tsx b/packages/manager/src/components/Tabs/Tab.test.tsx index acbeeb938ac..a8e4ce9300b 100644 --- a/packages/manager/src/components/Tabs/Tab.test.tsx +++ b/packages/manager/src/components/Tabs/Tab.test.tsx @@ -1,31 +1,44 @@ import { screen } from '@testing-library/react'; import React from 'react'; +import { Tabs } from 'src/components/Tabs/Tabs'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { Tab } from './Tab'; describe('Tab Component', () => { it('renders tab with children', () => { - renderWithTheme(Hello Tab); + renderWithTheme( + + Hello Tab + + ); const tabElement = screen.getByText('Hello Tab'); expect(tabElement).toBeInTheDocument(); }); it('applies styles correctly', () => { - renderWithTheme(Hello Tab); + renderWithTheme( + + Hello Tab + + ); const tabElement = screen.getByText('Hello Tab'); expect(tabElement).toHaveStyle(` display: inline-flex; - color: rgb(1, 116, 188); + color: rgb(52, 52, 56); `); }); it('handles disabled state', () => { - renderWithTheme(Click Me); + renderWithTheme( + + Click Me + + ); const tabElement = screen.getByText('Click Me'); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx index f1fe04dfae0..fd57fad5f4f 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsLanding/AlertsLanding.tsx @@ -66,11 +66,7 @@ export const AlertsLanding = React.memo(() => { docsLabel="Docs" docsLink="https://techdocs.akamai.com/cloud-computing/docs/akamai-cloud-pulse" /> - + diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 619dceaaba9..28df22916cf 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -26,6 +26,7 @@ import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { checkIfUserCanModifyFirewall } from '../shared'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; const FirewallRulesLanding = React.lazy(() => import('./Rules/FirewallRulesLanding').then((module) => ({ @@ -172,33 +173,35 @@ export const FirewallDetail = () => { {...secureVMFirewallBanner.firewallDetails} /> )} - + - - - - - - - - - - - + }> + + + + + + + + + + + + setIsGenerateDialogOpen(false)} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/IntegrationsTabPanel.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/IntegrationsTabPanel.test.tsx index b62fdd695a5..9e69e382c51 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/IntegrationsTabPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/IntegrationsTabPanel.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; +import { Tabs } from 'src/components/Tabs/Tabs'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { gettingStartedGuides as ansibleResources } from './AnsibleIntegrationResources'; @@ -30,13 +31,21 @@ vi.mock('@reach/tabs', async () => { describe('IntegrationsTabPanel', () => { it('Should render IntegrationsTabPanel', () => { - renderWithTheme(); + renderWithTheme( + + + + ); expect( screen.getByPlaceholderText('Select Integration') ).toBeInTheDocument(); }); it('Should update the state correctly and render relevant resources when Ansible is selected', async () => { - renderWithTheme(); + renderWithTheme( + + + + ); // Check initial value of the Inegrations field expect(screen.getByPlaceholderText('Select Integration')).toHaveValue(''); @@ -69,7 +78,11 @@ describe('IntegrationsTabPanel', () => { }); }); it('Should update the state correctly and render relevant resources when Terraform is selected', async () => { - renderWithTheme(); + renderWithTheme( + + + + ); // Check initial value of the Inegrations field expect(screen.getByPlaceholderText('Select Integration')).toHaveValue(''); diff --git a/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/SDKTabPanel.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/SDKTabPanel.test.tsx index 036a8726bdd..01daab3a62f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/SDKTabPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/ApiAwarenessModal/SDKTabPanel.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; +import { Tabs } from 'src/components/Tabs/Tabs'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { gettingStartedGuides as goResources } from './GoSDKResources'; @@ -30,11 +31,19 @@ vi.mock('@reach/tabs', async () => { describe('SDKTabPanel', () => { it('Should render SDKTabPanel', () => { - renderWithTheme(); + renderWithTheme( + + + + ); expect(screen.getByPlaceholderText('Select An SDK')).toBeInTheDocument(); }); it('Should update the state correctly and render relevant resources when Go is selected', async () => { - renderWithTheme(); + renderWithTheme( + + + + ); // Check initial value of the SDK field expect(screen.getByPlaceholderText('Select An SDK')).toHaveValue(''); @@ -67,7 +76,11 @@ describe('SDKTabPanel', () => { }); }); it('Should update the state correctly and render relevant resources when Python is selected', async () => { - renderWithTheme(); + renderWithTheme( + + + + ); // Check initial value of the SDK field expect(screen.getByPlaceholderText('Select An SDK')).toHaveValue(''); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx index 3a5564785d0..2d6c882e4b4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerDetail.tsx @@ -8,6 +8,7 @@ import { useMatch, useParams } from '@tanstack/react-router'; import * as React from 'react'; import { LandingHeader } from 'src/components/LandingHeader'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; @@ -120,22 +121,23 @@ export const NodeBalancerDetail = () => { )} - - - - - - - - - - - - + }> + + + + + + + + + + + + ); diff --git a/packages/manager/src/hooks/useTabs.ts b/packages/manager/src/hooks/useTabs.ts index 52d5e5f0b3b..c97821a3e04 100644 --- a/packages/manager/src/hooks/useTabs.ts +++ b/packages/manager/src/hooks/useTabs.ts @@ -1,4 +1,4 @@ -import { useMatchRoute } from '@tanstack/react-router'; +import { useMatchRoute, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; import type { LinkProps } from '@tanstack/react-router'; @@ -35,6 +35,7 @@ export interface Tab { * since Reach Tabs maintains its own index state. */ export function useTabs(tabs: T[]) { + const navigate = useNavigate(); const matchRoute = useMatchRoute(); // Filter out hidden tabs @@ -55,10 +56,15 @@ export function useTabs(tabs: T[]) { return index === -1 ? 0 : index; }, [visibleTabs, matchRoute]); - // Simple handler to satisfy Reach Tabs props - const handleTabChange = React.useCallback(() => { - // No-op - navigation is handled by Tanstack Router `Link` - }, []); + const handleTabChange = React.useCallback( + (index: number) => { + const tab = visibleTabs[index]; + if (tab) { + navigate({ to: tab.to }); + } + }, + [visibleTabs, navigate] + ); return { handleTabChange, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1e7aa49326..6d0df829697 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,8 +137,8 @@ importers: specifier: ^7.8.3 version: 7.8.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@reach/tabs': - specifier: ^0.10.5 - version: 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^0.18.0 + version: 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@sentry/react': specifier: ^7.119.1 version: 7.120.0(react@18.3.1) @@ -1849,29 +1849,34 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - '@reach/auto-id@0.10.5': - resolution: {integrity: sha512-we4/bwjFxJ3F+2eaddQ1HltbKvJ7AB8clkN719El7Zugpn/vOjfPMOVUiBqTmPGLUvkYrq4tpuFwLvk2HyOVHg==} + '@reach/auto-id@0.18.0': + resolution: {integrity: sha512-XwY1IwhM7mkHZFghhjiqjQ6dstbOdpbFLdggeke75u8/8icT8uEHLbovFUgzKjy9qPvYwZIB87rLiR8WdtOXCg==} + peerDependencies: + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x + + '@reach/descendants@0.18.0': + resolution: {integrity: sha512-GXUxnM6CfrX5URdnipPIl3Tlc6geuz4xb4n61y4tVWXQX1278Ra9Jz9DMRN8x4wheHAysvrYwnR/SzAlxQzwtA==} peerDependencies: - react: ^16.8.0 - react-dom: ^16.8.0 + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x - '@reach/descendants@0.10.5': - resolution: {integrity: sha512-8HhN4DwS/HsPQ+Ym/Ft/XJ1spXBYdE8hqpnbYR9UcU7Nx3oDbTIdhjA6JXXt23t5avYIx2jRa8YHCtVKSHuiwA==} + '@reach/polymorphic@0.18.0': + resolution: {integrity: sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA==} peerDependencies: - react: ^16.8.0 - react-dom: ^16.8.0 + react: ^16.8.0 || 17.x - '@reach/tabs@0.10.5': - resolution: {integrity: sha512-oQJxQ9FwFsXo2HxEzJxFU/wP31bPVh4VU54NlhHW9f49uofyYkIKBbAhdF0Zb3TnaFp4cGKPHX39pXBYGPDkAQ==} + '@reach/tabs@0.18.0': + resolution: {integrity: sha512-gTRJzStWJJtgMhn9FDEmKogAJMcqNaGZx0i1SGoTdVM+D29DBhVeRdO8qEg+I2l2k32DkmuZxG/Mrh+GZTjczQ==} peerDependencies: - react: ^16.8.0 - react-dom: ^16.8.0 + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x - '@reach/utils@0.10.5': - resolution: {integrity: sha512-5E/xxQnUbmpI/LrufBAOXjunl96DnqX6B4zC2MO2KH/dRzLug5gM5VuOwV26egsp0jvsSPxojwciOhS43px3qw==} + '@reach/utils@0.18.0': + resolution: {integrity: sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==} peerDependencies: - react: ^16.8.0 - react-dom: ^16.8.0 + react: ^16.8.0 || 17.x + react-dom: ^16.8.0 || 17.x '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} @@ -2644,9 +2649,6 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/warning@3.0.3': - resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} - '@types/xml2js@0.4.14': resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==} @@ -6594,9 +6596,6 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} - warning@4.0.3: - resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -7735,37 +7734,35 @@ snapshots: '@popperjs/core@2.11.8': {} - '@reach/auto-id@0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reach/auto-id@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reach/utils': 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reach/utils': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - '@reach/descendants@0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reach/descendants@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@reach/utils': 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reach/utils': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - '@reach/tabs@0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reach/polymorphic@0.18.0(react@18.3.1)': dependencies: - '@reach/auto-id': 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reach/descendants': 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reach/utils': 0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - prop-types: 15.8.1 + react: 18.3.1 + + '@reach/tabs@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@reach/auto-id': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reach/descendants': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reach/polymorphic': 0.18.0(react@18.3.1) + '@reach/utils': 0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - '@reach/utils@0.10.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reach/utils@0.18.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@types/warning': 3.0.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.8.1 - warning: 4.0.3 '@rollup/pluginutils@5.1.3(rollup@4.34.8)': dependencies: @@ -8573,8 +8570,6 @@ snapshots: '@types/uuid@9.0.8': {} - '@types/warning@3.0.3': {} - '@types/xml2js@0.4.14': dependencies: '@types/node': 20.17.6 @@ -13198,10 +13193,6 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - warning@4.0.3: - dependencies: - loose-envify: 1.4.0 - webidl-conversions@3.0.1: {} webidl-conversions@4.0.2: {} From c0bfd30efdb094d051151d5dc5e1ec2729aa99ad Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Mon, 24 Mar 2025 17:00:17 -0400 Subject: [PATCH 14/84] upcoming: [M3-9539] - Quotas Tab Beta Chip (#11872) * save progress * Add proper colors for secondary styles * fix test * Delete old instance of BetaChip * Changesets --- .../pr-11872-removed-1742318958886.md | 5 ++ ...r-11872-upcoming-features-1742318903725.md | 5 ++ .../component/components/beta-chip.spec.tsx | 7 +-- .../components/BetaChip/BetaChip.stories.tsx | 17 ----- .../src/components/BetaChip/BetaChip.test.tsx | 32 ---------- .../src/components/BetaChip/BetaChip.tsx | 63 ------------------- .../src/features/Account/AccountLanding.tsx | 2 + .../Databases/DatabaseDetail/index.tsx | 7 ++- .../pr-11872-fixed-1742318981340.md | 5 ++ .../src/components/BetaChip/BetaChip.test.tsx | 8 +-- .../ui/src/components/BetaChip/BetaChip.tsx | 18 ++++-- 11 files changed, 41 insertions(+), 128 deletions(-) create mode 100644 packages/manager/.changeset/pr-11872-removed-1742318958886.md create mode 100644 packages/manager/.changeset/pr-11872-upcoming-features-1742318903725.md delete mode 100644 packages/manager/src/components/BetaChip/BetaChip.stories.tsx delete mode 100644 packages/manager/src/components/BetaChip/BetaChip.test.tsx delete mode 100644 packages/manager/src/components/BetaChip/BetaChip.tsx create mode 100644 packages/ui/.changeset/pr-11872-fixed-1742318981340.md diff --git a/packages/manager/.changeset/pr-11872-removed-1742318958886.md b/packages/manager/.changeset/pr-11872-removed-1742318958886.md new file mode 100644 index 00000000000..125e2b652b6 --- /dev/null +++ b/packages/manager/.changeset/pr-11872-removed-1742318958886.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Legacy BetaChip component ([#11872](https://github.com/linode/manager/pull/11872)) diff --git a/packages/manager/.changeset/pr-11872-upcoming-features-1742318903725.md b/packages/manager/.changeset/pr-11872-upcoming-features-1742318903725.md new file mode 100644 index 00000000000..92e160929b8 --- /dev/null +++ b/packages/manager/.changeset/pr-11872-upcoming-features-1742318903725.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Quotas Tab Beta Chip ([#11872](https://github.com/linode/manager/pull/11872)) diff --git a/packages/manager/cypress/component/components/beta-chip.spec.tsx b/packages/manager/cypress/component/components/beta-chip.spec.tsx index 58641a6e63b..b05c7b501da 100644 --- a/packages/manager/cypress/component/components/beta-chip.spec.tsx +++ b/packages/manager/cypress/component/components/beta-chip.spec.tsx @@ -1,9 +1,8 @@ +import { BetaChip } from '@linode/ui'; import * as React from 'react'; import { checkComponentA11y } from 'support/util/accessibility'; import { componentTests, visualTests } from 'support/util/components'; -import { BetaChip } from 'src/components/BetaChip/BetaChip'; - componentTests('BetaChip', () => { visualTests((mount) => { it('renders "BETA" text indicator with primary color', () => { @@ -12,7 +11,7 @@ componentTests('BetaChip', () => { }); it('renders "BETA" text indicator with default color', () => { - mount(); + mount(); cy.findByText('beta').should('be.visible'); }); @@ -22,7 +21,7 @@ componentTests('BetaChip', () => { }); it('passes aXe check with default color', () => { - mount(); + mount(); checkComponentA11y(); }); }); diff --git a/packages/manager/src/components/BetaChip/BetaChip.stories.tsx b/packages/manager/src/components/BetaChip/BetaChip.stories.tsx deleted file mode 100644 index 3cc3a4ea934..00000000000 --- a/packages/manager/src/components/BetaChip/BetaChip.stories.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -import { BetaChip } from './BetaChip'; - -import type { BetaChipProps } from './BetaChip'; -import type { Meta, StoryObj } from '@storybook/react'; - -export const Default: StoryObj = { - render: (args) => , -}; - -const meta: Meta = { - args: { color: 'default' }, - component: BetaChip, - title: 'Foundations/Chip/BetaChip', -}; -export default meta; diff --git a/packages/manager/src/components/BetaChip/BetaChip.test.tsx b/packages/manager/src/components/BetaChip/BetaChip.test.tsx deleted file mode 100644 index 39d28178640..00000000000 --- a/packages/manager/src/components/BetaChip/BetaChip.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { fireEvent } from '@testing-library/react'; -import React from 'react'; - -import { renderWithTheme } from 'src/utilities/testHelpers'; - -import { BetaChip } from './BetaChip'; - -describe('BetaChip', () => { - it('renders with default color', () => { - const { getByTestId } = renderWithTheme(); - const betaChip = getByTestId('betaChip'); - expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: rgba(0, 0, 0, 0.08)'); - }); - - it('renders with primary color', () => { - const { getByTestId } = renderWithTheme(); - const betaChip = getByTestId('betaChip'); - expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: rgb(16, 138, 214)'); - }); - - it('triggers an onClick callback', () => { - const onClickMock = vi.fn(); - const { getByTestId } = renderWithTheme( - - ); - const betaChip = getByTestId('betaChip'); - fireEvent.click(betaChip); - expect(onClickMock).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/manager/src/components/BetaChip/BetaChip.tsx b/packages/manager/src/components/BetaChip/BetaChip.tsx deleted file mode 100644 index d5d7f589133..00000000000 --- a/packages/manager/src/components/BetaChip/BetaChip.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Chip } from '@linode/ui'; -import { styled } from '@mui/material/styles'; -import * as React from 'react'; - -import type { ChipProps } from '@linode/ui'; - -export interface BetaChipProps - extends Omit< - ChipProps, - | 'avatar' - | 'clickable' - | 'deleteIcon' - | 'disabled' - | 'icon' - | 'label' - | 'onDelete' - | 'outlineColor' - | 'size' - | 'variant' - > { - /** - * The color of the chip. - * default renders a gray chip, primary renders a blue chip. - */ - color?: 'default' | 'primary'; -} - -/** - * ## Usage - * - * Beta chips label features that are not yet part of Cloud Manager's core supported functionality.
- * **Example:** A beta chip may appear in the [primary navigation](https://github.com/linode/manager/pull/8104#issuecomment-1309334374), - * breadcrumbs, [banners](/docs/components-notifications-dismissible-banners--beta-banners), tabs, and/or plain text to designate beta functionality.
- * **Visual style:** bold, capitalized text; reduced height, letter spacing, and font size; solid color background. - * - */ -export const BetaChip = (props: BetaChipProps) => { - const { color } = props; - - return ( - - ); -}; - -const StyledBetaChip = styled(Chip, { - label: 'StyledBetaChip', -})(({ theme }) => ({ - '& .MuiChip-label': { - padding: 0, - }, - font: theme.font.bold, - fontSize: '0.625rem', - height: 16, - letterSpacing: '.25px', - marginLeft: theme.spacing(), - padding: theme.spacing(0.5), - textTransform: 'uppercase', -})); diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index b011cd3dcae..a047349a024 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -1,4 +1,5 @@ import { useAccount, useProfile } from '@linode/queries'; +import { BetaChip } from '@linode/ui'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; @@ -89,6 +90,7 @@ const AccountLanding = () => { ...(showQuotasTab ? [ { + chip: , routeName: '/account/quotas', title: 'Quotas', }, diff --git a/packages/manager/src/features/Databases/DatabaseDetail/index.tsx b/packages/manager/src/features/Databases/DatabaseDetail/index.tsx index af844c1e1c6..c07b8bc8684 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/index.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/index.tsx @@ -1,10 +1,9 @@ -import { CircleProgress, ErrorState, Notice } from '@linode/ui'; +import { BetaChip, CircleProgress, ErrorState, Notice } from '@linode/ui'; import { useEditableLabelState } from '@linode/utilities'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { matchPath, useHistory, useParams } from 'react-router-dom'; -import { BetaChip } from 'src/components/BetaChip/BetaChip'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; @@ -115,7 +114,9 @@ export const DatabaseDetail = () => { if (isMonitorEnabled) { tabs.splice(1, 0, { - chip: flags.dbaasV2MonitorMetrics?.beta ? : null, + chip: flags.dbaasV2MonitorMetrics?.beta ? ( + + ) : null, routeName: `/databases/${engine}/${id}/monitor`, title: 'Monitor', }); diff --git a/packages/ui/.changeset/pr-11872-fixed-1742318981340.md b/packages/ui/.changeset/pr-11872-fixed-1742318981340.md new file mode 100644 index 00000000000..d64d3f64d4b --- /dev/null +++ b/packages/ui/.changeset/pr-11872-fixed-1742318981340.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Fixed +--- + +BetaChip `color` prop ([#11872](https://github.com/linode/manager/pull/11872)) diff --git a/packages/ui/src/components/BetaChip/BetaChip.test.tsx b/packages/ui/src/components/BetaChip/BetaChip.test.tsx index ebd06322c14..b9300601a24 100644 --- a/packages/ui/src/components/BetaChip/BetaChip.test.tsx +++ b/packages/ui/src/components/BetaChip/BetaChip.test.tsx @@ -7,7 +7,7 @@ import { renderWithTheme } from '../../utilities/testHelpers'; import { BetaChip } from './BetaChip'; describe('BetaChip', () => { - it('renders with default color', () => { + it('renders with default color (primary)', () => { const { getByTestId } = renderWithTheme(); const betaChip = getByTestId('betaChip'); expect(betaChip).toBeInTheDocument(); @@ -15,16 +15,16 @@ describe('BetaChip', () => { }); it('renders with primary color', () => { - const { getByTestId } = renderWithTheme(); + const { getByTestId } = renderWithTheme(); const betaChip = getByTestId('betaChip'); expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: rgb(16, 138, 214)'); + expect(betaChip).toHaveStyle('background-color: rgb(131, 131, 140)'); }); it('triggers an onClick callback', () => { const onClickMock = vi.fn(); const { getByTestId } = renderWithTheme( - + ); const betaChip = getByTestId('betaChip'); fireEvent.click(betaChip); diff --git a/packages/ui/src/components/BetaChip/BetaChip.tsx b/packages/ui/src/components/BetaChip/BetaChip.tsx index 33b67e12711..a5c870b99b1 100644 --- a/packages/ui/src/components/BetaChip/BetaChip.tsx +++ b/packages/ui/src/components/BetaChip/BetaChip.tsx @@ -23,7 +23,7 @@ export interface BetaChipProps * The color of the chip. * default renders a gray chip, primary renders a blue chip. */ - color?: 'default' | 'primary'; + color?: 'primary' | 'secondary'; } /** @@ -36,7 +36,7 @@ export interface BetaChipProps * */ export const BetaChip = (props: BetaChipProps) => { - const { color } = props; + const { color = 'primary' } = props; return ( { const StyledBetaChip = styled(Chip, { label: 'StyledBetaChip', -})(({ theme }) => ({ + shouldForwardProp: (prop) => prop !== 'color', +})(({ color, theme }) => ({ '& .MuiChip-label': { padding: 0, }, - background: 'lch(77.7 28.7 275 / 0.12)', - color: theme.tokens.color.Ultramarine[50], + background: + color === 'primary' + ? 'lch(77.7 28.7 275 / 0.12)' + : theme.tokens.color.Neutrals[60], + color: + color === 'primary' + ? theme.tokens.color.Ultramarine[50] + : theme.tokens.color.Neutrals.White, + font: theme.font.bold, fontSize: '0.625rem', height: 16, From 25d1079ebafae90bf85964e01153bfa7588509c2 Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Tue, 25 Mar 2025 11:56:58 +0530 Subject: [PATCH 15/84] refactor: [M3-9545] - Add `luxon` dependency and move related utils (#11905) * Add luxon dep and move luxon dependent utils * Added changeset: Move `luxon` dependent utils from `manager` to `utilities` package * Added changeset: Add `luxon` dependency and move related utils from `manager` to `utilities` package --- .../pr-11905-removed-1742540403425.md | 5 +++++ packages/manager/src/features/Events/utils.tsx | 2 +- .../LinodeBackup/BackupTableRow.tsx | 2 +- .../LinodeBackup/ScheduleSettings.tsx | 12 ++++++------ .../DetailTabs/Apache/Apache.tsx | 2 +- .../LongviewDetail/DetailTabs/Disks/Graphs.tsx | 2 +- .../LongviewDetail/DetailTabs/IconSection.tsx | 3 +-- .../DetailTabs/MySQL/MySQLLanding.tsx | 2 +- .../LongviewDetail/DetailTabs/NGINX/NGINX.tsx | 2 +- .../DetailTabs/Network/NetworkLanding.tsx | 12 ++++++------ .../OverviewGraphs/OverviewGraphs.tsx | 3 +-- .../DetailTabs/Processes/ProcessesLanding.tsx | 2 +- .../LongviewLanding/LongviewClientHeader.tsx | 2 +- .../.changeset/pr-11905-added-1742540619261.md | 5 +++++ packages/utilities/package.json | 8 +++++--- .../src/helpers}/formatDuration.test.ts | 2 ++ .../src/helpers}/formatDuration.ts | 0 .../src/helpers}/formatUptime.test.ts | 2 ++ .../src/helpers}/formatUptime.ts | 1 + packages/utilities/src/helpers/index.ts | 4 ++++ .../src/helpers}/initWindows.test.ts | 2 ++ .../src/helpers}/initWindows.ts | 0 .../src/helpers}/isToday.test.ts | 2 ++ .../src/helpers}/isToday.ts | 0 pnpm-lock.yaml | 18 ++++++++++++------ 25 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 packages/manager/.changeset/pr-11905-removed-1742540403425.md create mode 100644 packages/utilities/.changeset/pr-11905-added-1742540619261.md rename packages/{manager/src/utilities => utilities/src/helpers}/formatDuration.test.ts (96%) rename packages/{manager/src/utilities => utilities/src/helpers}/formatDuration.ts (100%) rename packages/{manager/src/utilities => utilities/src/helpers}/formatUptime.test.ts (96%) rename packages/{manager/src/utilities => utilities/src/helpers}/formatUptime.ts (99%) rename packages/{manager/src/utilities => utilities/src/helpers}/initWindows.test.ts (93%) rename packages/{manager/src/utilities => utilities/src/helpers}/initWindows.ts (100%) rename packages/{manager/src/utilities => utilities/src/helpers}/isToday.test.ts (95%) rename packages/{manager/src/utilities => utilities/src/helpers}/isToday.ts (100%) diff --git a/packages/manager/.changeset/pr-11905-removed-1742540403425.md b/packages/manager/.changeset/pr-11905-removed-1742540403425.md new file mode 100644 index 00000000000..b12c6f56164 --- /dev/null +++ b/packages/manager/.changeset/pr-11905-removed-1742540403425.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `luxon` dependent utils from `manager` to `utilities` package ([#11905](https://github.com/linode/manager/pull/11905)) diff --git a/packages/manager/src/features/Events/utils.tsx b/packages/manager/src/features/Events/utils.tsx index e3390b1fc42..7a35a17048f 100644 --- a/packages/manager/src/features/Events/utils.tsx +++ b/packages/manager/src/features/Events/utils.tsx @@ -1,9 +1,9 @@ +import { formatDuration } from '@linode/utilities'; import { Duration } from 'luxon'; import { ACTIONS_TO_INCLUDE_AS_PROGRESS_EVENTS } from 'src/features/Events/constants'; import { isInProgressEvent } from 'src/queries/events/event.helpers'; import { parseAPIDate } from 'src/utilities/date'; -import { formatDuration } from 'src/utilities/formatDuration'; import { ACTIONS_WITHOUT_USERNAMES } from './constants'; import { eventMessages } from './factory'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx index 57a24cbc351..321b0bfc82c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/BackupTableRow.tsx @@ -1,3 +1,4 @@ +import { formatDuration } from '@linode/utilities'; import { DateTime, Duration } from 'luxon'; import * as React from 'react'; @@ -6,7 +7,6 @@ import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { parseAPIDate } from 'src/utilities/date'; -import { formatDuration } from 'src/utilities/formatDuration'; import { LinodeBackupActionMenu } from './LinodeBackupActionMenu'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx index b1f7f6c4e6a..aeffde9fb56 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.tsx @@ -1,3 +1,8 @@ +import { + useLinodeQuery, + useLinodeUpdateMutation, + useProfile, +} from '@linode/queries'; import { ActionsPanel, Autocomplete, @@ -7,18 +12,13 @@ import { Paper, Typography, } from '@linode/ui'; +import { initWindows } from '@linode/utilities'; import { styled } from '@mui/material/styles'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { - useLinodeQuery, - useLinodeUpdateMutation, - useProfile, -} from '@linode/queries'; import { getUserTimezone } from 'src/utilities/getUserTimezone'; -import { initWindows } from 'src/utilities/initWindows'; interface Props { isReadOnly: boolean; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx index 1daf1246856..2713b560ed6 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Apache/Apache.tsx @@ -1,11 +1,11 @@ import { Box, Notice, Typography } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Link } from 'src/components/Link'; import { TimeRangeSelect } from 'src/features/Longview/shared/TimeRangeSelect'; -import { isToday as _isToday } from 'src/utilities/isToday'; import { StyledTypography } from '../CommonStyles.styles'; import { useGraphs } from '../OverviewGraphs/useGraphs'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Disks/Graphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Disks/Graphs.tsx index 597279b49de..48f6fcf8dc8 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Disks/Graphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Disks/Graphs.tsx @@ -1,9 +1,9 @@ import { Typography } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { LongviewLineGraph } from 'src/components/LongviewLineGraph/LongviewLineGraph'; -import { isToday as _isToday } from 'src/utilities/isToday'; import { convertData } from '../../../shared/formatters'; import GraphCard from '../../GraphCard'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx index c015f97a0c7..9f6fbaf6462 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/IconSection.tsx @@ -1,5 +1,5 @@ import { Box, Stack, Typography } from '@linode/ui'; -import { readableBytes } from '@linode/utilities'; +import { formatUptime, readableBytes } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; @@ -9,7 +9,6 @@ import PackageIcon from 'src/assets/icons/longview/package-icon.svg'; import RamIcon from 'src/assets/icons/longview/ram-sticks.svg'; import ServerIcon from 'src/assets/icons/longview/server-icon.svg'; import { IconTextLink } from 'src/components/IconTextLink/IconTextLink'; -import { formatUptime } from 'src/utilities/formatUptime'; import { getPackageNoticeText, diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx index fd9b403eb9a..cf45330be89 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/MySQL/MySQLLanding.tsx @@ -1,11 +1,11 @@ import { Box, Notice, Typography } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Link } from 'src/components/Link'; import { TimeRangeSelect } from 'src/features/Longview/shared/TimeRangeSelect'; -import { isToday as _isToday } from 'src/utilities/isToday'; import { StyledTypography } from '../CommonStyles.styles'; import { useGraphs } from '../OverviewGraphs/useGraphs'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx index 56683109ec2..8b637126438 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/NGINX/NGINX.tsx @@ -1,11 +1,11 @@ import { Box, Notice, Typography } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { Link } from 'src/components/Link'; import { TimeRangeSelect } from 'src/features/Longview/shared/TimeRangeSelect'; -import { isToday as _isToday } from 'src/utilities/isToday'; import { StyledTypography } from '../CommonStyles.styles'; import { useGraphs } from '../OverviewGraphs/useGraphs'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx index b0c95a7f3e6..58a926f7d38 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Network/NetworkLanding.tsx @@ -1,19 +1,19 @@ +import { isToday as _isToday } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { isToday as _isToday } from 'src/utilities/isToday'; +import { TimeRangeSelect } from 'src/features/Longview/shared/TimeRangeSelect'; -import type { - LongviewNetworkInterface, - WithStartAndEnd, -} from '../../../request.types'; import { StyledBox } from '../Disks/Disks.styles'; import { useGraphs } from '../OverviewGraphs/useGraphs'; import { NetworkGraphs } from './NetworkGraphs'; +import type { + LongviewNetworkInterface, + WithStartAndEnd, +} from '../../../request.types'; import type { APIError } from '@linode/api-v4'; -import { TimeRangeSelect } from 'src/features/Longview/shared/TimeRangeSelect'; interface Props { clientAPIKey: string; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx index a2bf6a51716..9196cd4d419 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/OverviewGraphs/OverviewGraphs.tsx @@ -1,10 +1,9 @@ import { Paper } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; -import { isToday as _isToday } from 'src/utilities/isToday'; - import { TimeRangeSelect } from '../../../shared/TimeRangeSelect'; import { StyledTypography } from '../CommonStyles.styles'; import { CPUGraph } from './CPUGraph'; diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx index c37e4e523f6..5e8ed70fb86 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx @@ -1,11 +1,11 @@ import { TextField } from '@linode/ui'; +import { isToday as _isToday } from '@linode/utilities'; import { escapeRegExp } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { statAverage, statMax } from 'src/features/Longview/shared/utilities'; -import { isToday as _isToday } from 'src/utilities/isToday'; import { useGraphs } from '../OverviewGraphs/useGraphs'; import { ProcessesGraphs } from './ProcessesGraphs'; diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx index 839a63bab8d..2a60e5b9c9b 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewClientHeader.tsx @@ -1,5 +1,6 @@ import { useProfile } from '@linode/queries'; import { Typography } from '@linode/ui'; +import { formatUptime } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { compose } from 'recompose'; @@ -9,7 +10,6 @@ import { Link } from 'src/components/Link'; import withClientStats from 'src/containers/longview.stats.container'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { formatDate } from 'src/utilities/formatDate'; -import { formatUptime } from 'src/utilities/formatUptime'; import { getPackageNoticeText } from '../shared/utilities'; import { diff --git a/packages/utilities/.changeset/pr-11905-added-1742540619261.md b/packages/utilities/.changeset/pr-11905-added-1742540619261.md new file mode 100644 index 00000000000..0b90bf30902 --- /dev/null +++ b/packages/utilities/.changeset/pr-11905-added-1742540619261.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Added +--- + +Add `luxon` dependency and move related utils from `manager` to `utilities` package ([#11905](https://github.com/linode/manager/pull/11905)) diff --git a/packages/utilities/package.json b/packages/utilities/package.json index 0fd577f49d7..f8bf2f1eadc 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -32,7 +32,8 @@ }, "dependencies": { "@linode/api-v4": "workspace:*", - "@types/ramda": "0.25.16", + "luxon": "3.4.4", + "ramda": "~0.25.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -41,6 +42,8 @@ "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "~6.4.2", "@testing-library/react": "~16.0.0", + "@types/luxon": "3.4.2", + "@types/ramda": "0.25.16", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.18", "@typescript-eslint/eslint-plugin": "^6.21.0", @@ -50,7 +53,6 @@ "eslint-plugin-prettier": "~3.3.1", "eslint-plugin-sonarjs": "^0.5.0", "factory.ts": "^0.5.1", - "prettier": "~2.2.1", - "ramda": "~0.25.0" + "prettier": "~2.2.1" } } diff --git a/packages/manager/src/utilities/formatDuration.test.ts b/packages/utilities/src/helpers/formatDuration.test.ts similarity index 96% rename from packages/manager/src/utilities/formatDuration.test.ts rename to packages/utilities/src/helpers/formatDuration.test.ts index 1db03ec14e1..281615d66fb 100644 --- a/packages/manager/src/utilities/formatDuration.test.ts +++ b/packages/utilities/src/helpers/formatDuration.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { Duration } from 'luxon'; import { formatDuration } from './formatDuration'; diff --git a/packages/manager/src/utilities/formatDuration.ts b/packages/utilities/src/helpers/formatDuration.ts similarity index 100% rename from packages/manager/src/utilities/formatDuration.ts rename to packages/utilities/src/helpers/formatDuration.ts diff --git a/packages/manager/src/utilities/formatUptime.test.ts b/packages/utilities/src/helpers/formatUptime.test.ts similarity index 96% rename from packages/manager/src/utilities/formatUptime.test.ts rename to packages/utilities/src/helpers/formatUptime.test.ts index 92ac96042cd..fb2738f4617 100644 --- a/packages/manager/src/utilities/formatUptime.test.ts +++ b/packages/utilities/src/helpers/formatUptime.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { Duration } from 'luxon'; import { formatUptime } from './formatUptime'; diff --git a/packages/manager/src/utilities/formatUptime.ts b/packages/utilities/src/helpers/formatUptime.ts similarity index 99% rename from packages/manager/src/utilities/formatUptime.ts rename to packages/utilities/src/helpers/formatUptime.ts index 064b91a93e8..44c99b207db 100644 --- a/packages/manager/src/utilities/formatUptime.ts +++ b/packages/utilities/src/helpers/formatUptime.ts @@ -1,4 +1,5 @@ import { Duration } from 'luxon'; + export const formatUptime = (uptime: number) => { /** * We get uptime from the Longview API in diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts index adaf31c4581..26a8fb46241 100644 --- a/packages/utilities/src/helpers/index.ts +++ b/packages/utilities/src/helpers/index.ts @@ -10,13 +10,17 @@ export * from './downloadFile'; export * from './env'; export * from './escapeRegExp'; export * from './evenizeNumber'; +export * from './formatDuration'; export * from './formatStorageUnits'; +export * from './formatUptime'; export * from './getAll'; export * from './getIsLegacyInterfaceArray'; export * from './getNewRegionLabel'; export * from './groupByTags'; export * from './getDisplayName'; +export * from './initWindows'; export * from './isNumber'; +export * from './isToday'; export * from './link'; export * from './manuallySetVPCConfigInterfacesToActive'; export * from './metadata'; diff --git a/packages/manager/src/utilities/initWindows.test.ts b/packages/utilities/src/helpers/initWindows.test.ts similarity index 93% rename from packages/manager/src/utilities/initWindows.test.ts rename to packages/utilities/src/helpers/initWindows.test.ts index 880d53b6ad2..2ae6107f286 100644 --- a/packages/manager/src/utilities/initWindows.test.ts +++ b/packages/utilities/src/helpers/initWindows.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { initWindows } from './initWindows'; const timezone1 = 'America/New_York'; diff --git a/packages/manager/src/utilities/initWindows.ts b/packages/utilities/src/helpers/initWindows.ts similarity index 100% rename from packages/manager/src/utilities/initWindows.ts rename to packages/utilities/src/helpers/initWindows.ts diff --git a/packages/manager/src/utilities/isToday.test.ts b/packages/utilities/src/helpers/isToday.test.ts similarity index 95% rename from packages/manager/src/utilities/isToday.test.ts rename to packages/utilities/src/helpers/isToday.test.ts index 3fc32c76bdc..004b052e546 100644 --- a/packages/manager/src/utilities/isToday.test.ts +++ b/packages/utilities/src/helpers/isToday.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { DateTime } from 'luxon'; import { isToday } from './isToday'; diff --git a/packages/manager/src/utilities/isToday.ts b/packages/utilities/src/helpers/isToday.ts similarity index 100% rename from packages/manager/src/utilities/isToday.ts rename to packages/utilities/src/helpers/isToday.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d0df829697..56c8eea321d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -775,9 +775,12 @@ importers: '@linode/api-v4': specifier: workspace:* version: link:../api-v4 - '@types/ramda': - specifier: 0.25.16 - version: 0.25.16 + luxon: + specifier: 3.4.4 + version: 3.4.4 + ramda: + specifier: ~0.25.0 + version: 0.25.0 react: specifier: ^18.2.0 version: 18.3.1 @@ -797,6 +800,12 @@ importers: '@testing-library/react': specifier: ~16.0.0 version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/luxon': + specifier: 3.4.2 + version: 3.4.2 + '@types/ramda': + specifier: 0.25.16 + version: 0.25.16 '@types/react': specifier: ^18.2.55 version: 18.3.12 @@ -827,9 +836,6 @@ importers: prettier: specifier: ~2.2.1 version: 2.2.1 - ramda: - specifier: ~0.25.0 - version: 0.25.0 packages/validation: dependencies: From ca8c956b183ca4cb9728d58ec83239a3d95e6110 Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Tue, 25 Mar 2025 12:46:22 +0530 Subject: [PATCH 16/84] change: [M3-9437] - Akamai Design System: Profile Menu (#11884) * change: [M3-9437] - Akamai Design System: Profile Menu * replace with CDS icon * Added changeset: Update styles to CSD for profile menu * Update changeset description --- .../pr-11884-changed-1742456199053.md | 5 +++ .../manager/src/assets/icons/swapSmall.svg | 4 +-- .../features/Account/SwitchAccountButton.tsx | 12 ++++++- .../TopMenu/UserMenu/UserMenuPopover.tsx | 32 ++++++++++++++----- 4 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 packages/manager/.changeset/pr-11884-changed-1742456199053.md diff --git a/packages/manager/.changeset/pr-11884-changed-1742456199053.md b/packages/manager/.changeset/pr-11884-changed-1742456199053.md new file mode 100644 index 00000000000..6a6603009bf --- /dev/null +++ b/packages/manager/.changeset/pr-11884-changed-1742456199053.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) diff --git a/packages/manager/src/assets/icons/swapSmall.svg b/packages/manager/src/assets/icons/swapSmall.svg index 6711e50df3b..3d69d430869 100644 --- a/packages/manager/src/assets/icons/swapSmall.svg +++ b/packages/manager/src/assets/icons/swapSmall.svg @@ -1,3 +1,3 @@ - - + + diff --git a/packages/manager/src/features/Account/SwitchAccountButton.tsx b/packages/manager/src/features/Account/SwitchAccountButton.tsx index 945e816fedd..e9c2b548e86 100644 --- a/packages/manager/src/features/Account/SwitchAccountButton.tsx +++ b/packages/manager/src/features/Account/SwitchAccountButton.tsx @@ -7,7 +7,17 @@ import type { ButtonProps } from '@linode/ui'; export const SwitchAccountButton = (props: ButtonProps) => { return ( - } {...props}> + ); diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx index ebf1e7c8e57..5dd9b19b4fb 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx @@ -1,8 +1,9 @@ +import { useProfile } from '@linode/queries'; import { Box, Divider, Stack, Typography } from '@linode/ui'; import { styled } from '@mui/material'; +import Grid from '@mui/material/Grid2'; import Popover from '@mui/material/Popover'; import { useTheme } from '@mui/material/styles'; -import Grid from '@mui/material/Grid2'; import React from 'react'; import { Link } from 'src/components/Link'; @@ -11,7 +12,6 @@ import { SwitchAccountButton } from 'src/features/Account/SwitchAccountButton'; import { useIsParentTokenExpired } from 'src/features/Account/SwitchAccounts/useIsParentTokenExpired'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { useFlags } from 'src/hooks/useFlags'; -import { useProfile } from '@linode/queries'; import { sendSwitchAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; import { getStorage } from 'src/utilities/storage'; @@ -128,9 +128,12 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { return ( {link.display} @@ -158,6 +161,7 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { slotProps={{ paper: { sx: (theme) => ({ + backgroundColor: theme.tokens.alias.Background.Normal, paddingX: theme.tokens.spacing.S24, paddingY: theme.tokens.spacing.S16, }), @@ -179,11 +183,19 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { > theme.tokens.spacing.S8}> {canSwitchBetweenParentOrProxyAccount && ( - Current account: + ({ + color: theme.tokens.alias.Content.Text.Primary.Default, + font: theme.tokens.alias.Typography.Label.Semibold.S, + })} + > + Current account: + )} ({ - font: theme.tokens.alias.Typography.Heading.M, + color: theme.tokens.alias.Content.Text.Primary.Default, + font: theme.tokens.alias.Typography.Label.Bold.L, })} > {canSwitchBetweenParentOrProxyAccount && companyNameOrEmail @@ -205,10 +217,10 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { My Profile - + {profileLinks.slice(0, 4).map(renderLink)} - + {profileLinks.slice(4).map(renderLink)} @@ -224,10 +236,13 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { {accountLinks.map((menuLink) => menuLink.hide ? null : ( {menuLink.display} @@ -243,6 +258,7 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { }; const Heading = styled(Typography)(({ theme }) => ({ + color: theme.tokens.alias.Content.Text.Primary.Default, font: theme.tokens.alias.Typography.Heading.Overline, letterSpacing: theme.tokens.alias.Typography.Heading.OverlineLetterSpacing, textTransform: theme.tokens.alias.Typography.Heading.OverlineTextCase, From 491a0cdfdd194b793b9c1517c18d797d37a47b79 Mon Sep 17 00:00:00 2001 From: Harsh Shankar Rao Date: Tue, 25 Mar 2025 13:24:47 +0530 Subject: [PATCH 17/84] upcoming: [M3-9421] - Add API endpoints and types for `/v4/nodebalancers` (#11832) * upcoming: [M3-9421] - Add API endpoints and types for /v4/nodebalancers * Added changeset: Add `/v4beta/nodebalancers` and `/v4/nodebalancers` endpoints for NB-VPC Integration * removed error in configRebuild api call * removed schema change * added beta functions in utils for accomodating subnet_id * corrected createNodeBalancerPayload types to match APISpec * function name change --- ...r-11832-upcoming-features-1741778030625.md | 5 ++ .../src/nodebalancers/nodebalancer-configs.ts | 60 ++++++++++++++++++- .../api-v4/src/nodebalancers/nodebalancers.ts | 50 +++++++++++++++- packages/api-v4/src/nodebalancers/types.ts | 13 +++- packages/api-v4/src/nodebalancers/utils.ts | 25 ++++++++ 5 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11832-upcoming-features-1741778030625.md diff --git a/packages/api-v4/.changeset/pr-11832-upcoming-features-1741778030625.md b/packages/api-v4/.changeset/pr-11832-upcoming-features-1741778030625.md new file mode 100644 index 00000000000..d812f32f564 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11832-upcoming-features-1741778030625.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Add `/v4beta/nodebalancers` and `/v4/nodebalancers` endpoints for NB-VPC Integration ([#11832](https://github.com/linode/manager/pull/11832)) diff --git a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts index a690d580c41..2a2806e0712 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancer-configs.ts @@ -8,9 +8,13 @@ import { ResourcePage as Page, Params } from '../types'; import { CreateNodeBalancerConfig, NodeBalancerConfig, + RebuildNodeBalancerConfig, UpdateNodeBalancerConfig, } from './types'; -import { combineConfigNodeAddressAndPort } from './utils'; +import { + combineConfigNodeAddressAndPort, + combineConfigNodeAddressAndPortBeta, +} from './utils'; /** * getNodeBalancerConfigs @@ -96,6 +100,33 @@ export const createNodeBalancerConfigBeta = ( nodeBalancerId )}/configs` ), + setData( + data, + createNodeBalancerConfigSchema, + combineConfigNodeAddressAndPortBeta + ) + ); + +/** + * rebuildNodeBalancerConfig + * + * Rebuilds a NodeBalancer Config and its Nodes that you have permission to modify. + * + * @param nodeBalancerId { number } The NodeBalancer to receive the new config. + * @param configId { number } The ID of the configuration profile to be updated + */ +export const rebuildNodeBalancerConfig = ( + nodeBalancerId: number, + configId: number, + data: RebuildNodeBalancerConfig +) => + Request( + setMethod('POST'), + setURL( + `${API_ROOT}/nodebalancers/${encodeURIComponent( + nodeBalancerId + )}/configs/${encodeURIComponent(configId)}/rebuild` + ), setData( data, createNodeBalancerConfigSchema, @@ -103,6 +134,33 @@ export const createNodeBalancerConfigBeta = ( ) ); +/** + * rebuildNodeBalancerConfigBeta + * + * Rebuilds a NodeBalancer Config and its Nodes that you have permission to modify. + * + * @param nodeBalancerId { number } The NodeBalancer to receive the new config. + * @param configId { number } The ID of the configuration profile to be updated + */ +export const rebuildNodeBalancerConfigBeta = ( + nodeBalancerId: number, + configId: number, + data: RebuildNodeBalancerConfig +) => + Request( + setMethod('POST'), + setURL( + `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( + nodeBalancerId + )}/configs/${encodeURIComponent(configId)}/rebuild` + ), + setData( + data, + createNodeBalancerConfigSchema, + combineConfigNodeAddressAndPortBeta + ) + ); + /** * updateNodeBalancerConfig * diff --git a/packages/api-v4/src/nodebalancers/nodebalancers.ts b/packages/api-v4/src/nodebalancers/nodebalancers.ts index f9469565dd8..8560eafe0eb 100644 --- a/packages/api-v4/src/nodebalancers/nodebalancers.ts +++ b/packages/api-v4/src/nodebalancers/nodebalancers.ts @@ -15,8 +15,12 @@ import type { CreateNodeBalancerPayload, NodeBalancer, NodeBalancerStats, + NodebalancerVpcConfig, } from './types'; -import { combineNodeBalancerConfigNodeAddressAndPort } from './utils'; +import { + combineNodeBalancerConfigNodeAddressAndPort, + combineNodeBalancerConfigNodeAddressAndPortBeta, +} from './utils'; import type { Firewall } from '../firewalls/types'; /** @@ -107,7 +111,7 @@ export const createNodeBalancerBeta = (data: CreateNodeBalancerPayload) => setData( data, NodeBalancerSchema, - combineNodeBalancerConfigNodeAddressAndPort + combineNodeBalancerConfigNodeAddressAndPortBeta ) ); @@ -174,3 +178,45 @@ export const getNodeBalancerTypes = (params?: Params) => setMethod('GET'), setParams(params) ); + +/** + * getNodeBalancerVPCConfigsBeta + * + * View all VPC Config information for this NodeBalancer + * + * @param nodeBalancerId { number } The ID of the NodeBalancer to view vpc config info for. + */ +export const getNodeBalancerVPCConfigsBeta = ( + nodeBalancerId: number, + params?: Params, + filter?: Filter +) => + Request>( + setURL( + `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( + nodeBalancerId + )}/vpcs` + ), + setMethod('GET'), + setXFilter(filter), + setParams(params) + ); +/** + * getNodeBalancerVPCConfigBeta + * + * View VPC Config information for this NodeBalancer and VPC Config id + * + * @param nodeBalancerId { number } The ID of the NodeBalancer to view vpc config info for. + */ +export const getNodeBalancerVPCConfigBeta = ( + nodeBalancerId: number, + nbVpcConfigId: number +) => + Request( + setURL( + `${BETA_API_ROOT}/nodebalancers/${encodeURIComponent( + nodeBalancerId + )}/vpcs/${encodeURIComponent(nbVpcConfigId)}` + ), + setMethod('GET') + ); diff --git a/packages/api-v4/src/nodebalancers/types.ts b/packages/api-v4/src/nodebalancers/types.ts index 2e782011d09..0ce208c81fa 100644 --- a/packages/api-v4/src/nodebalancers/types.ts +++ b/packages/api-v4/src/nodebalancers/types.ts @@ -134,6 +134,15 @@ export interface NodeBalancerStats { }; } +export interface NodebalancerVpcConfig { + id: number; + nodebalancer_id: number; + vpc_id: number; + subnet_id: number; + ipv4_range: string | null; + ipv6_range: string | null; +} + export interface CreateNodeBalancerConfig { port?: number; /** @@ -186,6 +195,8 @@ export interface CreateNodeBalancerConfig { export type UpdateNodeBalancerConfig = CreateNodeBalancerConfig; +export type RebuildNodeBalancerConfig = CreateNodeBalancerConfig; + export interface CreateNodeBalancerConfigNode { address: string; label: string; @@ -235,7 +246,7 @@ export interface CreateNodeBalancerPayload { configs: CreateNodeBalancerConfig[]; firewall_id?: number; tags?: string[]; - vpc?: { + vpcs?: { subnet_id: number; ipv4_range: string; ipv6_range?: string; diff --git a/packages/api-v4/src/nodebalancers/utils.ts b/packages/api-v4/src/nodebalancers/utils.ts index 42f81e54700..17d4fdab160 100644 --- a/packages/api-v4/src/nodebalancers/utils.ts +++ b/packages/api-v4/src/nodebalancers/utils.ts @@ -10,6 +10,17 @@ export const combineConfigNodeAddressAndPort = (data: any) => ({ })), }); +export const combineConfigNodeAddressAndPortBeta = (data: any) => ({ + ...data, + nodes: data.nodes.map((n: any) => ({ + address: `${n.address}:${n.port}`, + label: n.label, + mode: n.mode, + weight: n.weight, + subnet_id: n.subnet_id, + })), +}); + export const combineNodeBalancerConfigNodeAddressAndPort = (data: any) => ({ ...data, configs: data.configs.map((c: any) => ({ @@ -23,6 +34,20 @@ export const combineNodeBalancerConfigNodeAddressAndPort = (data: any) => ({ })), }); +export const combineNodeBalancerConfigNodeAddressAndPortBeta = (data: any) => ({ + ...data, + configs: data.configs.map((c: any) => ({ + ...c, + nodes: c.nodes.map((n: any) => ({ + address: `${n.address}:${n.port}`, + label: n.label, + mode: n.mode, + weight: n.weight, + subnet_id: n.subnet_id, + })), + })), +}); + export const mergeAddressAndPort = (node: NodeBalancerConfigNodeWithPort) => ({ ...node, address: `${node.address}:${node.port}`, From ba33b07935c7feb7caf36ed64faf210c810f7251 Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Tue, 25 Mar 2025 14:01:06 +0530 Subject: [PATCH 18/84] refactor: [M3-9266] - Make `RegionSelect` pure (#11790) * Initial commit: Make RegionSelect Pure * Import fix * Fix import * Refactor & add accountAvailability props to RegionSelect * Fix RegionSelect cypress component tests * Refactor & add accountAvailability props to RegionMultiSelect * Clean up * More clean up... * Clean up comments * Move regionsData to `utilities` package * Move ListItemOption to `ui` package * Update comment * Update todo comments * Update comment * Added changeset: Make `RegionSelect` and `RegionMultiSelect` pure * Added changeset: Move `ListItemOption` from `manager` to `ui` package * Added changeset: Move `ListItemOption` from `manager` to `ui` package * Added changeset: Move `regionsData` from `manager` to `utilities` package * Added changeset: Move `regionsData` from `manager` to `utilities` package * Add todo comments * Update remaining imports * Move `LinodeCreateType` to @linode/utilities types * Move regions factory to utilities package * Update todo comments * Add/update todo comments * More updates * Add Flag Component as a prop to `RegionSelect` and `RegionMultiSelect` * RegionOption to except Flag component prop and move accountAvability factory * Fix storybook * fix: Temporarily add flags CDN in RegionSelect and update tests * Update image alt * Revert `Flag` as a prop to `RegionSelect` & `RegionMultiSelect` * Revert accountAvailability query data as a prop * regions query as a prop clean up * Added changeset: Move `LinodeCreateType` to `utilities` package * Added changeset: Move `LinodeCreateType` to `utilities` package * Clean up: unnecessary code comments --- .../pr-11790-removed-1741678858029.md | 5 ++++ .../pr-11790-removed-1741678912235.md | 5 ++++ .../pr-11790-removed-1742396865376.md | 5 ++++ .../pr-11790-tech-stories-1741678788058.md | 5 ++++ .../components/region-select.spec.tsx | 19 ++++++++++++++- .../cloudpulse/alert-show-details.spec.ts | 3 +-- .../cloudpulse-dashboard-errors.spec.ts | 2 +- .../core/cloudpulse/create-user-alert.spec.ts | 2 +- .../dbaas-widgets-verification.spec.ts | 2 +- .../core/cloudpulse/edit-system-alert.spec.ts | 8 ++----- .../core/cloudpulse/edit-user-alert.spec.ts | 2 +- .../linode-widget-verification.spec.ts | 2 +- .../cloudpulse/timerange-verification.spec.ts | 2 +- .../migrate-linode-with-firewall.spec.ts | 2 +- .../e2e/core/general/gdpr-agreement.spec.ts | 3 ++- .../core/images/manage-image-regions.spec.ts | 3 ++- .../e2e/core/kubernetes/lke-create.spec.ts | 3 +-- .../create-linode-in-core-region.spec.ts | 3 ++- ...reate-linode-in-distributed-region.spec.ts | 3 ++- .../create-linode-region-select.spec.ts | 2 +- ...create-linode-with-disk-encryption.spec.ts | 2 +- .../create-linode-with-user-data.spec.ts | 3 ++- .../linodes/create-linode-with-vlan.spec.ts | 3 ++- .../linodes/create-linode-with-vpc.spec.ts | 6 +++-- .../e2e/core/linodes/create-linode.spec.ts | 2 +- .../e2e/core/linodes/plan-selection.spec.ts | 8 ++----- .../smoke-create-nodebal.spec.ts | 7 ++---- .../enable-object-storage.spec.ts | 4 ++-- .../bucket-create-gen2.spec.ts | 2 +- .../bucket-details-gen2.spec.ts | 2 +- .../bucket-object-gen2.spec.ts | 2 +- .../access-keys-multicluster.spec.ts | 2 +- .../bucket-create-multicluster.spec.ts | 7 ++---- .../bucket-details-multicluster.spec.ts | 7 ++---- ...reate-linode-with-placement-groups.spec.ts | 2 +- .../create-placement-groups.spec.ts | 2 +- ...placement-groups-linode-assignment.spec.ts | 2 +- .../e2e/core/volumes/create-volume.spec.ts | 5 ++-- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 8 ++----- .../manager/cypress/support/util/regions.ts | 2 +- .../components/ImageSelect/ImageOption.tsx | 7 +++--- .../src/components/ImageSelect/utilities.ts | 2 +- .../PlacementGroupSelectOption.tsx | 8 +++---- .../PlacementGroupsSelect.test.tsx | 7 +++--- .../RegionMultiSelect.stories.tsx | 5 ++-- .../RegionSelect/RegionMultiSelect.test.tsx | 12 +++++++--- .../RegionSelect/RegionMultiSelect.tsx | 13 ++++++++--- .../components/RegionSelect/RegionOption.tsx | 18 ++++++++------- .../RegionSelect/RegionSelect.stories.tsx | 4 ++-- .../RegionSelect/RegionSelect.test.tsx | 8 ++++--- .../components/RegionSelect/RegionSelect.tsx | 16 ++++++++----- .../RegionSelect/RegionSelect.types.ts | 13 +++++++++-- .../RegionSelect/RegionSelect.utils.test.tsx | 2 +- .../RegionSelect/RegionSelect.utils.tsx | 23 +++++++++++-------- .../TransferDisplayDialog.test.tsx | 2 +- .../TransferDisplay/TransferDisplayDialog.tsx | 6 +++-- .../components/TransferDisplay/utils.test.tsx | 5 ++-- packages/manager/src/factories/index.ts | 2 -- .../features/Account/Quotas/Quotas.test.tsx | 2 +- .../src/features/Account/Quotas/Quotas.tsx | 3 +++ .../Billing/PdfGenerator/PdfGenerator.test.ts | 2 +- .../Billing/PdfGenerator/utils.test.ts | 6 +++-- .../Alerts/AlertsDetail/AlertDetail.test.tsx | 2 +- .../AlertsRegionFilter.test.tsx | 2 +- .../AlertsResources/AlertsRegionFilter.tsx | 3 +++ .../AlertsResources/AlertsResources.test.tsx | 5 ++-- .../GeneralInformation/RegionSelect.tsx | 4 ++++ .../CloudPulseModifyAlertResources.test.tsx | 3 ++- .../EditAlert/EditAlertResources.test.tsx | 3 ++- .../Alerts/Utils/AlertResourceUtils.test.ts | 2 +- .../shared/CloudPulseRegionSelect.test.tsx | 3 ++- .../shared/CloudPulseRegionSelect.tsx | 11 +++++---- .../DatabaseCreate/DatabaseClusterData.tsx | 3 +++ ...tabaseSummaryClusterConfiguration.test.tsx | 2 +- .../RegionStatusBanner.test.tsx | 4 ++-- .../ImagesCreate/CreateImageTab.test.tsx | 8 ++----- .../Images/ImagesCreate/ImageUpload.tsx | 1 + .../ImageRegions/ImageRegionRow.test.tsx | 2 +- .../ManageImageRegionsForm.test.tsx | 3 ++- .../ImageRegions/ManageImageRegionsForm.tsx | 8 +++++-- .../ClusterList/KubernetesClusterRow.test.tsx | 3 ++- .../CreateCluster/CreateCluster.tsx | 3 +++ .../KubeCheckoutBar/KubeCheckoutBar.test.tsx | 3 ++- .../LinodeCreate/Addons/Addons.test.tsx | 2 +- .../LinodeCreate/Addons/Backups.test.tsx | 7 ++---- .../LinodeCreate/Addons/PrivateIP.test.tsx | 3 ++- .../Details/PlacementGroupPanel.test.tsx | 2 +- .../Linodes/LinodeCreate/EUAgreement.test.tsx | 3 ++- .../Linodes/LinodeCreate/Region.test.tsx | 2 +- .../features/Linodes/LinodeCreate/Region.tsx | 3 ++- .../Linodes/LinodeCreate/Region.utils.test.ts | 4 +++- .../Linodes/LinodeCreate/Region.utils.ts | 2 +- .../Linodes/LinodeCreate/Security.test.tsx | 8 ++----- .../LinodeCreate/Summary/Summary.test.tsx | 3 ++- .../Linodes/LinodeCreate/TwoStepRegion.tsx | 8 +++++-- .../LinodeCreate/UserData/UserData.test.tsx | 3 ++- .../LinodeCreate/UserData/UserDataHeading.tsx | 2 +- .../Linodes/LinodeCreate/VLAN/VLAN.test.tsx | 2 +- .../Linodes/LinodeCreate/VPC/VPC.test.tsx | 2 +- .../Linodes/LinodeCreate/resolvers.ts | 2 +- .../shared/LinodeSelectTableRow.test.tsx | 8 ++----- .../Linodes/LinodeCreate/utilities.ts | 2 +- .../LinodeBackup/EnableBackupsDialog.test.tsx | 3 ++- .../NetworkTransfer.test.tsx | 7 ++---- .../NetworkingSummaryPanel.tsx | 8 ++++--- .../LinodeSettings/VPCPanel.test.tsx | 3 ++- .../LinodesLanding/DisplayGroupedLinodes.tsx | 5 +++- .../Linodes/LinodesLanding/DisplayLinodes.tsx | 7 +++--- .../LinodeActionMenu.test.tsx | 6 +++-- .../Linodes/MigrateLinode/ConfigureForm.tsx | 3 ++- .../manager/src/features/Linodes/index.tsx | 12 ++++++---- .../manager/src/features/Linodes/types.ts | 3 +-- .../NodeBalancers/NodeBalancerCreate.tsx | 3 +++ .../AccessKeyRegions/AccessKeyRegions.tsx | 3 +++ .../AccessKeyTable/HostNameTableCell.test.tsx | 3 ++- .../AccessKeyLanding/HostNamesDrawer.test.tsx | 2 +- .../AccessKeyLanding/HostNamesList.test.tsx | 4 ++-- .../BucketDetailsDrawer.test.tsx | 7 ++++-- .../BucketLanding/BucketRegions.test.tsx | 2 +- .../BucketLanding/BucketRegions.tsx | 5 ++++ .../BucketLanding/ClusterSelect.tsx | 4 ++++ .../BucketLanding/CreateBucketDrawer.test.tsx | 2 +- ...lacementGroupsAssignLinodesDrawer.test.tsx | 7 ++---- .../PlacementGroupsCreateDrawer.tsx | 7 ++++-- .../PlacementGroupsDetail.test.tsx | 7 ++---- .../PlacementGroupsSummary.test.tsx | 3 ++- .../PlacementGroupsDetailPanel.test.tsx | 3 ++- .../PlacementGroupsEditDrawer.test.tsx | 3 ++- .../PlacementGroupsRow.test.tsx | 7 ++---- .../features/PlacementGroups/utils.test.ts | 7 ++---- .../FormComponents/SubnetContent.tsx | 2 +- .../FormComponents/VPCTopSectionContent.tsx | 7 ++++-- .../VPCs/VPCLanding/VPCEditDrawer.tsx | 3 +++ .../src/features/Volumes/VolumeCreate.tsx | 3 +++ .../PlansAvailabilityNotice.test.tsx | 3 +-- .../components/PlansPanel/PlansPanel.tsx | 2 +- .../components/PlansPanel/utils.test.ts | 2 +- packages/manager/src/hooks/useCreateVPC.ts | 2 +- .../src/mocks/presets/crud/handlers/quotas.ts | 3 +-- .../presets/extra/regions/legacyRegions.ts | 2 +- packages/manager/src/mocks/serverHandlers.ts | 10 ++++---- .../manager/src/routes/firewalls/index.ts | 2 +- .../utilities/analytics/formEventAnalytics.ts | 2 +- .../manager/src/utilities/analytics/types.ts | 2 +- .../doesRegionSupportFeature.test.ts | 2 +- .../pr-11790-added-1741678870637.md | 5 ++++ .../ListItemOption}/ListItemOption.tsx | 10 +++++--- .../ui/src/components/ListItemOption/index.ts | 1 + packages/ui/src/components/index.ts | 1 + .../pr-11790-added-1741678972894.md | 5 ++++ .../pr-11790-added-1742396887174.md | 5 ++++ packages/utilities/.prettierrc | 2 +- packages/utilities/package.json | 2 +- packages/utilities/src/__data__/index.ts | 1 + .../src/__data__/regionsData.ts | 0 .../src/factories/accountAvailability.ts | 5 ++-- packages/utilities/src/factories/index.ts | 2 ++ .../src/factories/regions.ts | 2 +- packages/utilities/src/index.ts | 2 ++ .../src/types/LinodeCreateType.ts} | 0 packages/utilities/src/types/index.ts | 1 + 161 files changed, 421 insertions(+), 277 deletions(-) create mode 100644 packages/manager/.changeset/pr-11790-removed-1741678858029.md create mode 100644 packages/manager/.changeset/pr-11790-removed-1741678912235.md create mode 100644 packages/manager/.changeset/pr-11790-removed-1742396865376.md create mode 100644 packages/manager/.changeset/pr-11790-tech-stories-1741678788058.md create mode 100644 packages/ui/.changeset/pr-11790-added-1741678870637.md rename packages/{manager/src/components => ui/src/components/ListItemOption}/ListItemOption.tsx (92%) create mode 100644 packages/ui/src/components/ListItemOption/index.ts create mode 100644 packages/utilities/.changeset/pr-11790-added-1741678972894.md create mode 100644 packages/utilities/.changeset/pr-11790-added-1742396887174.md create mode 100644 packages/utilities/src/__data__/index.ts rename packages/{manager => utilities}/src/__data__/regionsData.ts (100%) rename packages/{manager => utilities}/src/factories/accountAvailability.ts (79%) rename packages/{manager => utilities}/src/factories/regions.ts (96%) rename packages/{manager/src/features/Linodes/LinodeCreate/types.ts => utilities/src/types/LinodeCreateType.ts} (100%) diff --git a/packages/manager/.changeset/pr-11790-removed-1741678858029.md b/packages/manager/.changeset/pr-11790-removed-1741678858029.md new file mode 100644 index 00000000000..062d5dcd4a9 --- /dev/null +++ b/packages/manager/.changeset/pr-11790-removed-1741678858029.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/manager/.changeset/pr-11790-removed-1741678912235.md b/packages/manager/.changeset/pr-11790-removed-1741678912235.md new file mode 100644 index 00000000000..ef01ea6e319 --- /dev/null +++ b/packages/manager/.changeset/pr-11790-removed-1741678912235.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `regionsData` from `manager` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/manager/.changeset/pr-11790-removed-1742396865376.md b/packages/manager/.changeset/pr-11790-removed-1742396865376.md new file mode 100644 index 00000000000..7d7eeb8e50e --- /dev/null +++ b/packages/manager/.changeset/pr-11790-removed-1742396865376.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `LinodeCreateType` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/manager/.changeset/pr-11790-tech-stories-1741678788058.md b/packages/manager/.changeset/pr-11790-tech-stories-1741678788058.md new file mode 100644 index 00000000000..06030801f26 --- /dev/null +++ b/packages/manager/.changeset/pr-11790-tech-stories-1741678788058.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Make `RegionSelect` and `RegionMultiSelect` pure ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/manager/cypress/component/components/region-select.spec.tsx b/packages/manager/cypress/component/components/region-select.spec.tsx index 656d5dacaf3..0e18e24fb0f 100644 --- a/packages/manager/cypress/component/components/region-select.spec.tsx +++ b/packages/manager/cypress/component/components/region-select.spec.tsx @@ -1,3 +1,4 @@ +import { accountAvailabilityFactory, regionFactory } from '@linode/utilities'; import * as React from 'react'; import { mockGetAccountAvailability } from 'support/intercepts/account'; import { ui } from 'support/ui'; @@ -6,7 +7,6 @@ import { createSpy } from 'support/util/components'; import { componentTests, visualTests } from 'support/util/components'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; -import { accountAvailabilityFactory, regionFactory } from 'src/factories'; componentTests('RegionSelect', (mount) => { beforeEach(() => { @@ -26,6 +26,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -54,6 +55,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -83,6 +85,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={[region]} value={undefined} @@ -112,6 +115,7 @@ componentTests('RegionSelect', (mount) => { Other Element {}} regions={[region]} value={undefined} @@ -145,6 +149,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -179,6 +184,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={regionToPreselect.id} @@ -214,6 +220,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={regionToSelect.id} @@ -241,6 +248,7 @@ componentTests('RegionSelect', (mount) => { {}} regions={regions} value={regionToSelect.id} @@ -260,6 +268,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -277,6 +286,7 @@ componentTests('RegionSelect', (mount) => { mount( { mount( { mount( {}} regions={regions} value={undefined} @@ -379,6 +391,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -408,6 +421,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -438,6 +452,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={undefined} @@ -450,6 +465,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={selectedRegion.id} @@ -462,6 +478,7 @@ componentTests('RegionSelect', (mount) => { mount( {}} regions={regions} value={selectedRegion.id} diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts index cc1eb5f9dbf..5dfa6cbc1c5 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alert-show-details.spec.ts @@ -4,7 +4,7 @@ * This file contains Cypress tests that validate the display and content of the Alerts Show Detail Page in the CloudPulse application. * It ensures that all alert details, criteria, and resource information are displayed correctly. */ -import { capitalize } from '@linode/utilities'; +import { capitalize, regionFactory } from '@linode/utilities'; import { aggregationTypeMap, dimensionOperatorTypeMap, @@ -28,7 +28,6 @@ import { alertRulesFactory, databaseFactory, notificationChannelFactory, - regionFactory, } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts index 663108e3b42..24571e6df3d 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/cloudpulse-dashboard-errors.spec.ts @@ -1,6 +1,7 @@ /** * @file Error Handling Tests for CloudPulse Dashboard. */ +import { regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -32,7 +33,6 @@ import { dashboardFactory, dashboardMetricFactory, databaseFactory, - regionFactory, widgetFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts index 7b5fdff0cb1..7a12abb99b1 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts @@ -2,6 +2,7 @@ * @fileoverview Cypress test suite for the "Create Alert" functionality. */ +import { regionFactory } from '@linode/utilities'; import { statusMap } from 'support/constants/alert'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; @@ -26,7 +27,6 @@ import { databaseFactory, memoryRulesFactory, notificationChannelFactory, - regionFactory, triggerConditionFactory, } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index 6b88891e6c4..e56c52232f3 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -1,6 +1,7 @@ /** * @file Integration Tests for CloudPulse Dbass Dashboard. */ +import { regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -27,7 +28,6 @@ import { databaseFactory, kubeLinodeFactory, linodeFactory, - regionFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts index b0f28d636c0..de0def5b4b4 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-system-alert.spec.ts @@ -4,6 +4,7 @@ * This file contains Cypress tests for the Edit Alert page of the CloudPulse application. * It ensures that users can navigate to the Edit Alert Page and that alerts are correctly displayed and interactive on the Edit page. */ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetAlertDefinitions, @@ -15,12 +16,7 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; -import { - accountFactory, - alertFactory, - databaseFactory, - regionFactory, -} from 'src/factories'; +import { accountFactory, alertFactory, databaseFactory } from 'src/factories'; import type { Alert, Database } from '@linode/api-v4'; import type { Flags } from 'src/featureFlags'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts index a39d248f5c6..e9aae01f0bc 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts @@ -5,6 +5,7 @@ * It verifies that alert details are correctly displayed, interactive, and editable. */ +import { regionFactory } from '@linode/utilities'; import { EVALUATION_PERIOD_DESCRIPTION, METRIC_DESCRIPTION_DATA_FIELD, @@ -36,7 +37,6 @@ import { databaseFactory, memoryRulesFactory, notificationChannelFactory, - regionFactory, triggerConditionFactory, } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index 640d5b22b87..b3323f2b5b3 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -1,6 +1,7 @@ /** * @file Integration Tests for CloudPulse Linode Dashboard. */ +import { regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -25,7 +26,6 @@ import { dashboardMetricFactory, kubeLinodeFactory, linodeFactory, - regionFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts index 8dab09b4141..17d5a0d5f84 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/timerange-verification.spec.ts @@ -1,6 +1,7 @@ /** * @file Integration Tests for CloudPulse Custom and Preset Verification */ +import { regionFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; @@ -29,7 +30,6 @@ import { dashboardMetricFactory, databaseFactory, profileFactory, - regionFactory, widgetFactory, } from 'src/factories'; import { convertToGmt } from 'src/features/CloudPulse/Utils/CloudPulseDateTimePickerUtils'; diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index 241e0652c41..6856938f28e 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable sonarjs/no-duplicate-string */ +import { regionFactory } from '@linode/utilities'; import { createLinodeRequestFactory, firewallFactory, linodeFactory, - regionFactory, } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { diff --git a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts index 69f9ff92b38..529867889bc 100644 --- a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts +++ b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts @@ -1,4 +1,5 @@ -import { linodeFactory, regionFactory } from '@src/factories'; +import { regionFactory } from '@linode/utilities'; +import { linodeFactory } from '@src/factories'; import { mockGetAccountAgreements } from 'support/intercepts/account'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; diff --git a/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts index 215ebb55219..c4139b628ad 100644 --- a/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts +++ b/packages/manager/cypress/e2e/core/images/manage-image-regions.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetCustomImages, mockGetImage, @@ -8,7 +9,7 @@ import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { extendRegion } from 'support/util/regions'; -import { imageFactory, regionFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; import type { Image, Region } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 5a3b3490b3e..920ad23698b 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -1,7 +1,7 @@ /** * @file LKE creation end-to-end tests. */ -import { pluralize } from '@linode/utilities'; +import { pluralize, regionFactory } from '@linode/utilities'; import { dcPricingDocsLabel, dcPricingDocsUrl, @@ -52,7 +52,6 @@ import { linodeTypeFactory, lkeHighAvailabilityTypeFactory, nodePoolFactory, - regionFactory, } from 'src/factories'; import { CLUSTER_TIER_DOCS_LINK, diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts index 7c82a1177d0..71ae9f83058 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { @@ -8,7 +9,7 @@ import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; -import { linodeFactory, regionFactory } from 'src/factories'; +import { linodeFactory } from 'src/factories'; describe('Create Linode in a Core Region', () => { /* diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts index 746117f2952..ac52798d3c4 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode, @@ -12,7 +13,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import { linodeFactory, linodeTypeFactory, regionFactory } from 'src/factories'; +import { linodeFactory, linodeTypeFactory } from 'src/factories'; import type { Region } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts index 914b1dc8d3a..aa4275e4f34 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-region-select.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@src/factories'; +import { regionFactory } from '@linode/utilities'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { extendRegion } from 'support/util/regions'; diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts index 884fc31471c..de6534ef710 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts @@ -1,8 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { accountFactory, linodeFactory, linodeTypeFactory, - regionFactory, } from '@src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index d112ad97449..c500e0f5a94 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { mockCreateLinode, @@ -9,7 +10,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory, linodeFactory, regionFactory } from 'src/factories'; +import { imageFactory, linodeFactory } from 'src/factories'; describe('Create Linode with user data', () => { /* diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index 7515c251c16..8a1b4cdc67f 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; @@ -12,7 +13,7 @@ import { } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { VLANFactory, linodeFactory, regionFactory } from 'src/factories'; +import { VLANFactory, linodeFactory } from 'src/factories'; describe('Create Linode with VLANs', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index 7cf424d3faf..1aa3877ae42 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -1,4 +1,7 @@ -import { linodeConfigInterfaceFactoryWithVPC } from '@linode/utilities'; +import { + linodeConfigInterfaceFactoryWithVPC, + regionFactory, +} from '@linode/utilities'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -27,7 +30,6 @@ import { chooseRegion } from 'support/util/regions'; import { linodeConfigFactory, linodeFactory, - regionFactory, subnetFactory, vpcFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index fb5bc50cea9..7e01e91e1f0 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -5,6 +5,7 @@ import { linodeConfigInterfaceFactory, linodeConfigInterfaceFactoryWithVPC, + regionFactory, } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; @@ -46,7 +47,6 @@ import { linodeFactory, linodeTypeFactory, profileFactory, - regionFactory, subnetFactory, vpcFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts index f4c539c1afd..bad5a19b2ce 100644 --- a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts @@ -1,10 +1,6 @@ // TODO: Cypress -import { - accountFactory, - linodeTypeFactory, - regionAvailabilityFactory, - regionFactory, -} from '@src/factories'; +import { regionAvailabilityFactory, regionFactory } from '@linode/utilities'; +import { accountFactory, linodeTypeFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 12471265c8d..690377d60cf 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -16,15 +16,12 @@ const deployNodeBalancer = () => { cy.get('[data-qa-deploy-nodebalancer]').click(); }; +import { regionFactory } from '@linode/utilities'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; import { mockGetRegions } from 'support/intercepts/regions'; -import { - linodeFactory, - nodeBalancerFactory, - regionFactory, -} from 'src/factories'; +import { linodeFactory, nodeBalancerFactory } from 'src/factories'; const createNodeBalancerWithUI = ( nodeBal: NodeBalancer, diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts index 78150cba226..78dd831ccd1 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts @@ -1,13 +1,13 @@ /** * @file Cypress integration tests for OBJ enrollment and cancellation. */ +import { regionFactory } from '@linode/utilities'; import { accountFactory, accountSettingsFactory, objectStorageClusterFactory, objectStorageKeyFactory, profileFactory, - regionFactory, } from '@src/factories'; import { mockGetAccount, @@ -17,10 +17,10 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCancelObjectStorage, mockCreateAccessKey, + mockGetAccessKeys, mockGetBuckets, mockGetClusters, } from 'support/intercepts/object-storage'; -import { mockGetAccessKeys } from 'support/intercepts/object-storage'; import { mockGetProfile } from 'support/intercepts/profile'; import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts index 98265c7b772..596693bf153 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-create-gen2.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -19,7 +20,6 @@ import { accountFactory, objectStorageBucketFactoryGen2, objectStorageEndpointsFactory, - regionFactory, } from 'src/factories'; import { profileFactory } from 'src/factories/profile'; diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts index 6ac187e0c66..ba58475cd86 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-details-gen2.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -10,7 +11,6 @@ import { accountFactory, objectStorageBucketFactoryGen2, objectStorageEndpointsFactory, - regionFactory, } from 'src/factories'; import type { ACLType, ObjectStorageEndpointTypes } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts index dd1feeda2b4..6d10cf1b60a 100644 --- a/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageGen2/bucket-object-gen2.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import 'cypress-file-upload'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; @@ -22,7 +23,6 @@ import { accountFactory, objectStorageBucketFactoryGen2, objectStorageEndpointsFactory, - regionFactory, } from 'src/factories'; import type { ObjectStorageEndpoint } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts index df03e62632c..1b3db135ab7 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/access-keys-multicluster.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -21,7 +22,6 @@ import { accountFactory, objectStorageBucketFactory, objectStorageKeyFactory, - regionFactory, } from 'src/factories'; import type { ObjectStorageKeyBucketAccess } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts index a6ba75cd979..3c597b9def5 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-create-multicluster.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -10,11 +11,7 @@ import { ui } from 'support/ui'; import { randomLabel, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import { - accountFactory, - objectStorageBucketFactory, - regionFactory, -} from 'src/factories'; +import { accountFactory, objectStorageBucketFactory } from 'src/factories'; describe('Object Storage Multicluster Bucket create', () => { /* diff --git a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts index d6769154a3a..253b54a4596 100644 --- a/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorageMulticluster/bucket-details-multicluster.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetBucket } from 'support/intercepts/object-storage'; @@ -5,11 +6,7 @@ import { mockGetRegions } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { randomLabel } from 'support/util/random'; -import { - accountFactory, - objectStorageBucketFactory, - regionFactory, -} from 'src/factories'; +import { accountFactory, objectStorageBucketFactory } from 'src/factories'; describe('Object Storage Multicluster Bucket Details Tabs', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts index 3acbd05ace0..dbc412e08d9 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateLinode, @@ -18,7 +19,6 @@ import { linodeFactory, placementGroupFactory, } from 'src/factories'; -import { regionFactory } from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; const mockAccount = accountFactory.build(); diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts index a56e60fe1e9..8a42a655912 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-placement-groups.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockCreatePlacementGroup, @@ -9,7 +10,6 @@ import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { accountFactory, placementGroupFactory } from 'src/factories'; -import { regionFactory } from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; const mockAccount = accountFactory.build(); diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts index 46c161c0882..6934266d06d 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodeDetails, @@ -21,7 +22,6 @@ import { accountFactory, linodeFactory, placementGroupFactory, - regionFactory, } from 'src/factories'; import type { Linode } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index 3fcdaad7eaf..49c42bebf4d 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { accountUserFactory, grantsFactory, @@ -5,8 +6,8 @@ import { } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { entityTag } from 'support/constants/cypress'; -import { mockGetUser } from 'support/intercepts/account'; import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetUser } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetLinodeDetails, @@ -28,7 +29,7 @@ import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { accountFactory, regionFactory, volumeFactory } from 'src/factories'; +import { accountFactory, volumeFactory } from 'src/factories'; import { createLinodeRequestFactory, linodeFactory, diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index bfa81f820c7..dde410c921c 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -2,12 +2,8 @@ * @file Integration tests for VPC create flow. */ -import { - linodeFactory, - regionFactory, - subnetFactory, - vpcFactory, -} from '@src/factories'; +import { regionFactory } from '@linode/utilities'; +import { linodeFactory, subnetFactory, vpcFactory } from '@src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateVPC, diff --git a/packages/manager/cypress/support/util/regions.ts b/packages/manager/cypress/support/util/regions.ts index bda74ea62d3..26b80b4ace0 100644 --- a/packages/manager/cypress/support/util/regions.ts +++ b/packages/manager/cypress/support/util/regions.ts @@ -17,7 +17,7 @@ import type { Capabilities, Region } from '@linode/api-v4'; * the `apiLabel` property. * * @see {@link https://github.com/linode/manager/pull/10740|Cloud Manager PR #10740} - * @see {@link src/queries/regions/regions.ts} + * @see {@link packages/queries/src/regions/regions.ts (@linode/queries)} */ export interface ExtendedRegion extends Region { /** Region label as defined by API v4. */ diff --git a/packages/manager/src/components/ImageSelect/ImageOption.tsx b/packages/manager/src/components/ImageSelect/ImageOption.tsx index 7616ab5af15..87d920afd6d 100644 --- a/packages/manager/src/components/ImageSelect/ImageOption.tsx +++ b/packages/manager/src/components/ImageSelect/ImageOption.tsx @@ -1,22 +1,21 @@ -import { Stack, Tooltip, Typography } from '@linode/ui'; +import { ListItemOption, Stack, Tooltip, Typography } from '@linode/ui'; import React from 'react'; import CloudInitIcon from 'src/assets/icons/cloud-init.svg'; -import { ListItemOption } from 'src/components/ListItemOption'; import { useFlags } from 'src/hooks/useFlags'; import { OSIcon } from '../OSIcon'; import { isImageDeprecated } from './utilities'; import type { Image } from '@linode/api-v4'; -import type { ListItemProps } from 'src/components/ListItemOption'; +import type { ListItemOptionProps } from '@linode/ui'; export const ImageOption = ({ disabledOptions, item, props, selected, -}: ListItemProps) => { +}: ListItemOptionProps) => { const flags = useFlags(); return ( diff --git a/packages/manager/src/components/ImageSelect/utilities.ts b/packages/manager/src/components/ImageSelect/utilities.ts index dcb1babfb1e..e18797f7500 100644 --- a/packages/manager/src/components/ImageSelect/utilities.ts +++ b/packages/manager/src/components/ImageSelect/utilities.ts @@ -4,7 +4,7 @@ import { MAX_MONTHS_EOL_FILTER } from 'src/constants'; import type { ImageSelectVariant } from './ImageSelect'; import type { Image, RegionSite } from '@linode/api-v4'; -import type { DisableItemOption } from 'src/components/ListItemOption'; +import type { DisableItemOption } from '@linode/ui'; /** * Given a Image Select "variant", this PR returns an diff --git a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx index 5e5904431ba..d7af0dac896 100644 --- a/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx +++ b/packages/manager/src/components/PlacementGroupsSelect/PlacementGroupSelectOption.tsx @@ -1,18 +1,16 @@ import { PLACEMENT_GROUP_TYPES } from '@linode/api-v4'; -import { Box, Stack } from '@linode/ui'; +import { Box, ListItemOption, Stack } from '@linode/ui'; import React from 'react'; -import { ListItemOption } from 'src/components/ListItemOption'; - import type { PlacementGroup } from '@linode/api-v4'; -import type { ListItemProps } from 'src/components/ListItemOption'; +import type { ListItemOptionProps } from '@linode/ui'; export const PlacementGroupSelectOption = ({ disabledOptions, item, props, selected, -}: ListItemProps) => ( +}: ListItemOptionProps) => ( { queryMocks.useAllPlacementGroupsQuery.mockReturnValue({ data: [ placementGroupFactory.build({ - placement_group_type: 'affinity:local', id: 1, is_compliant: true, - placement_group_policy: 'strict', label: 'my-placement-group', members: [ { @@ -78,6 +77,8 @@ describe('PlacementGroupSelect', () => { linode_id: 1, }, ], + placement_group_policy: 'strict', + placement_group_type: 'affinity:local', region: 'ca-central', }), ], diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx index ab53a8c8db3..d92abe920fd 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.stories.tsx @@ -1,8 +1,8 @@ import { Box } from '@linode/ui'; -import { sortByString } from '@linode/utilities'; +import { regions, sortByString } from '@linode/utilities'; import React, { useState } from 'react'; -import { regions } from 'src/__data__/regionsData'; +// @todo: modularization - Move `SelectedRegionsList` to the `/data` directory in the `@linode/shared` package. import { SelectedRegionsList } from 'src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/SelectedRegionsList'; import { RegionMultiSelect } from './RegionMultiSelect'; @@ -47,6 +47,7 @@ const meta: Meta = { currentCapability: 'Linodes', disabled: false, errorText: '', + flags: {}, isClearable: false, label: 'Regions', placeholder: 'Select Regions or type to search', diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx index ea64ef5929b..4e209b68b40 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.test.tsx @@ -1,12 +1,14 @@ -import { Region } from '@linode/api-v4'; +import { regionFactory } from '@linode/utilities'; import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; -import { regionFactory } from 'src/factories/regions'; +// @todo: modularization - Replace 'testHelpers' with 'testHelpers' from the shared package once available. import { renderWithTheme } from 'src/utilities/testHelpers'; import { RegionMultiSelect } from './RegionMultiSelect'; +import type { Region } from '@linode/api-v4'; + const regionNewark = regionFactory.build({ id: 'us-east', label: 'Newark, NJ', @@ -42,6 +44,7 @@ describe('RegionMultiSelect', () => { renderWithTheme( { renderWithTheme( { renderWithTheme( { /> )} currentCapability="Block Storage" + flags={{}} onChange={mockHandleSelection} regions={[regionNewark, regionAtlanta]} selectedIds={[regionNewark.id]} @@ -108,7 +114,7 @@ describe('RegionMultiSelect', () => { // Open the dropdown fireEvent.click(screen.getByRole('button', { name: 'Open' })); - // Check Newark chip shows becaused it is selected + // Check Newark chip shows because it is selected expect( screen.getByRole('listitem', { name: 'Newark, NJ', diff --git a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx index 1847f88c772..0c76b30cc9b 100644 --- a/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionMultiSelect.tsx @@ -1,21 +1,25 @@ +import { useAllAccountAvailabilitiesQuery } from '@linode/queries'; import { Autocomplete, Chip, Stack, StyledListItem } from '@linode/ui'; import CloseIcon from '@mui/icons-material/Close'; import React from 'react'; -import { Flag } from 'src/components/Flag'; -import { useAllAccountAvailabilitiesQuery } from '@linode/queries'; +// @todo: modularization - Move `getRegionCountryGroup` utility to `@linode/shared` package +// as it imports GLOBAL_QUOTA_VALUE from RegionSelect's constants.ts and update the import. import { getRegionCountryGroup } from 'src/utilities/formatRegion'; +// @todo: modularization - Move `Flag` component to `@linode/shared` package. +import { Flag } from '../Flag'; import { RegionOption } from './RegionOption'; import { StyledAutocompleteContainer } from './RegionSelect.styles'; import { getRegionOptions, isRegionOptionUnavailable, + useIsGeckoEnabled, } from './RegionSelect.utils'; import type { RegionMultiSelectProps } from './RegionSelect.types'; import type { Region } from '@linode/api-v4'; -import type { DisableItemOption } from 'src/components/ListItemOption'; +import type { DisableItemOption } from '@linode/ui'; interface RegionChipLabelProps { region: Region; @@ -37,6 +41,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { disabled, disabledRegions: disabledRegionsFromProps, errorText, + flags, forcefullyShownRegionIds, helperText, ignoreAccountAvailability, @@ -62,6 +67,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { forcefullyShownRegionIds, regions, }); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const selectedRegions = regionOptions.filter((r) => selectedIds.includes(r.id) @@ -122,6 +128,7 @@ export const RegionMultiSelect = React.memo((props: RegionMultiSelectProps) => { return ( { + isGeckoLAEnabled: boolean; +} export const RegionOption = ({ disabledOptions, + isGeckoLAEnabled, item, props, selected, -}: ListItemProps) => { - const { isGeckoLAEnabled } = useIsGeckoEnabled(); - +}: RegionOptionProps) => { return ( = { currentCapability: undefined, disabled: false, errorText: '', + flags: {}, helperText: '', label: 'Region', regions, diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx index 3f6721f2ac6..33b9ff82607 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { regionFactory } from 'src/factories'; +// @todo: modularization - Replace 'testHelpers' with 'testHelpers' from the shared package once available. import { renderWithTheme } from 'src/utilities/testHelpers'; import { RegionSelect } from './RegionSelect'; @@ -15,13 +16,14 @@ describe('RegionSelect', () => { currentCapability: 'Linodes', disabled: false, errorText: '', - onChange: vi.fn(), + flags: {}, helperText: '', label: '', + onChange: vi.fn(), regions, required: false, - value: '', tooltipText: '', + value: '', width: 100, }; diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.tsx index 4dce699e0e4..8591099cfd3 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.tsx @@ -1,23 +1,26 @@ +import { useAllAccountAvailabilitiesQuery } from '@linode/queries'; import { Autocomplete } from '@linode/ui'; import PublicIcon from '@mui/icons-material/Public'; import { createFilterOptions } from '@mui/material/Autocomplete'; import * as React from 'react'; -import { Flag } from 'src/components/Flag'; -import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; -import { useAllAccountAvailabilitiesQuery } from '@linode/queries'; +// @todo: modularization - Move `getRegionCountryGroup` utility to `@linode/shared` package +// as it imports GLOBAL_QUOTA_VALUE from RegionSelect's constants.ts and update the import. import { getRegionCountryGroup } from 'src/utilities/formatRegion'; +// @todo: modularization - Move `Flag` component to `@linode/shared` package. +import { Flag } from '../Flag'; import { RegionOption } from './RegionOption'; import { StyledAutocompleteContainer } from './RegionSelect.styles'; import { getRegionOptions, isRegionOptionUnavailable, + useIsGeckoEnabled, } from './RegionSelect.utils'; import type { RegionSelectProps } from './RegionSelect.types'; import type { Region } from '@linode/api-v4'; -import type { DisableItemOption } from 'src/components/ListItemOption'; +import type { DisableItemOption } from '@linode/ui'; /** * A specific select for regions. @@ -39,6 +42,7 @@ export const RegionSelect = < disabled, disabledRegions: disabledRegionsFromProps, errorText, + flags, forcefullyShownRegionIds, helperText, ignoreAccountAvailability, @@ -55,8 +59,7 @@ export const RegionSelect = < width, } = props; - const { isGeckoLAEnabled } = useIsGeckoEnabled(); - + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const { data: accountAvailability, isLoading: accountAvailabilityLoading, @@ -117,6 +120,7 @@ export const RegionSelect = < return ( ; + /** + * Feature Flags + */ + flags: FlagSet; /** * Used to override filtering done by the `currentCapability` prop * @todo Remove this after Object Storage Gen2. @@ -78,6 +83,10 @@ export interface RegionMultiSelectProps }>; currentCapability: Capabilities | undefined; disabledRegions?: Record; + /** + * Feature Flags + */ + flags: FlagSet; /** * Used to override filtering done by the `currentCapability` prop * @todo Remove this after Object Storage Gen2. diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx index 1681e0bb59b..e371deb03f1 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.test.tsx @@ -1,4 +1,4 @@ -import { accountAvailabilityFactory, regionFactory } from 'src/factories'; +import { accountAvailabilityFactory, regionFactory } from '@linode/utilities'; import { getRegionOptions, diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index 9fea41ec64d..460c6a453e9 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -1,7 +1,8 @@ import { CONTINENT_CODE_TO_CONTINENT } from '@linode/api-v4'; - -import { useFlags } from 'src/hooks/useFlags'; import { useRegionsQuery } from '@linode/queries'; + +// @todo: modularization - Move `getRegionCountryGroup` utility to `@linode/shared` package +// as it imports GLOBAL_QUOTA_VALUE from RegionSelect's constants.ts and update the import. import { getRegionCountryGroup } from 'src/utilities/formatRegion'; import type { @@ -9,7 +10,9 @@ import type { RegionFilterValue, } from './RegionSelect.types'; import type { AccountAvailability, Capabilities, Region } from '@linode/api-v4'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; +// @todo: modularization - Update type FlagSet to import from `@linode/shared` package once available. +import type { FlagSet } from 'src/featureFlags'; const NORTH_AMERICA = CONTINENT_CODE_TO_CONTINENT.NA; @@ -164,17 +167,19 @@ export const getIsDistributedRegion = ( return region?.site_type === 'distributed'; }; -export const useIsGeckoEnabled = () => { - const flags = useFlags(); - const isGeckoLA = flags?.gecko2?.enabled && flags.gecko2.la; - const isGeckoBeta = flags.gecko2?.enabled && !flags.gecko2?.la; +export const useIsGeckoEnabled = (flags: FlagSet) => { const { data: regions } = useRegionsQuery(); + const isGeckoLA = flags?.gecko2?.enabled && flags.gecko2.la; + const isGeckoBeta = flags?.gecko2?.enabled && !flags.gecko2?.la; + const hasDistributedRegionCapability = regions?.some((region: Region) => region.capabilities.includes('Distributed Plans') ); - const isGeckoLAEnabled = hasDistributedRegionCapability && isGeckoLA; - const isGeckoBetaEnabled = hasDistributedRegionCapability && isGeckoBeta; + const isGeckoLAEnabled = Boolean(hasDistributedRegionCapability && isGeckoLA); + const isGeckoBetaEnabled = Boolean( + hasDistributedRegionCapability && isGeckoBeta + ); return { isGeckoBetaEnabled, isGeckoLAEnabled }; }; diff --git a/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.test.tsx b/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.test.tsx index 1c82bc0abe2..bc0254826fd 100644 --- a/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.test.tsx +++ b/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.test.tsx @@ -1,7 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { accountTransferFactory, accountTransferNoResourceFactory, diff --git a/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.tsx b/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.tsx index d99221a5f9a..ee32a0ba7e2 100644 --- a/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.tsx +++ b/packages/manager/src/components/TransferDisplay/TransferDisplayDialog.tsx @@ -1,9 +1,10 @@ import { Box, Dialog, Divider, Typography } from '@linode/ui'; -import { styled } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles'; +import { styled } from '@mui/material/styles'; import * as React from 'react'; import { Link } from 'src/components/Link'; +import { useFlags } from 'src/hooks/useFlags'; import { useIsGeckoEnabled } from '../RegionSelect/RegionSelect.utils'; import { NETWORK_TRANSFER_USAGE_AND_COST_LINK } from './constants'; @@ -36,7 +37,8 @@ export const TransferDisplayDialog = React.memo( regionTransferPools, } = props; const theme = useTheme(); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const flags = useFlags(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const daysRemainingInMonth = getDaysRemaining(); const listOfOtherRegionTransferPools: string[] = diff --git a/packages/manager/src/components/TransferDisplay/utils.test.tsx b/packages/manager/src/components/TransferDisplay/utils.test.tsx index bd91d7a1ba4..61d5e8b1cce 100644 --- a/packages/manager/src/components/TransferDisplay/utils.test.tsx +++ b/packages/manager/src/components/TransferDisplay/utils.test.tsx @@ -1,8 +1,9 @@ -import { accountTransferFactory } from 'src/factories/account'; import { regionFactory, regionWithDynamicPricingFactory, -} from 'src/factories/regions'; +} from '@linode/utilities'; + +import { accountTransferFactory } from 'src/factories/account'; import { calculatePoolUsagePct, diff --git a/packages/manager/src/factories/index.ts b/packages/manager/src/factories/index.ts index ccfc6cc35e8..5698c3f11fe 100644 --- a/packages/manager/src/factories/index.ts +++ b/packages/manager/src/factories/index.ts @@ -1,6 +1,5 @@ export * from './account'; export * from './accountAgreements'; -export * from './accountAvailability'; export * from './accountLogin'; export * from './accountMaintenance'; export * from './accountOAuth'; @@ -39,7 +38,6 @@ export * from './placementGroups'; export * from './preferences'; export * from './profile'; export * from './promotionalOffer'; -export * from './regions'; export * from './stackscripts'; export * from './statusPage'; export * from './subnets'; diff --git a/packages/manager/src/features/Account/Quotas/Quotas.test.tsx b/packages/manager/src/features/Account/Quotas/Quotas.test.tsx index df347dfd92d..d90cef86c43 100644 --- a/packages/manager/src/features/Account/Quotas/Quotas.test.tsx +++ b/packages/manager/src/features/Account/Quotas/Quotas.test.tsx @@ -1,9 +1,9 @@ +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'; import * as React from 'react'; -import { regionFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { Quotas } from './Quotas'; diff --git a/packages/manager/src/features/Account/Quotas/Quotas.tsx b/packages/manager/src/features/Account/Quotas/Quotas.tsx index 60089bd7eba..91fd9fd6489 100644 --- a/packages/manager/src/features/Account/Quotas/Quotas.tsx +++ b/packages/manager/src/features/Account/Quotas/Quotas.tsx @@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom'; import { DocsLink } from 'src/components/DocsLink/DocsLink'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { QuotasTable } from './QuotasTable'; import { useGetLocationsForQuotaService } from './utils'; @@ -15,6 +16,7 @@ import type { SelectOption } from '@linode/ui'; import type { Theme } from '@mui/material'; export const Quotas = () => { + const flags = useFlags(); const history = useHistory(); const [selectedService, setSelectedService] = React.useState< SelectOption @@ -110,6 +112,7 @@ export const Quotas = () => { currentCapability={undefined} disableClearable disabled={isFetchingLocations} + flags={flags} loading={isFetchingLocations} noOptionsText={`No resource found for ${selectedService.label}`} regions={regions ?? []} diff --git a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.test.ts b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.test.ts index ce429358e8d..4bb008048f8 100644 --- a/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.test.ts +++ b/packages/manager/src/features/Billing/PdfGenerator/PdfGenerator.test.ts @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import fs from 'fs'; import { PdfReader } from 'pdfreader'; @@ -6,7 +7,6 @@ import { invoiceFactory, invoiceItemFactory, paymentFactory, - regionFactory, } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { formatDate } from 'src/utilities/formatDate'; diff --git a/packages/manager/src/features/Billing/PdfGenerator/utils.test.ts b/packages/manager/src/features/Billing/PdfGenerator/utils.test.ts index c69336a539b..943f8580518 100644 --- a/packages/manager/src/features/Billing/PdfGenerator/utils.test.ts +++ b/packages/manager/src/features/Billing/PdfGenerator/utils.test.ts @@ -1,11 +1,13 @@ -import { invoiceItemFactory, regionFactory } from 'src/factories'; +import { regionFactory } from '@linode/utilities'; + +import { ADDRESSES } from 'src/constants'; +import { invoiceItemFactory } from 'src/factories'; import { getInvoiceRegion, getRemitAddress, invoiceCreatedAfterDCPricingLaunch, } from './utils'; -import { ADDRESSES } from 'src/constants'; describe('getInvoiceRegion', () => { it('should get a formatted label given invoice items and regions', () => { diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx index e99ca26e930..02762f651e2 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx @@ -1,10 +1,10 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; import { alertFactory, linodeFactory, notificationChannelFactory, - regionFactory, serviceTypesFactory, } from 'src/factories/'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.test.tsx index 34651adcc73..9532552fe2e 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.test.tsx @@ -1,7 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { AlertsRegionFilter } from './AlertsRegionFilter'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.tsx index f200dfb19ad..0a299e513ba 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsRegionFilter.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect'; +import { useFlags } from 'src/hooks/useFlags'; import type { Region } from '@linode/api-v4'; @@ -19,6 +20,7 @@ export const AlertsRegionFilter = React.memo((props: AlertsRegionProps) => { const { handleSelectionChange, regionOptions } = props; const [selectedRegion, setSelectedRegion] = React.useState([]); + const flags = useFlags(); const handleRegionChange = React.useCallback( (regionIds: string[]) => { @@ -46,6 +48,7 @@ export const AlertsRegionFilter = React.memo((props: AlertsRegionProps) => { }} currentCapability={undefined} // this is a required property, no specific capability required here disableSelectAll + flags={flags} isClearable label="Select Regions" limitTags={1} diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx index 1e59a95f23f..418605f6657 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx @@ -1,8 +1,9 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory, regionFactory } from 'src/factories'; +import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { AlertResources } from './AlertsResources'; @@ -192,7 +193,7 @@ describe('AlertResources component tests', () => { it('should handle selection correctly and publish', async () => { const handleResourcesSelection = vi.fn(); - const { getByTestId, queryByTestId, getByText } = renderWithTheme( + const { getByTestId, getByText, queryByTestId } = renderWithTheme( { const { name } = props; const { data: regions, isError, isLoading } = useRegionsQuery(); const { control } = useFormContext(); + const flags = useFlags(); + return ( ( @@ -30,6 +33,7 @@ export const CloudPulseRegionSelect = (props: CloudViewRegionSelectProps) => { field.onChange(value?.id); }} currentCapability={undefined} + flags={flags} fullWidth label="Region" loading={isLoading} diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx index d7d2fcb985b..f6f2dc56d63 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { alertFactory, linodeFactory, regionFactory } from 'src/factories'; +import { alertFactory, linodeFactory } from 'src/factories'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; import { CloudPulseModifyAlertResources } from './CloudPulseModifyAlertResources'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx index 90e884ead4c..318826ca7bc 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx @@ -1,10 +1,11 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryHistory } from 'history'; import React from 'react'; import { Router } from 'react-router-dom'; -import { alertFactory, linodeFactory, regionFactory } from 'src/factories'; +import { alertFactory, linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { EditAlertResources } from './EditAlertResources'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/Utils/AlertResourceUtils.test.ts b/packages/manager/src/features/CloudPulse/Alerts/Utils/AlertResourceUtils.test.ts index 4d286c25d4f..b166d62d038 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/Utils/AlertResourceUtils.test.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/Utils/AlertResourceUtils.test.ts @@ -1,4 +1,4 @@ -import { regionFactory } from 'src/factories'; +import { regionFactory } from '@linode/utilities'; import { getFilteredResources, diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx index c087921ed8e..569504a2859 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { dashboardFactory, regionFactory } from 'src/factories'; +import { dashboardFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { DBAAS_CAPABILITY, LINODE_CAPABILITY } from '../Utils/FilterConfig'; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx index 41171287355..669c2e45f62 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseRegionSelect.tsx @@ -1,8 +1,8 @@ +import { useRegionsQuery } from '@linode/queries'; import * as React from 'react'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { useFlags } from 'src/hooks/useFlags'; -import { useRegionsQuery } from '@linode/queries'; import { FILTER_CONFIG } from '../Utils/FilterConfig'; @@ -24,10 +24,6 @@ export interface CloudPulseRegionSelectProps { export const CloudPulseRegionSelect = React.memo( (props: CloudPulseRegionSelectProps) => { - const { data: regions, isError, isLoading } = useRegionsQuery(); - - const flags = useFlags(); - const { defaultValue, handleRegionChange, @@ -37,6 +33,10 @@ export const CloudPulseRegionSelect = React.memo( selectedDashboard, } = props; + const { data: regions, isError, isLoading } = useRegionsQuery(); + + const flags = useFlags(); + const serviceType: string | undefined = selectedDashboard?.service_type; const capability = serviceType ? FILTER_CONFIG.get(serviceType)?.capability @@ -93,6 +93,7 @@ export const CloudPulseRegionSelect = React.memo( disableClearable={false} disabled={!selectedDashboard || !regions} errorText={isError ? `Failed to fetch ${label || 'Regions'}.` : ''} + flags={flags} fullWidth label={label || 'Region'} loading={isLoading} diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx index 000292b902e..4f251e3adde 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx @@ -9,6 +9,7 @@ import { StyledTextField, } from 'src/features/Databases/DatabaseCreate/DatabaseCreate.style'; import { DatabaseEngineSelect } from 'src/features/Databases/DatabaseCreate/DatabaseEngineSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import type { @@ -51,6 +52,7 @@ export const DatabaseClusterData = (props: Props) => { const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_databases', }); + const flags = useFlags(); const labelToolTip = ( @@ -93,6 +95,7 @@ export const DatabaseClusterData = (props: Props) => { disableClearable disabled={isRestricted} errorText={errors.region} + flags={flags} onChange={(e, region) => onChange('region', region.id)} regions={regionsData} value={values.region} diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx index 454abcdbfb2..a27429a7f47 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx @@ -1,8 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { databaseFactory, databaseTypeFactory } from 'src/factories/databases'; -import { regionFactory } from 'src/factories/regions'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { DatabaseSummaryClusterConfiguration } from './DatabaseSummaryClusterConfiguration'; diff --git a/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.test.tsx b/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.test.tsx index 48d3b49d19f..07a55e4ff1a 100644 --- a/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.test.tsx +++ b/packages/manager/src/features/GlobalNotifications/RegionStatusBanner.test.tsx @@ -1,9 +1,9 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { regionFactory } from 'src/factories/regions'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { RegionStatusBanner } from './RegionStatusBanner'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx index 5c67f4c31c0..454d63aa156 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx @@ -1,13 +1,9 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import React from 'react'; -import { - imageFactory, - linodeDiskFactory, - linodeFactory, - regionFactory, -} from 'src/factories'; +import { imageFactory, linodeDiskFactory, linodeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx index 46a2794842b..16ce637bad0 100644 --- a/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/ImageUpload.tsx @@ -267,6 +267,7 @@ export const ImageUpload = () => { currentCapability="Object Storage" // Images use Object Storage as their storage backend disableClearable errorText={fieldState.error?.message} + flags={flags} ignoreAccountAvailability label="Region" onChange={(e, region) => field.onChange(region.id)} diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.test.tsx index da58325b963..21c0167d16f 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ImageRegionRow.test.tsx @@ -1,7 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { regionFactory } from 'src/factories/regions'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx index 0d6f2124885..0a87e5d6899 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { imageFactory, regionFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx index 383d14dfdda..ff886d5fbe5 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImageRegions/ManageImageRegionsForm.tsx @@ -1,3 +1,4 @@ +import { useRegionsQuery } from '@linode/queries'; import { ActionsPanel, Notice, Paper, Stack, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import React from 'react'; @@ -5,8 +6,8 @@ import { useForm } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { useUpdateImageRegionsMutation } from 'src/queries/images'; -import { useRegionsQuery } from '@linode/queries'; import { ImageRegionRow } from './ImageRegionRow'; @@ -16,8 +17,8 @@ import type { Region, UpdateImageRegionsPayload, } from '@linode/api-v4'; +import type { DisableItemOption } from '@linode/ui'; import type { Resolver } from 'react-hook-form'; -import type { DisableItemOption } from 'src/components/ListItemOption'; interface Props { image: Image | undefined; @@ -31,6 +32,8 @@ interface Context { export const ManageImageReplicasForm = (props: Props) => { const { image, onClose } = props; + const flags = useFlags(); + const imageRegionIds = image?.regions.map(({ region }) => region) ?? []; const { enqueueSnackbar } = useSnackbar(); @@ -115,6 +118,7 @@ export const ManageImageReplicasForm = (props: Props) => { currentCapability="Object Storage" // Images use Object Storage as the storage backend disabledRegions={disabledRegions} errorText={errors.regions?.message} + flags={flags} ignoreAccountAvailability // Ignore the account capability because we are just using "Object Storage" for region compatibility label="Add Regions" placeholder="Select regions or type to search" diff --git a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.test.tsx b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.test.tsx index d8ba2d4ea45..a79bcc91d48 100644 --- a/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.test.tsx +++ b/packages/manager/src/features/Kubernetes/ClusterList/KubernetesClusterRow.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { render } from '@testing-library/react'; import * as React from 'react'; -import { kubernetesClusterFactory, regionFactory } from 'src/factories'; +import { kubernetesClusterFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { wrapWithTableBody, wrapWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 3fbb9cc1bd2..5a977a5e4d3 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -36,6 +36,7 @@ import { useIsLkeEnterpriseEnabled, useLkeStandardOrEnterpriseVersions, } from 'src/features/Kubernetes/kubeUtils'; +import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useCreateKubernetesClusterBetaMutation, @@ -78,6 +79,7 @@ import type { APIError } from '@linode/api-v4/lib/types'; import type { ExtendedIP } from 'src/utilities/ipUtils'; export const CreateCluster = () => { + const flags = useFlags(); const { classes } = useStyles(); const [selectedRegion, setSelectedRegion] = React.useState< Region | undefined @@ -430,6 +432,7 @@ export const CreateCluster = () => { disableClearable disabled={isCreateClusterRestricted} errorText={errorMap.region} + flags={flags} onChange={(e, region) => setSelectedRegion(region)} regions={regionsData} value={selectedRegion?.id} diff --git a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.test.tsx b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.test.tsx index 632b192b026..c8907f87587 100644 --- a/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubeCheckoutBar/KubeCheckoutBar.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { regionFactory, typeFactory } from 'src/factories'; +import { typeFactory } from 'src/factories'; import { nodePoolFactory } from 'src/factories/kubernetesCluster'; import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { LKE_CREATE_CLUSTER_CHECKOUT_MESSAGE } from 'src/utilities/pricing/constants'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.test.tsx index 2199037af5e..b7a738f560f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Addons.test.tsx @@ -1,6 +1,6 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.test.tsx index 227568b9ce6..537aa716d70 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/Backups.test.tsx @@ -1,11 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { - accountSettingsFactory, - profileFactory, - regionFactory, -} from 'src/factories'; +import { accountSettingsFactory, profileFactory } from 'src/factories'; import { grantsFactory } from 'src/factories/grants'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.test.tsx index 9f5ba35f0c9..a57756aee8a 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Addons/PrivateIP.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { profileFactory, regionFactory } from 'src/factories'; +import { profileFactory } from 'src/factories'; import { grantsFactory } from 'src/factories/grants'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Details/PlacementGroupPanel.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Details/PlacementGroupPanel.test.tsx index db7616cbed8..bd499104896 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Details/PlacementGroupPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Details/PlacementGroupPanel.test.tsx @@ -1,6 +1,6 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/EUAgreement.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/EUAgreement.test.tsx index fd4f89a0519..a9d01a7bc80 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/EUAgreement.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/EUAgreement.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { accountAgreementsFactory, regionFactory } from 'src/factories'; +import { accountAgreementsFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx index bb30f57896b..1aaabaef4bb 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import React from 'react'; @@ -8,7 +9,6 @@ import { linodeFactory, linodeTypeFactory, profileFactory, - regionFactory, } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx index 0521039ed22..6a850a93e94 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.tsx @@ -83,7 +83,7 @@ export const Region = React.memo(() => { const { data: regions } = useRegionsQuery(); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const showTwoStepRegion = isGeckoLAEnabled && isDistributedRegionSupported(params.type ?? 'OS'); @@ -252,6 +252,7 @@ export const Region = React.memo(() => { disabled={isLinodeCreateRestricted} disabledRegions={disabledRegions} errorText={fieldState.error?.message} + flags={flags} onChange={(e, region) => onChange(region)} regions={regions ?? []} textFieldProps={{ onBlur: field.onBlur }} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.test.ts b/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.test.ts index cfc1ef5db54..bd7c8e36fc7 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.test.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.test.ts @@ -1,4 +1,6 @@ -import { imageFactory, regionFactory } from 'src/factories'; +import { regionFactory } from '@linode/utilities'; + +import { imageFactory } from 'src/factories'; import { getDisabledRegions } from './Region.utils'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.ts b/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.ts index 52bbf1b35d9..af7c2cca5cd 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.utils.ts @@ -1,5 +1,5 @@ import type { Image, Region } from '@linode/api-v4'; -import type { DisableItemOption } from 'src/components/ListItemOption'; +import type { DisableItemOption } from '@linode/ui'; interface DisabledRegionOptions { regions: Region[]; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Security.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Security.test.tsx index 988f665f38b..3938483b0cc 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Security.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Security.test.tsx @@ -1,12 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { - accountFactory, - profileFactory, - regionFactory, - sshKeyFactory, -} from 'src/factories'; +import { accountFactory, profileFactory, sshKeyFactory } from 'src/factories'; import { grantsFactory } from 'src/factories/grants'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.test.tsx index 156dad3ed8d..3a1136ac010 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Summary/Summary.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { imageFactory, regionFactory, typeFactory } from 'src/factories'; +import { imageFactory, typeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/TwoStepRegion.tsx b/packages/manager/src/features/Linodes/LinodeCreate/TwoStepRegion.tsx index 496821cb342..d0ba308fd6f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/TwoStepRegion.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/TwoStepRegion.tsx @@ -1,3 +1,4 @@ +import { useRegionsQuery } from '@linode/queries'; import { Autocomplete, Box, Paper, Typography } from '@linode/ui'; import * as React from 'react'; @@ -9,7 +10,7 @@ import { Tab } from 'src/components/Tabs/Tab'; import { TabList } from 'src/components/Tabs/TabList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; -import { useRegionsQuery } from '@linode/queries'; +import { useFlags } from 'src/hooks/useFlags'; import { sendLinodeCreateDocsEvent } from 'src/utilities/analytics/customEventAnalytics'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; import { DOCS_LINK_LABEL_DC_PRICING } from 'src/utilities/pricing/constants'; @@ -73,9 +74,10 @@ export const TwoStepRegion = (props: CombinedProps) => { const { data: regions } = useRegionsQuery(); const { params } = useLinodeCreateQueryParams(); + const flags = useFlags(); return ( - + Region { disabled={disabled} disabledRegions={disabledRegions} errorText={errorText} + flags={flags} onChange={(e, region) => onChange(region)} regionFilter="core" regions={regions ?? []} @@ -140,6 +143,7 @@ export const TwoStepRegion = (props: CombinedProps) => { disabled={disabled} disabledRegions={disabledRegions} errorText={errorText} + flags={flags} onChange={(e, region) => onChange(region)} regionFilter={regionFilter} regions={regions ?? []} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.test.tsx index b98c7e924bd..80601ccb056 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserData.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { imageFactory, regionFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserDataHeading.tsx b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserDataHeading.tsx index b3103192789..8e4a4325aec 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserDataHeading.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/UserData/UserDataHeading.tsx @@ -5,7 +5,7 @@ import { Link } from 'src/components/Link'; import { useLinodeCreateQueryParams } from '../utilities'; -import type { LinodeCreateType } from '../types'; +import type { LinodeCreateType } from '@linode/utilities'; export const UserDataHeading = () => { const { params } = useLinodeCreateQueryParams(); diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.test.tsx index 80f9b2193dc..222ad4bc6e1 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.test.tsx @@ -1,7 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.test.tsx index eb193d24815..1c02510da41 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.test.tsx @@ -1,6 +1,6 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts b/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts index b4095fd6905..3bd13dea259 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/resolvers.ts @@ -13,11 +13,11 @@ import { } from './schemas'; import { getInterfacesPayload } from './utilities'; -import type { LinodeCreateType } from './types'; import type { LinodeCreateFormContext, LinodeCreateFormValues, } from './utilities'; +import type { LinodeCreateType } from '@linode/utilities'; import type { QueryClient } from '@tanstack/react-query'; import type { FieldErrors, Resolver } from 'react-hook-form'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx index 085c3f6d85f..2e10389b690 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx @@ -1,12 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { - imageFactory, - linodeFactory, - regionFactory, - typeFactory, -} from 'src/factories'; +import { imageFactory, linodeFactory, typeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts index 187b44eb538..66081f3fafc 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.ts @@ -23,7 +23,6 @@ import { getDefaultUDFData } from './Tabs/StackScripts/UserDefinedFields/utiliti import type { LinodeCreateInterface } from './Networking/utilities'; import type { StackScriptTabType } from './Tabs/StackScripts/utilities'; -import type { LinodeCreateType } from './types'; import type { AccountSettings, CreateLinodeRequest, @@ -33,6 +32,7 @@ import type { Profile, StackScript, } from '@linode/api-v4'; +import type { LinodeCreateType } from '@linode/utilities'; import type { QueryClient } from '@tanstack/react-query'; import type { FieldErrors } from 'react-hook-form'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx index 2ab7837e1a9..767a5ef4671 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; import { DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY } from 'src/components/Encryption/constants'; -import { linodeFactory, regionFactory } from 'src/factories'; +import { linodeFactory } from 'src/factories'; import { typeFactory } from 'src/factories/types'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx index de7cd04dfe4..cad53f7fbe1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx @@ -1,10 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import React from 'react'; -import { - accountTransferFactory, - linodeTransferFactory, - regionFactory, -} from 'src/factories'; +import { accountTransferFactory, linodeTransferFactory } from 'src/factories'; import { typeFactory } from 'src/factories/types'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx index cca3fd1146b..a283bc8dc04 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkingSummaryPanel.tsx @@ -1,10 +1,11 @@ +import { useLinodeQuery } from '@linode/queries'; import { Paper } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; -import { useLinodeQuery } from '@linode/queries'; +import { useFlags } from 'src/hooks/useFlags'; import { DNSResolvers } from './DNSResolvers'; import { NetworkTransfer } from './NetworkTransfer'; @@ -15,10 +16,11 @@ interface Props { } export const LinodeNetworkingSummaryPanel = React.memo((props: Props) => { + const flags = useFlags(); + const theme = useTheme(); // @todo maybe move this query closer to the consuming component const { data: linode } = useLinodeQuery(props.linodeId); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); - const theme = useTheme(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); if (!linode) { return null; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.test.tsx index 74e9117610c..7357606aeb0 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { accountFactory, regionFactory } from 'src/factories'; +import { accountFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx index 1c6dd625ca2..2843571e404 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayGroupedLinodes.tsx @@ -16,6 +16,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; +import { useFlags } from 'src/hooks/useFlags'; import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; import { @@ -94,7 +95,9 @@ export const DisplayGroupedLinodes = (props: DisplayGroupedLinodesProps) => { return acc; }, 0); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const flags = useFlags(); + + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); if (display === 'grid') { return ( diff --git a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx index 521aec2df9b..d8142cc35aa 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/DisplayLinodes.tsx @@ -1,5 +1,4 @@ -import { Box, CircleProgress, Paper, Tooltip } from '@linode/ui'; -import { IconButton } from '@linode/ui'; +import { Box, CircleProgress, IconButton, Paper, Tooltip } from '@linode/ui'; import { getQueryParamsFromQueryString } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; @@ -13,6 +12,7 @@ import { getMinimumPageSizeForNumberOfItems } from 'src/components/PaginationFoo import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { TableBody } from 'src/components/TableBody'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; +import { useFlags } from 'src/hooks/useFlags'; import { useInfinitePageSize } from 'src/hooks/useInfinitePageSize'; import { StyledControlHeader } from './DisplayLinodes.styles'; @@ -82,6 +82,7 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { const displayViewDescriptionId = React.useId(); const groupByDescriptionId = React.useId(); const { infinitePageSize, setInfinitePageSize } = useInfinitePageSize(); + const flags = useFlags(); const numberOfLinodesWithMaintenance = React.useMemo(() => { return data.reduce((acc, thisLinode) => { @@ -104,7 +105,7 @@ export const DisplayLinodes = React.memo((props: DisplayLinodesProps) => { const params = getQueryParamsFromQueryString(search); const queryPage = Math.min(Number(params.page), maxPageNumber) || 1; - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); return ( { currentCapability="Linodes" disableClearable errorText={errorText} + flags={flags} label="New Region" onChange={(e, region) => handleSelectRegion(region.id)} value={selectedRegion} diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 0be63223a2c..de57d6ebe58 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -1,13 +1,14 @@ +import { + useAllAccountMaintenanceQuery, + useAllLinodesQuery, +} from '@linode/queries'; import { createLazyRoute } from '@tanstack/react-router'; import React from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; import { useIsGeckoEnabled } from 'src/components/RegionSelect/RegionSelect.utils'; import { SuspenseLoader } from 'src/components/SuspenseLoader'; -import { - useAllAccountMaintenanceQuery, - useAllLinodesQuery, -} from '@linode/queries'; +import { useFlags } from 'src/hooks/useFlags'; import { useInProgressEvents } from 'src/queries/events/events'; import { addMaintenanceToLinodes } from 'src/utilities/linodes'; import { storage } from 'src/utilities/storage'; @@ -54,8 +55,9 @@ export const LinodesLandingWrapper = React.memo(() => { {}, PENDING_MAINTENANCE_FILTER ); + const flags = useFlags(); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const [regionFilter, setRegionFilter] = React.useState< RegionFilter | undefined diff --git a/packages/manager/src/features/Linodes/types.ts b/packages/manager/src/features/Linodes/types.ts index 39718d3acee..a55e038b791 100644 --- a/packages/manager/src/features/Linodes/types.ts +++ b/packages/manager/src/features/Linodes/types.ts @@ -1,5 +1,4 @@ -import type { LinodeCreateType } from './LinodeCreate/types'; -import type { BaseQueryParams } from '@linode/utilities'; +import type { BaseQueryParams, LinodeCreateType } from '@linode/utilities'; export type DialogType = | 'delete' diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index fe520fdb23c..f90e8830745 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -37,6 +37,7 @@ import { RegionHelperText } from 'src/components/SelectRegionPanel/RegionHelperT import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { FIREWALL_GET_STARTED_LINK } from 'src/constants'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { sendCreateNodeBalancerEvent } from 'src/utilities/analytics/customEventAnalytics'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -97,6 +98,7 @@ const defaultFieldsStates = { }; const NodeBalancerCreate = () => { + const flags = useFlags(); const navigate = useNavigate(); const { data: agreements } = useAccountAgreements(); const { data: profile } = useProfile(); @@ -543,6 +545,7 @@ const NodeBalancerCreate = () => { currentCapability="NodeBalancers" disableClearable errorText={hasErrorFor('region')} + flags={flags} noMarginTop onChange={(e, region) => regionChange(region?.id ?? '')} regions={regions ?? []} diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx index 384e504365c..350c5d72bb7 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyRegions/AccessKeyRegions.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { RegionMultiSelect } from 'src/components/RegionSelect/RegionMultiSelect'; import { useObjectStorageRegions } from 'src/features/ObjectStorage/hooks/useObjectStorageRegions'; +import { useFlags } from 'src/hooks/useFlags'; import { useIsObjectStorageGen2Enabled } from '../../hooks/useIsObjectStorageGen2Enabled'; import { WHITELISTED_REGIONS } from '../../utilities'; @@ -25,6 +26,7 @@ const sortRegionOptions = (a: Region, b: Region) => { export const AccessKeyRegions = (props: Props) => { const { disabled, error, onChange, required, selectedRegion } = props; + const flags = useFlags(); const { allRegionsError, availableStorageRegions, @@ -46,6 +48,7 @@ export const AccessKeyRegions = (props: Props) => { currentCapability="Object Storage" disabled={disabled} errorText={errorText} + flags={flags} isClearable={false} label="Regions" onChange={onChange} diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx index 63a1a81ef19..f093cacf825 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/AccessKeyTable/HostNameTableCell.test.tsx @@ -1,8 +1,9 @@ +import { regionFactory } from '@linode/utilities'; import '@testing-library/jest-dom'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { objectStorageKeyFactory, regionFactory } from 'src/factories'; +import { objectStorageKeyFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx index 89bb66640a1..7ad193597b8 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesDrawer.test.tsx @@ -1,8 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { regionFactory } from 'src/factories/regions'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { HostNamesDrawer } from './HostNamesDrawer'; diff --git a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesList.test.tsx b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesList.test.tsx index 115cc71f937..a6facc18c09 100644 --- a/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesList.test.tsx +++ b/packages/manager/src/features/ObjectStorage/AccessKeyLanding/HostNamesList.test.tsx @@ -1,9 +1,9 @@ +import { regionFactory } from '@linode/utilities'; import { screen } from '@testing-library/react'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { HostNamesList } from './HostNamesList'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx index fbb0c7df5a7..239b43adc71 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketDetailsDrawer.test.tsx @@ -1,4 +1,8 @@ -import { readableBytes, truncateMiddle } from '@linode/utilities'; +import { + readableBytes, + regionFactory, + truncateMiddle, +} from '@linode/utilities'; import { screen, waitFor } from '@testing-library/react'; import React from 'react'; import { vi } from 'vitest'; @@ -7,7 +11,6 @@ import { objectStorageBucketFactory, objectStorageBucketFactoryGen2, profileFactory, - regionFactory, } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.test.tsx index a6eeaca8f4f..de0ea605363 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.test.tsx @@ -1,7 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import { screen } from '@testing-library/react'; import React from 'react'; -import { regionFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { BucketRegions } from './BucketRegions'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx index 36f85d91bf5..13a79f4cf23 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/BucketRegions.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { useObjectStorageRegions } from 'src/features/ObjectStorage/hooks/useObjectStorageRegions'; +import { useFlags } from 'src/hooks/useFlags'; + import { useIsObjectStorageGen2Enabled } from '../hooks/useIsObjectStorageGen2Enabled'; import { WHITELISTED_REGIONS } from '../utilities'; @@ -24,6 +26,8 @@ export const BucketRegions = (props: Props) => { const { isObjectStorageGen2Enabled } = useIsObjectStorageGen2Enabled(); + const flags = useFlags(); + // Error could be: 1. General Regions error, 2. Field error, 3. Nothing const errorText = error || allRegionsError?.[0]?.reason; @@ -36,6 +40,7 @@ export const BucketRegions = (props: Props) => { disableClearable disabled={disabled} errorText={errorText} + flags={flags} label="Region" onBlur={onBlur} onChange={(e, region) => onChange(region.id)} diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx index e3215ae4c62..04ab044c216 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/ClusterSelect.tsx @@ -2,6 +2,7 @@ import { useRegionsQuery } from '@linode/queries'; import * as React from 'react'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { useObjectStorageClusters } from 'src/queries/object-storage/queries'; import type { Region } from '@linode/api-v4/lib/regions'; @@ -28,6 +29,8 @@ export const ClusterSelect: React.FC = (props) => { const { data: clusters, error: clustersError } = useObjectStorageClusters(); const { data: regions } = useRegionsQuery(); + const flags = useFlags(); + const regionOptions = clusters?.reduce((acc, cluster) => { const region = regions?.find((r) => r.id === cluster.region); if (region) { @@ -50,6 +53,7 @@ export const ClusterSelect: React.FC = (props) => { disableClearable disabled={disabled} errorText={errorText} + flags={flags} label="Region" onBlur={onBlur} onChange={(e, region) => onChange(region.id)} diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx index f01236e1113..4a73de1f4fd 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.test.tsx @@ -1,3 +1,4 @@ +import { regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; @@ -5,7 +6,6 @@ import * as React from 'react'; import { accountSettingsFactory, objectStorageClusterFactory, - regionFactory, } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx index afaa30e96ad..efa6105d883 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx @@ -1,11 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { - linodeFactory, - placementGroupFactory, - regionFactory, -} from 'src/factories'; +import { linodeFactory, placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsAssignLinodesDrawer } from './PlacementGroupsAssignLinodesDrawer'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx index 81dcae30e01..262f07b729e 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsCreateDrawer.tsx @@ -31,6 +31,7 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { NotFound } from 'src/components/NotFound'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { useFlags } from 'src/hooks/useFlags'; import { sendLinodeCreateFormStepEvent } from 'src/utilities/analytics/formEventAnalytics'; import { getFormikErrorsFromAPIErrors } from 'src/utilities/formikErrorUtils'; @@ -42,15 +43,15 @@ import { hasRegionReachedPlacementGroupCapacity, } from './utils'; -import type { LinodeCreateType } from '../Linodes/LinodeCreate/types'; import type { PlacementGroupsCreateDrawerProps } from './types'; import type { CreatePlacementGroupPayload, PlacementGroup, Region, } from '@linode/api-v4'; +import type { DisableItemOption } from '@linode/ui'; +import type { LinodeCreateType } from '@linode/utilities'; import type { FormikHelpers } from 'formik'; -import type { DisableItemOption } from 'src/components/ListItemOption'; export const PlacementGroupsCreateDrawer = ( props: PlacementGroupsCreateDrawerProps @@ -62,6 +63,7 @@ export const PlacementGroupsCreateDrawer = ( open, selectedRegionId, } = props; + const flags = useFlags(); const { data: regions } = useRegionsQuery(); const { data: allPlacementGroupsInRegion } = useAllPlacementGroupsQuery({ enabled: Boolean(selectedRegionId), @@ -265,6 +267,7 @@ export const PlacementGroupsCreateDrawer = ( currentCapability="Placement Group" disableClearable disabledRegions={disabledRegions} + flags={flags} helperText={values.region && pgRegionLimitHelperText} onChange={(e, region) => handleRegionSelect(region.id)} regions={regions ?? []} diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx index ea98bd86674..b827b445d98 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx @@ -1,10 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { - linodeFactory, - placementGroupFactory, - regionFactory, -} from 'src/factories'; +import { linodeFactory, placementGroupFactory } from 'src/factories'; import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; import { PlacementGroupsDetail } from './PlacementGroupsDetail'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.test.tsx index 27922913f5c..16b1f00d0bf 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsSummary/PlacementGroupsSummary.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { placementGroupFactory, regionFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsSummary } from './PlacementGroupsSummary'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.test.tsx index eff250d88e4..4f8bf2fbefe 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetailPanel.test.tsx @@ -1,6 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { placementGroupFactory, regionFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsDetailPanel } from './PlacementGroupsDetailPanel'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx index 11ce5ae6be1..f0f4b1afccd 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsEditDrawer.test.tsx @@ -1,7 +1,8 @@ +import { regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { placementGroupFactory, regionFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsEditDrawer } from './PlacementGroupsEditDrawer'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx index 7b5fc4f1a8b..b5c159373c5 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx @@ -1,10 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { - linodeFactory, - placementGroupFactory, - regionFactory, -} from 'src/factories'; +import { linodeFactory, placementGroupFactory } from 'src/factories'; import { renderWithTheme, resizeScreenSize, diff --git a/packages/manager/src/features/PlacementGroups/utils.test.ts b/packages/manager/src/features/PlacementGroups/utils.test.ts index c50f7c351ec..63a0d4061f2 100644 --- a/packages/manager/src/features/PlacementGroups/utils.test.ts +++ b/packages/manager/src/features/PlacementGroups/utils.test.ts @@ -1,10 +1,7 @@ +import { regionFactory } from '@linode/utilities'; import { renderHook } from '@testing-library/react'; -import { - linodeFactory, - placementGroupFactory, - regionFactory, -} from 'src/factories'; +import { linodeFactory, placementGroupFactory } from 'src/factories'; import { getLinodesFromAllPlacementGroups, diff --git a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx index 9f3c15074ec..64a6b3a6816 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx @@ -15,7 +15,7 @@ import { } from './VPCCreateForm.styles'; import type { CreateVPCPayload } from '@linode/api-v4'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; import type { LinodeCreateQueryParams } from 'src/features/Linodes/types'; interface Props { diff --git a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx index ff986f11b1c..3d7fd33e3c4 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx @@ -6,14 +6,15 @@ import { useLocation } from 'react-router-dom'; import { Link } from 'src/components/Link'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; import { VPC_CREATE_FORM_VPC_HELPER_TEXT } from '../../constants'; import { StyledBodyTypography } from './VPCCreateForm.styles'; -import type { Region } from '@linode/api-v4'; import type { CreateVPCPayload } from '@linode/api-v4'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { Region } from '@linode/api-v4'; +import type { LinodeCreateType } from '@linode/utilities'; import type { LinodeCreateQueryParams } from 'src/features/Linodes/types'; interface Props { @@ -25,6 +26,7 @@ interface Props { export const VPCTopSectionContent = (props: Props) => { const { disabled, isDrawer, regions } = props; const location = useLocation(); + const flags = useFlags(); const isFromLinodeCreate = location.pathname.includes('/linodes/create'); const queryParams = getQueryParamsFromQueryString( location.search @@ -59,6 +61,7 @@ export const VPCTopSectionContent = (props: Props) => { currentCapability="VPCs" disabled={isDrawer ? true : disabled} errorText={fieldState.error?.message} + flags={flags} onBlur={field.onBlur} onChange={(_, region) => field.onChange(region?.id ?? '')} regions={regions} diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index f94cfc1e712..4b42d252d68 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -12,6 +12,7 @@ import { Controller, useForm } from 'react-hook-form'; import { NotFound } from 'src/components/NotFound'; import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; +import { useFlags } from 'src/hooks/useFlags'; import type { UpdateVPCPayload, VPC } from '@linode/api-v4'; @@ -26,6 +27,7 @@ const REGION_HELPER_TEXT = 'Region cannot be changed during beta.'; export const VPCEditDrawer = (props: Props) => { const { onClose, open, vpc } = props; + const flags = useFlags(); const { data: profile } = useProfile(); const { data: grants } = useGrants(); @@ -131,6 +133,7 @@ export const VPCEditDrawer = (props: Props) => { currentCapability="VPCs" disabled // the Region field will not be editable during beta errorText={(regionsError && regionsError[0].reason) || undefined} + flags={flags} helperText={REGION_HELPER_TEXT} onChange={() => null} regions={regionsData} diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index eeb10198f8a..07ff836f15f 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -45,6 +45,7 @@ import { MAX_VOLUME_SIZE } from 'src/constants'; import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; +import { useFlags } from 'src/hooks/useFlags'; import { sendCreateVolumeEvent } from 'src/utilities/analytics/customEventAnalytics'; import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; @@ -126,6 +127,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); export const VolumeCreate = () => { + const flags = useFlags(); const theme = useTheme(); const navigate = useNavigate(); const { classes } = useStyles(); @@ -410,6 +412,7 @@ export const VolumeCreate = () => { currentCapability="Block Storage" disabled={doesNotHavePermission} errorText={touched.region ? errors.region : undefined} + flags={flags} label="Region" onBlur={handleBlur} regions={regions ?? []} diff --git a/packages/manager/src/features/components/PlansPanel/PlansAvailabilityNotice.test.tsx b/packages/manager/src/features/components/PlansPanel/PlansAvailabilityNotice.test.tsx index 43f74766a0b..0542b51e9b9 100644 --- a/packages/manager/src/features/components/PlansPanel/PlansAvailabilityNotice.test.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlansAvailabilityNotice.test.tsx @@ -1,7 +1,6 @@ -import { formatPlanTypes } from '@linode/utilities'; +import { formatPlanTypes, regionFactory } from '@linode/utilities'; import React from 'react'; -import { regionFactory } from 'src/factories/regions'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlansAvailabilityNotice } from './PlansAvailabilityNotice'; diff --git a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx index de5053e9a65..ac6c95947e0 100644 --- a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx @@ -88,7 +88,7 @@ export const PlansPanel = (props: PlansPanelProps) => { } = props; const flags = useFlags(); - const { isGeckoLAEnabled } = useIsGeckoEnabled(); + const { isGeckoLAEnabled } = useIsGeckoEnabled(flags); const location = useLocation(); const params = getQueryParamsFromQueryString( location.search diff --git a/packages/manager/src/features/components/PlansPanel/utils.test.ts b/packages/manager/src/features/components/PlansPanel/utils.test.ts index b1988601ab2..e17ff6990ab 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.test.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.test.ts @@ -1,7 +1,7 @@ +import { regionAvailabilityFactory } from '@linode/utilities'; import { renderHook } from '@testing-library/react'; import { extendedTypes } from 'src/__data__/ExtendedType'; -import { regionAvailabilityFactory } from 'src/factories'; import { planSelectionTypeFactory, typeFactory } from 'src/factories/types'; import { PLAN_IS_CURRENTLY_UNAVAILABLE_COPY } from './constants'; diff --git a/packages/manager/src/hooks/useCreateVPC.ts b/packages/manager/src/hooks/useCreateVPC.ts index 3bb7cfda10a..d2eff39607b 100644 --- a/packages/manager/src/hooks/useCreateVPC.ts +++ b/packages/manager/src/hooks/useCreateVPC.ts @@ -19,7 +19,7 @@ import { sendLinodeCreateFormStepEvent } from 'src/utilities/analytics/formEvent import { DEFAULT_SUBNET_IPV4_VALUE } from 'src/utilities/subnets'; import type { CreateVPCPayload, VPC } from '@linode/api-v4'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; // Custom hook to consolidate shared logic between VPCCreate.tsx and VPCCreateDrawer.tsx export interface UseCreateVPCInputs { diff --git a/packages/manager/src/mocks/presets/crud/handlers/quotas.ts b/packages/manager/src/mocks/presets/crud/handlers/quotas.ts index ca8167f1afe..2d9c2cad3d1 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/quotas.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/quotas.ts @@ -1,7 +1,6 @@ -import { pickRandom } from '@linode/utilities'; +import { pickRandom, regions } from '@linode/utilities'; import { http } from 'msw'; -import { regions } from 'src/__data__/regionsData'; import { objectStorageEndpointsFactory } from 'src/factories/objectStorage'; import { quotaFactory, quotaUsageFactory } from 'src/factories/quotas'; import { diff --git a/packages/manager/src/mocks/presets/extra/regions/legacyRegions.ts b/packages/manager/src/mocks/presets/extra/regions/legacyRegions.ts index 8aa91a808f9..0c78d6470e3 100644 --- a/packages/manager/src/mocks/presets/extra/regions/legacyRegions.ts +++ b/packages/manager/src/mocks/presets/extra/regions/legacyRegions.ts @@ -1,6 +1,6 @@ +import { regions } from '@linode/utilities'; import { http } from 'msw'; -import { regions } from 'src/__data__/regionsData'; import { makePaginatedResponse } from 'src/mocks/utilities/response'; import type { MockPresetExtra } from 'src/mocks/types'; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 5cf51a3c4e4..7b488e2ef81 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -6,16 +6,19 @@ * * New handlers should be added to the CRUD baseline preset instead (ex: src/mocks/presets/crud/handlers/linodes.ts) which support a much more dynamic data mocking. */ -import { pickRandom } from '@linode/utilities'; +import { + accountAvailabilityFactory, + pickRandom, + regionAvailabilityFactory, + regions, +} from '@linode/utilities'; import { DateTime } from 'luxon'; import { HttpResponse, http } from 'msw'; -import { regions } from 'src/__data__/regionsData'; import { MOCK_THEME_STORAGE_KEY } from 'src/dev-tools/ThemeSelector'; import { VLANFactory, // abuseTicketNotificationFactory, - accountAvailabilityFactory, accountBetaFactory, accountFactory, accountMaintenanceFactory, @@ -90,7 +93,6 @@ import { proDedicatedTypeFactory, profileFactory, promoFactory, - regionAvailabilityFactory, securityQuestionsFactory, serviceTypesFactory, stackScriptFactory, diff --git a/packages/manager/src/routes/firewalls/index.ts b/packages/manager/src/routes/firewalls/index.ts index 66436a0eb23..9e3674c4e22 100644 --- a/packages/manager/src/routes/firewalls/index.ts +++ b/packages/manager/src/routes/firewalls/index.ts @@ -4,7 +4,7 @@ import { rootRoute } from '../root'; import { FirewallsRoute } from './FirewallsRoute'; import type { TableSearchParams } from '../types'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; export interface FirewallsSearchParams extends TableSearchParams { type?: LinodeCreateType; diff --git a/packages/manager/src/utilities/analytics/formEventAnalytics.ts b/packages/manager/src/utilities/analytics/formEventAnalytics.ts index 084fb0fc51b..46086eae603 100644 --- a/packages/manager/src/utilities/analytics/formEventAnalytics.ts +++ b/packages/manager/src/utilities/analytics/formEventAnalytics.ts @@ -7,7 +7,7 @@ import type { FormStepEvent, LinodeCreateFormEventOptions, } from './types'; -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; /** * Form Events diff --git a/packages/manager/src/utilities/analytics/types.ts b/packages/manager/src/utilities/analytics/types.ts index 3ccf69f4de8..c4d635f122e 100644 --- a/packages/manager/src/utilities/analytics/types.ts +++ b/packages/manager/src/utilities/analytics/types.ts @@ -1,4 +1,4 @@ -import type { LinodeCreateType } from 'src/features/Linodes/LinodeCreate/types'; +import type { LinodeCreateType } from '@linode/utilities'; // Define a custom type for the _satellite object declare global { diff --git a/packages/manager/src/utilities/doesRegionSupportFeature.test.ts b/packages/manager/src/utilities/doesRegionSupportFeature.test.ts index b2fae919e5b..e6633f276af 100644 --- a/packages/manager/src/utilities/doesRegionSupportFeature.test.ts +++ b/packages/manager/src/utilities/doesRegionSupportFeature.test.ts @@ -1,4 +1,4 @@ -import { regions } from 'src/__data__/regionsData'; +import { regions } from '@linode/utilities'; import { doesRegionSupportFeature } from './doesRegionSupportFeature'; diff --git a/packages/ui/.changeset/pr-11790-added-1741678870637.md b/packages/ui/.changeset/pr-11790-added-1741678870637.md new file mode 100644 index 00000000000..6da7f561fef --- /dev/null +++ b/packages/ui/.changeset/pr-11790-added-1741678870637.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Added +--- + +Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/manager/src/components/ListItemOption.tsx b/packages/ui/src/components/ListItemOption/ListItemOption.tsx similarity index 92% rename from packages/manager/src/components/ListItemOption.tsx rename to packages/ui/src/components/ListItemOption/ListItemOption.tsx index 14333d4d7e6..0074342f05c 100644 --- a/packages/manager/src/components/ListItemOption.tsx +++ b/packages/ui/src/components/ListItemOption/ListItemOption.tsx @@ -1,11 +1,15 @@ -import { Box, ListItem, SelectedIcon, Tooltip } from '@linode/ui'; import { styled } from '@mui/material/styles'; import { visuallyHidden } from '@mui/utils'; import React from 'react'; +import { Box } from '../Box'; +import { ListItem } from '../ListItem'; +import { SelectedIcon } from '../Autocomplete'; +import { Tooltip } from '../Tooltip'; + import type { ListItemComponentsPropsOverrides } from '@mui/material/ListItem'; -export interface ListItemProps { +export interface ListItemOptionProps { children?: React.ReactNode; disabledOptions?: DisableItemOption; item: T & { id: number | string }; @@ -34,7 +38,7 @@ export const ListItemOption = ({ maxHeight, props, selected, -}: ListItemProps) => { +}: ListItemOptionProps) => { const { className, onClick, ...rest } = props; const isItemOptionDisabled = Boolean(disabledOptions); const itemOptionDisabledReason = disabledOptions?.reason; diff --git a/packages/ui/src/components/ListItemOption/index.ts b/packages/ui/src/components/ListItemOption/index.ts new file mode 100644 index 00000000000..03d3422cd55 --- /dev/null +++ b/packages/ui/src/components/ListItemOption/index.ts @@ -0,0 +1 @@ +export * from './ListItemOption'; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index 7860eb3b358..bc0a56a62bd 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -24,6 +24,7 @@ export * from './InputAdornment'; export * from './InputLabel'; export * from './List'; export * from './ListItem'; +export * from './ListItemOption'; export * from './Notice'; export * from './Paper'; export * from './Radio'; diff --git a/packages/utilities/.changeset/pr-11790-added-1741678972894.md b/packages/utilities/.changeset/pr-11790-added-1741678972894.md new file mode 100644 index 00000000000..7995b218ed7 --- /dev/null +++ b/packages/utilities/.changeset/pr-11790-added-1741678972894.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Added +--- + +Move `regionsData` from `manager` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/utilities/.changeset/pr-11790-added-1742396887174.md b/packages/utilities/.changeset/pr-11790-added-1742396887174.md new file mode 100644 index 00000000000..3c8ffe2690f --- /dev/null +++ b/packages/utilities/.changeset/pr-11790-added-1742396887174.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Added +--- + +Move `LinodeCreateType` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) diff --git a/packages/utilities/.prettierrc b/packages/utilities/.prettierrc index c563e850dad..7d06e0e8fdf 100644 --- a/packages/utilities/.prettierrc +++ b/packages/utilities/.prettierrc @@ -1,4 +1,4 @@ { "printWidth": 80, "singleQuote": true -} +} \ No newline at end of file diff --git a/packages/utilities/package.json b/packages/utilities/package.json index f8bf2f1eadc..209f4f1cd22 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -55,4 +55,4 @@ "factory.ts": "^0.5.1", "prettier": "~2.2.1" } -} +} \ No newline at end of file diff --git a/packages/utilities/src/__data__/index.ts b/packages/utilities/src/__data__/index.ts new file mode 100644 index 00000000000..4e832e3b55d --- /dev/null +++ b/packages/utilities/src/__data__/index.ts @@ -0,0 +1 @@ +export * from './regionsData'; diff --git a/packages/manager/src/__data__/regionsData.ts b/packages/utilities/src/__data__/regionsData.ts similarity index 100% rename from packages/manager/src/__data__/regionsData.ts rename to packages/utilities/src/__data__/regionsData.ts diff --git a/packages/manager/src/factories/accountAvailability.ts b/packages/utilities/src/factories/accountAvailability.ts similarity index 79% rename from packages/manager/src/factories/accountAvailability.ts rename to packages/utilities/src/factories/accountAvailability.ts index 05ce1dbd0ad..1d427dcf5b7 100644 --- a/packages/manager/src/factories/accountAvailability.ts +++ b/packages/utilities/src/factories/accountAvailability.ts @@ -1,5 +1,6 @@ -import { pickRandom } from '@linode/utilities'; -import { Factory } from '@linode/utilities'; +import { Factory } from './factoryProxy'; + +import { pickRandom } from '../helpers'; import type { AccountAvailability } from '@linode/api-v4'; diff --git a/packages/utilities/src/factories/index.ts b/packages/utilities/src/factories/index.ts index 603414c5554..2aecf0b5021 100644 --- a/packages/utilities/src/factories/index.ts +++ b/packages/utilities/src/factories/index.ts @@ -1,4 +1,6 @@ +export * from './accountAvailability'; export * from './config'; export * from './factoryProxy'; export * from './linodeConfigInterfaceFactory'; export * from './linodeInterface'; +export * from './regions'; diff --git a/packages/manager/src/factories/regions.ts b/packages/utilities/src/factories/regions.ts similarity index 96% rename from packages/manager/src/factories/regions.ts rename to packages/utilities/src/factories/regions.ts index 4b7460258d9..031b5f73223 100644 --- a/packages/manager/src/factories/regions.ts +++ b/packages/utilities/src/factories/regions.ts @@ -1,4 +1,4 @@ -import { Factory } from '@linode/utilities'; +import { Factory } from './factoryProxy'; import type { Country, diff --git a/packages/utilities/src/index.ts b/packages/utilities/src/index.ts index 2565a4b33af..21f4f2b8f5c 100644 --- a/packages/utilities/src/index.ts +++ b/packages/utilities/src/index.ts @@ -1,3 +1,5 @@ +export * from './__data__'; + export * from './constants'; export * from './factories'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/types.ts b/packages/utilities/src/types/LinodeCreateType.ts similarity index 100% rename from packages/manager/src/features/Linodes/LinodeCreate/types.ts rename to packages/utilities/src/types/LinodeCreateType.ts diff --git a/packages/utilities/src/types/index.ts b/packages/utilities/src/types/index.ts index 2565f7a7d2c..86d9b2bfdad 100644 --- a/packages/utilities/src/types/index.ts +++ b/packages/utilities/src/types/index.ts @@ -1 +1,2 @@ +export * from './LinodeCreateType'; export * from './ManagerPreferences'; From d9ced5290b9bc05c7f60e30d8fd05113e4f8595d Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Tue, 25 Mar 2025 15:51:10 +0530 Subject: [PATCH 19/84] change: [M3-9430] - Akamai Design System: Checkbox Component (#11871) * Initial attempt * More progress... * More progress * More progress + clean up... * Added changeset: Add `Checkbox` design tokens and update styles to match Akamai Design System * Update style ordering * Sort import * Some fixes... * Comments clean up... --- .../pr-11871-changed-1742300523305.md | 5 ++ .../components/Checkbox/Checkbox.stories.tsx | 76 +++++++++++++++---- .../ui/src/components/Checkbox/Checkbox.tsx | 50 +++++++----- packages/ui/src/foundations/themes/dark.ts | 23 ++++++ packages/ui/src/foundations/themes/light.ts | 32 +++++++- 5 files changed, 154 insertions(+), 32 deletions(-) create mode 100644 packages/ui/.changeset/pr-11871-changed-1742300523305.md diff --git a/packages/ui/.changeset/pr-11871-changed-1742300523305.md b/packages/ui/.changeset/pr-11871-changed-1742300523305.md new file mode 100644 index 00000000000..5a64f11292a --- /dev/null +++ b/packages/ui/.changeset/pr-11871-changed-1742300523305.md @@ -0,0 +1,5 @@ +--- +"@linode/ui": Changed +--- + +Add `Checkbox` design tokens and update styles to match Akamai Design System ([#11871](https://github.com/linode/manager/pull/11871)) diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx index 1cb40fce6b4..1c63c838d3b 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx @@ -1,14 +1,24 @@ import React from 'react'; +import { Box } from '../Box'; import { Checkbox } from './Checkbox'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; const meta: Meta = { component: Checkbox, + decorators: [ + (Story: StoryFn) => ( + ({ margin: theme.tokens.spacing.S16 })}> + + + ), + ], title: 'Foundations/Checkbox', }; +export default meta; + type Story = StoryObj; export const Default: Story = { @@ -27,43 +37,81 @@ export const Default: Story = { args: { checked: false, }, - render: (args) => , +}; + +export const Unchecked: Story = { + args: { + checked: false, + }, }; export const Checked: Story = { args: { checked: true, }, - render: (args) => , }; -export const Unchecked: Story = { +export const Indeterminate: Story = { args: { - checked: false, + indeterminate: true, + }, +}; + +export const UncheckedDisabled: Story = { + args: { + disabled: true, + }, +}; + +export const CheckedDisabled: Story = { + args: { + checked: true, + disabled: true, }, - render: (args) => , }; -export const Label: Story = { +export const IndeterminateDisabled: Story = { + args: { + indeterminate: true, + disabled: true, + }, +}; + +export const UncheckedReadOnly: Story = { + args: { + readOnly: true, + }, +}; + +export const CheckedReadOnly: Story = { + args: { + readOnly: true, + checked: true, + }, +}; + +export const IndeterminateReadOnly: Story = { + args: { + readOnly: true, + indeterminate: true, + }, +}; + +export const WithLabel: Story = { args: { text: 'This Checkbox has a label', }, - render: (args) => , }; -export const Tooltip: Story = { +export const WithTooltip: Story = { args: { toolTipText: 'This is the tooltip!', }, - render: (args) => , }; -export const LabelAndTooltip: Story = { +export const WithLabelAndTooltip: Story = { args: { text: 'This Checkbox has a tooltip', toolTipText: 'This is the tooltip!', }, - render: (args) => , }; - -export default meta; diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx index cfe056661ec..1470e11affe 100644 --- a/packages/ui/src/components/Checkbox/Checkbox.tsx +++ b/packages/ui/src/components/Checkbox/Checkbox.tsx @@ -1,5 +1,3 @@ -// @todo: modularization - Import from 'ui' package once FormControlLabel is migrated. -import { FormControlLabel } from '@mui/material'; import _Checkbox from '@mui/material/Checkbox'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -10,6 +8,7 @@ import { CheckboxIndeterminateIcon, } from '../../assets/icons'; import { TooltipIcon } from '../TooltipIcon'; +import { FormControlLabel } from '../FormControlLabel'; import type { CheckboxProps } from '@mui/material/Checkbox'; import type { SxProps, Theme } from '@mui/material/styles'; @@ -80,25 +79,42 @@ const StyledCheckbox = styled(_Checkbox)(({ theme, ...props }) => ({ '& .defaultFill': { transition: theme.transitions.create(['fill']), }, - '&:hover': { - color: theme.palette.primary.main, - }, - color: theme.tokens.color.Neutrals[40], + padding: theme.tokens.spacing.S8, transition: theme.transitions.create(['color']), - ...(props.checked && { - color: theme.palette.primary.main, - }), - ...(props.disabled && { - '& .defaultFill': { - fill: `${theme.bg.main}`, - opacity: 0.5, - }, - color: `${theme.tokens.color.Neutrals[40]} !important`, - fill: `${theme.bg.main} !important`, + // Unchecked & Readonly + ...(props.readOnly && { + color: theme.tokens.component.Checkbox.Empty.ReadOnly.Border, pointerEvents: 'none', }), + // Checked & Readonly + ...(props.checked && + props.readOnly && { + svg: { + '#Check': { + fill: theme.tokens.component.Checkbox.Checked.ReadOnly.Icon, + }, + border: `1px solid ${theme.tokens.component.Checkbox.Checked.ReadOnly.Border}`, + }, + color: `${theme.tokens.component.Checkbox.Checked.ReadOnly.Background} !important`, + pointerEvents: 'none', + }), + // Indeterminate & Readonly + ...(props.indeterminate && + props.readOnly && { + svg: { + 'g rect:nth-of-type(2)': { + fill: theme.tokens.component.Checkbox.Indeterminated.ReadOnly.Icon, + }, + border: `1px solid ${theme.tokens.component.Checkbox.Indeterminated.ReadOnly.Border}`, + }, + color: `${theme.tokens.component.Checkbox.Checked.ReadOnly.Background} !important`, + pointerEvents: 'none', + }), })); -const StyledFormControlLabel = styled(FormControlLabel)(() => ({ +const StyledFormControlLabel = styled(FormControlLabel)(({ theme }) => ({ + '& .MuiFormControlLabel-label': { + paddingTop: theme.tokens.spacing.S2, + }, marginRight: 0, })); diff --git a/packages/ui/src/foundations/themes/dark.ts b/packages/ui/src/foundations/themes/dark.ts index f2ed61e17cd..9059eb3773c 100644 --- a/packages/ui/src/foundations/themes/dark.ts +++ b/packages/ui/src/foundations/themes/dark.ts @@ -385,6 +385,29 @@ export const darkTheme: ThemeOptions = { }, }, }, + MuiCheckbox: { + styleOverrides: { + root: { + // Unchecked & Disabled + '&.Mui-disabled': { + '& svg': { + backgroundColor: Component.Checkbox.Empty.Disabled.Background, + }, + color: Component.Checkbox.Empty.Disabled.Border, + pointerEvents: 'none', + }, + // Checked & Disabled + '&.Mui-checked.Mui-disabled': { + color: Component.Checkbox.Checked.Disabled.Background, + }, + // Indeterminate & Disabled + '&.MuiCheckbox-indeterminate.Mui-disabled': { + color: Component.Checkbox.Indeterminated.Disabled.Background, + }, + color: Component.Checkbox.Empty.Default.Border, + }, + }, + }, MuiChip: { defaultProps: { // In dark mode, we decided our Chips will be our primary color by default. diff --git a/packages/ui/src/foundations/themes/light.ts b/packages/ui/src/foundations/themes/light.ts index d3a82697eae..d1f6f56d698 100644 --- a/packages/ui/src/foundations/themes/light.ts +++ b/packages/ui/src/foundations/themes/light.ts @@ -590,7 +590,37 @@ export const lightTheme: ThemeOptions = { MuiCheckbox: { styleOverrides: { root: { - color: Color.Neutrals[40], + '&:active': { + color: `${Component.Checkbox.Empty.Active.Border} !important`, + }, + '&:hover': { + color: `${Component.Checkbox.Empty.Hover.Border} !important`, + }, + // Checked + '&.Mui-checked': { + color: Component.Checkbox.Checked.Default.Background, + }, + // Indeterminate + '&.MuiCheckbox-indeterminate': { + color: Component.Checkbox.Indeterminated.Default.Background, + }, + // Unchecked & Disabled + '&.Mui-disabled': { + '& svg': { + backgroundColor: Component.Checkbox.Empty.Disabled.Background, + }, + color: Component.Checkbox.Empty.Disabled.Border, + pointerEvents: 'none', + }, + // Checked & Disabled + '&.Mui-checked.Mui-disabled': { + color: Component.Checkbox.Checked.Disabled.Background, + }, + // Indeterminate & Disabled + '&.MuiCheckbox-indeterminate.Mui-disabled': { + color: Component.Checkbox.Indeterminated.Disabled.Background, + }, + color: Component.Checkbox.Empty.Default.Border, }, }, }, From 508914741bc0402c8163b5b6968d8cd28c4e8eb8 Mon Sep 17 00:00:00 2001 From: hasyed-akamai Date: Tue, 25 Mar 2025 17:04:35 +0530 Subject: [PATCH 20/84] fix: [M3-9566, 7066] - Disable action menu buttons for Database with tooltip text and enable `Delete Cluster` for `read/write` grant user (#11890) * fix: [M3-9566] - Disable action menu for Database with tootip text * Added changeset: Disable action menu for `Database` with tooltip text * enable delete cluster button with read/write access user * disable suspend button * Update changeset description --- .../pr-11890-fixed-1742471963025.md | 5 +++ .../manager/src/features/Account/utils.ts | 2 + .../DatabaseSettings/DatabaseSettings.tsx | 4 +- .../DatabaseLanding/DatabaseActionMenu.tsx | 40 ++++++++++++++++--- 4 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 packages/manager/.changeset/pr-11890-fixed-1742471963025.md diff --git a/packages/manager/.changeset/pr-11890-fixed-1742471963025.md b/packages/manager/.changeset/pr-11890-fixed-1742471963025.md new file mode 100644 index 00000000000..a3d02758c9c --- /dev/null +++ b/packages/manager/.changeset/pr-11890-fixed-1742471963025.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Disable action menu for `Database` with tooltip text and enable `Delete Cluster` for `read/write` grant users ([#11890](https://github.com/linode/manager/pull/11890)) diff --git a/packages/manager/src/features/Account/utils.ts b/packages/manager/src/features/Account/utils.ts index 4fabcaa6edf..cd7aa09578c 100644 --- a/packages/manager/src/features/Account/utils.ts +++ b/packages/manager/src/features/Account/utils.ts @@ -18,6 +18,8 @@ export type ActionType = | 'rebuild' | 'rescue' | 'resize' + | 'resume' + | 'suspend' | 'view'; interface GetRestrictedResourceText { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettings.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettings.tsx index 50aef5ccbd0..f97bbac4cb5 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettings.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSettings/DatabaseSettings.tsx @@ -1,3 +1,4 @@ +import { useProfile } from '@linode/queries'; import { Divider, Paper, Stack, Typography } from '@linode/ui'; import * as React from 'react'; @@ -16,7 +17,6 @@ import { isDefaultDatabase, useIsDatabasesEnabled, } from 'src/features/Databases/utilities'; -import { useProfile } from '@linode/queries'; import AccessControls from '../AccessControls'; import DatabaseSettingsDeleteClusterDialog from './DatabaseSettingsDeleteClusterDialog'; @@ -148,7 +148,7 @@ export const DatabaseSettings: React.FC = (props) => { diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx index 439cca75acc..3084ac5aa7d 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseActionMenu.tsx @@ -3,6 +3,8 @@ import * as React from 'react'; import { useHistory } from 'react-router-dom'; import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useResumeDatabaseMutation } from 'src/queries/databases/databases'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -10,6 +12,7 @@ import { useIsDatabasesEnabled } from '../utilities'; import type { DatabaseStatus, Engine } from '@linode/api-v4'; import type { Action } from 'src/components/ActionMenu/ActionMenu'; +import type { ActionType } from 'src/features/Account/utils'; interface Props { databaseEngine: Engine; @@ -63,48 +66,73 @@ export const DatabaseActionMenu = (props: Props) => { } }; + const isDatabaseReadOnly = useIsResourceRestricted({ + grantLevel: 'read_only', + grantType: 'database', + id: databaseId, + }); + + const getTooltipText = (action: ActionType) => { + return isDatabaseReadOnly + ? getRestrictedResourceText({ + action, + isSingular: true, + resourceType: 'Databases', + }) + : undefined; + }; + const actions: Action[] = [ { - disabled: isDatabaseNotRunning || isDatabaseSuspended, + disabled: + isDatabaseNotRunning || isDatabaseSuspended || isDatabaseReadOnly, onClick: handlers.handleManageAccessControls, title: 'Manage Access Controls', + tooltip: getTooltipText('edit'), }, { - disabled: isDatabaseNotRunning || isDatabaseSuspended, + disabled: + isDatabaseNotRunning || isDatabaseSuspended || isDatabaseReadOnly, onClick: handlers.handleResetPassword, title: 'Reset Root Password', + tooltip: getTooltipText('edit'), }, { - disabled: isDatabaseNotRunning || isDatabaseSuspended, + disabled: + isDatabaseNotRunning || isDatabaseSuspended || isDatabaseReadOnly, onClick: () => { history.push({ pathname: `/databases/${databaseEngine}/${databaseId}/resize`, }); }, title: 'Resize', + tooltip: getTooltipText('resize'), }, { - disabled: isDatabaseNotRunning, + disabled: isDatabaseNotRunning || isDatabaseReadOnly, onClick: handlers.handleDelete, title: 'Delete', + tooltip: getTooltipText('delete'), }, ]; if (isDatabasesV2GA) { actions.unshift({ - disabled: databaseStatus !== 'active', + disabled: databaseStatus !== 'active' || isDatabaseReadOnly, onClick: () => { handlers.handleSuspend(); }, title: 'Suspend', + tooltip: getTooltipText('suspend'), }); actions.splice(4, 0, { - disabled: !isDatabaseSuspended, + disabled: !isDatabaseSuspended || isDatabaseReadOnly, onClick: () => { handleResume(); }, title: 'Resume', + tooltip: getTooltipText('resume'), }); } From 129b9b4f2e51177cf9283df08f034f2ecfe79b98 Mon Sep 17 00:00:00 2001 From: rodonnel-akamai Date: Tue, 25 Mar 2025 13:19:47 -0400 Subject: [PATCH 21/84] feat: [UIE-8140] - IAM RBAC - Assign New Roles drawer update (#11834) * UIE-8140: Assign New Roles drawer update * Added changeset: Adding in the functionality behind the Assign New Roles drawer for a single user in IAM * fix with using FormProvider * resolve conflicts and update the drawer for changing role --------- Co-authored-by: Anastasiia Alekseenko Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> --- ...r-11834-upcoming-features-1741795011556.md | 5 + .../src/features/IAM/Shared/utilities.ts | 6 + .../Users/UserRoles/AssignNewRoleDrawer.tsx | 125 +++++++++++------- .../IAM/Users/UserRoles/AssignSingleRole.tsx | 83 ++++++++++++ 4 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md create mode 100644 packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx diff --git a/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md new file mode 100644 index 00000000000..992ca971826 --- /dev/null +++ b/packages/manager/.changeset/pr-11834-upcoming-features-1741795011556.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add functionality to support the 'Assign New Roles' drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) diff --git a/packages/manager/src/features/IAM/Shared/utilities.ts b/packages/manager/src/features/IAM/Shared/utilities.ts index 62ad79a97b4..171e8eaeca0 100644 --- a/packages/manager/src/features/IAM/Shared/utilities.ts +++ b/packages/manager/src/features/IAM/Shared/utilities.ts @@ -429,3 +429,9 @@ export const updateUserRoles = ({ } ); }; + +export interface AssignNewRoleFormValues { + roles: { + role: RolesType | null; + }[]; +} diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx index 75d8db02052..3bc9e68864e 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignNewRoleDrawer.tsx @@ -1,14 +1,18 @@ -import { Autocomplete, Drawer, Typography } from '@linode/ui'; +import { ActionsPanel, Drawer, Typography } from '@linode/ui'; +import { useTheme } from '@mui/material'; import React from 'react'; +import { FormProvider, useFieldArray, useForm } from 'react-hook-form'; import { Link } from 'src/components/Link'; +import { LinkButton } from 'src/components/LinkButton'; import { NotFound } from 'src/components/NotFound'; +import { StyledLinkButtonBox } from 'src/components/SelectFirewallPanel/SelectFirewallPanel'; +import { AssignSingleRole } from 'src/features/IAM/Users/UserRoles/AssignSingleRole'; import { useAccountPermissions } from 'src/queries/iam/iam'; -import { AssignedPermissionsPanel } from '../../Shared/AssignedPermissionsPanel/AssignedPermissionsPanel'; -import { getAllRoles, getRoleByName } from '../../Shared/utilities'; +import { getAllRoles } from '../../Shared/utilities'; -import type { RolesType } from '../../Shared/utilities'; +import type { AssignNewRoleFormValues } from '../../Shared/utilities'; interface Props { onClose: () => void; @@ -16,31 +20,44 @@ interface Props { } export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { - const [ - selectedOptions, - setSelectedOptions, - ] = React.useState(null); + const theme = useTheme(); - const { - data: accountPermissions, - isLoading: accountPermissionsLoading, - } = useAccountPermissions(); + const { data: accountPermissions } = useAccountPermissions(); + + const form = useForm({ + defaultValues: { + roles: [{ role: null }], + }, + }); + + const { control, handleSubmit, reset, watch } = form; + const { append, fields, remove } = useFieldArray({ + control, + name: 'roles', + }); + + // to watch changes to this value since we're conditionally rendering "Add another role" + const roles = watch('roles'); const allRoles = React.useMemo(() => { if (!accountPermissions) { return []; } - return getAllRoles(accountPermissions); }, [accountPermissions]); - // Get the selected role based on the `selectedOptions` - const selectedRole = React.useMemo(() => { - if (!selectedOptions || !accountPermissions) { - return null; - } - return getRoleByName(accountPermissions, selectedOptions.value); - }, [selectedOptions, accountPermissions]); + const onSubmit = handleSubmit(async (values: AssignNewRoleFormValues) => { + // TODO - make this really do something apart from console logging - UIE-8590 + + // const selectedRoles = values.roles.map((r) => r.role).filter(Boolean); + handleClose(); + }); + + const handleClose = () => { + reset(); + + onClose(); + }; // TODO - add a link 'Learn more" - UIE-8534 return ( @@ -50,31 +67,49 @@ export const AssignNewRoleDrawer = ({ onClose, open }: Props) => { open={open} title="Assign New Roles" > - - Select a role you want to assign to a user. Some roles require selecting - resources they should apply to. Configure the first role and continue - adding roles or save the assignment. - Learn more about roles and permissions. - - - ( -
  • - {option.label} -
  • - )} - label="Assign New Roles" - loading={accountPermissionsLoading} - onChange={(_, value) => setSelectedOptions(value)} - options={allRoles} - placeholder="Select a Role" - textFieldProps={{ hideLabel: true, noMarginTop: true }} - value={selectedOptions} - /> - - {selectedRole && ( - - )} + {' '} + +
    + + Select a role you want to assign to a user. Some roles require + selecting resources they should apply to. Configure the first role + and continue adding roles or save the assignment. + Learn more about roles and permissions. + + + {!!accountPermissions && + fields.map((field, index) => ( + remove(index)} + options={allRoles} + permissions={accountPermissions} + /> + ))} + + {/* If all roles are filled, allow them to add another */} + {roles.length > 0 && roles.every((field) => field.role) && ( + + append({ role: null })}> + Add another role + + + )} + + +
    ); }; diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx new file mode 100644 index 00000000000..2b82c0e3882 --- /dev/null +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignSingleRole.tsx @@ -0,0 +1,83 @@ +import { Autocomplete, Button } from '@linode/ui'; +import Close from '@mui/icons-material/Close'; +import { Divider, useTheme } from '@mui/material'; +import Box from '@mui/material/Box'; +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { AssignedPermissionsPanel } from 'src/features/IAM/Shared/AssignedPermissionsPanel/AssignedPermissionsPanel'; +import { getRoleByName } from 'src/features/IAM/Shared/utilities'; + +import type { IamAccountPermissions } from '@linode/api-v4'; +import type { + AssignNewRoleFormValues, + RolesType, +} from 'src/features/IAM/Shared/utilities'; + +interface Props { + index: number; + onRemove: (idx: number) => void; + options: RolesType[]; + permissions: IamAccountPermissions; +} + +export const AssignSingleRole = ({ + index, + onRemove, + options, + permissions, +}: Props) => { + const theme = useTheme(); + + const { control } = useFormContext(); + + return ( + + + {index !== 0 && ( + + )} + + ( + <> + { + onChange(newValue); + }} + label="Assign New Roles" + options={options} + placeholder="Select a Role" + textFieldProps={{ hideLabel: true }} + value={value || null} + /> + {value && ( + + )} + + )} + control={control} + name={`roles.${index}.role`} + /> + + + + + + ); +}; From 54852dbc856bf85b2a48965e621bcf32bb873954 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:52:53 -0400 Subject: [PATCH 22/84] fix: [M3-9652] - Packages unable to publish (#11923) * use correct secret and fix typecheck error * Added changeset: Fix incorrect secret in `publish-packages` Github Action --------- Co-authored-by: Banks Nussman --- .github/workflows/ci.yml | 2 +- .../.changeset/pr-11923-tech-stories-1742933716536.md | 5 +++++ .../manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts | 2 +- .../LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx | 1 - 4 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-11923-tech-stories-1742933716536.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6508896b7a..e2f83577c65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -342,7 +342,7 @@ jobs: - run: pnpm install --frozen-lockfile - run: npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - run: pnpm publish -r --filter @linode/api-v4 --filter @linode/validation --no-git-checks --access public - name: slack-notify uses: rtCamp/action-slack-notify@master diff --git a/packages/manager/.changeset/pr-11923-tech-stories-1742933716536.md b/packages/manager/.changeset/pr-11923-tech-stories-1742933716536.md new file mode 100644 index 00000000000..d5aad00927c --- /dev/null +++ b/packages/manager/.changeset/pr-11923-tech-stories-1742933716536.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Fix incorrect secret in `publish-packages` Github Action ([#11923](https://github.com/linode/manager/pull/11923)) diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index 17bf313023c..7426806d295 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -1,9 +1,9 @@ import { createStackScript } from '@linode/api-v4/lib'; +import { regionFactory } from '@linode/utilities'; import { createLinodeRequestFactory, imageFactory, linodeFactory, - regionFactory, } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx index fa48d6c06f1..074098faef1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx @@ -1,4 +1,3 @@ -import { regionFactory } from '@linode/utilities'; import * as React from 'react'; import { DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY } from 'src/components/Encryption/constants'; From 8e7f578b9d3e54c65e154a4c8209b291f96a3280 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Tue, 25 Mar 2025 18:31:56 -0400 Subject: [PATCH 23/84] upcoming: [M3-9534] - Initial VPC Support in the `Add Network Interface` Drawer (#11887) * initial vpc and subnet select * add a shared firewallselect component * use firewall select globally * add jsdoc comments * add some testing * finish up testing for now * Added changeset: Add VPC support to the Add Network Interface Drawer * Added changeset: Added `FirewallSelect` component * Added changeset: Add test for Add Linode Interface drawer * clean up changesets * support default chips in the Firewall Select * fix spacing regression * properly handle disableClearable in the new Firewall Select * support default firewalls in the Add Interface drawer * use newer copy @coliu-akamai * fix unit test after UX tooltip changes --------- Co-authored-by: Banks Nussman --- .../pr-11887-tech-stories-1742422267464.md | 5 + .../pr-11887-tests-1742422286890.md | 5 + ...r-11887-upcoming-features-1742422230829.md | 5 + .../e2e/core/linodes/linode-network.spec.ts | 175 +++++++++++++++ .../cypress/support/intercepts/firewalls.ts | 23 ++ .../cypress/support/intercepts/linodes.ts | 40 ++++ .../src/features/Account/DefaultFirewalls.tsx | 208 ++++++++---------- .../components/DefaultFirewallChip.tsx | 17 ++ .../components/FirewallSelect.test.tsx | 53 +++++ .../Firewalls/components/FirewallSelect.tsx | 103 +++++++++ .../components/FirewallSelectOption.tsx | 50 +++++ .../FirewallSelectOption.utils.test.tsx | 108 +++++++++ .../components/FirewallSelectOption.utils.tsx | 85 +++++++ .../Linodes/LinodeCreate/Firewall.tsx | 18 +- .../LinodeCreate/Networking/Firewall.tsx | 19 +- .../Linodes/LinodeCreate/Networking/VPC.tsx | 2 +- .../LinodeFirewalls/AddFirewallForm.tsx | 25 +-- .../AddInterfaceDrawer/AddInterfaceDrawer.tsx | 9 +- .../AddInterfaceDrawer/AddInterfaceForm.tsx | 7 +- .../AddInterfaceDrawer/InterfaceFirewall.tsx | 24 +- .../AddInterfaceDrawer/InterfaceType.tsx | 30 ++- .../AddInterfaceDrawer/VPCInterface.tsx | 82 +++++++ .../AddInterfaceDrawer/utilities.ts | 11 + .../LinodeInterfaces/LinodeInterfaces.tsx | 4 +- .../LinodeNetworking/LinodeNetworking.tsx | 4 +- packages/queries/src/firewalls/firewalls.ts | 14 +- packages/ui/src/components/Chip/Chip.tsx | 8 +- .../src/factories/linodeInterface.ts | 12 +- 28 files changed, 942 insertions(+), 204 deletions(-) create mode 100644 packages/manager/.changeset/pr-11887-tech-stories-1742422267464.md create mode 100644 packages/manager/.changeset/pr-11887-tests-1742422286890.md create mode 100644 packages/manager/.changeset/pr-11887-upcoming-features-1742422230829.md create mode 100644 packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelect.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx create mode 100644 packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/VPCInterface.tsx diff --git a/packages/manager/.changeset/pr-11887-tech-stories-1742422267464.md b/packages/manager/.changeset/pr-11887-tech-stories-1742422267464.md new file mode 100644 index 00000000000..f24b6dcdde3 --- /dev/null +++ b/packages/manager/.changeset/pr-11887-tech-stories-1742422267464.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Add `FirewallSelect` component ([#11887](https://github.com/linode/manager/pull/11887)) diff --git a/packages/manager/.changeset/pr-11887-tests-1742422286890.md b/packages/manager/.changeset/pr-11887-tests-1742422286890.md new file mode 100644 index 00000000000..6004a53b2ad --- /dev/null +++ b/packages/manager/.changeset/pr-11887-tests-1742422286890.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add test for Add Linode Interface drawer ([#11887](https://github.com/linode/manager/pull/11887)) diff --git a/packages/manager/.changeset/pr-11887-upcoming-features-1742422230829.md b/packages/manager/.changeset/pr-11887-upcoming-features-1742422230829.md new file mode 100644 index 00000000000..ba34c2ef016 --- /dev/null +++ b/packages/manager/.changeset/pr-11887-upcoming-features-1742422230829.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add VPC support to the Add Network Interface Drawer ([#11887](https://github.com/linode/manager/pull/11887)) diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts index 9df12ab3fa8..794e3b868ce 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts @@ -1,20 +1,30 @@ +import { + linodeInterfaceFactoryPublic, + linodeInterfaceFactoryVPC, +} from '@linode/utilities'; import { firewallDeviceFactory, firewallFactory, ipAddressFactory, linodeFactory, + subnetFactory, + vpcFactory, } from '@src/factories'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockAddFirewallDevice, mockGetFirewalls, + mockGetLinodeInterfaceFirewalls, } from 'support/intercepts/firewalls'; import { + mockCreateLinodeInterface, mockGetLinodeDetails, mockGetLinodeFirewalls, mockGetLinodeIPAddresses, + mockGetLinodeInterfaces, } from 'support/intercepts/linodes'; import { mockUpdateIPAddress } from 'support/intercepts/networking'; +import { mockGetVPCs } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import type { IPRange } from '@linode/api-v4'; @@ -235,3 +245,168 @@ describe('Firewalls', () => { .should('be.disabled'); }); }); + +describe('Linode Interfaces', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + linodeInterfaces: { enabled: true }, + }); + }); + + it('allows the user to add a public network interface with a firewall', () => { + const linode = linodeFactory.build({ interface_generation: 'linode' }); + const firewalls = firewallFactory.buildList(3); + const linodeInterface = linodeInterfaceFactoryPublic.build(); + + const selectedFirewall = firewalls[1]; + + mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetLinodeInterfaces(linode.id, { interfaces: [] }).as('getInterfaces'); + mockGetFirewalls(firewalls).as('getFirewalls'); + mockCreateLinodeInterface(linode.id, linodeInterface).as('createInterface'); + mockGetLinodeInterfaceFirewalls(linode.id, linodeInterface.id, [ + selectedFirewall, + ]).as('getInterfaceFirewalls'); + + cy.visitWithLogin(`/linodes/${linode.id}/networking`); + + cy.wait(['@getLinode', '@getInterfaces']); + + ui.button.findByTitle('Add Network Interface').scrollIntoView().click(); + + ui.drawer.findByTitle('Add Network Interface').within(() => { + // Verify firewalls fetch + cy.wait('@getFirewalls'); + + // Try submitting the form + ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); + + // Verify a validation error shows + cy.findByText('You must selected an Interface type.').should( + 'be.visible' + ); + + // Select the public interface type + cy.findByLabelText('Public').click(); + + // Verify a validation error goes away + cy.findByText('You must selected an Interface type.').should('not.exist'); + + // Select a Firewall + ui.autocomplete.findByLabel('Firewall').click(); + ui.autocompletePopper.findByTitle(selectedFirewall.label).click(); + + mockGetLinodeInterfaces(linode.id, { interfaces: [linodeInterface] }); + + ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); + }); + + cy.wait('@createInterface').then((xhr) => { + const requestPayload = xhr.request.body; + + // Confirm that request payload includes a Public interface only + expect(requestPayload['public']).to.be.an('object'); + expect(requestPayload['vpc']).to.equal(null); + expect(requestPayload['vlan']).to.equal(null); + }); + + ui.toast.assertMessage('Successfully added network interface.'); + + // Verify the interface row shows upon creation + cy.findByText(linodeInterface.mac_address) + .closest('tr') + .within(() => { + // Verify we fetch the interfaces firewalls and the label shows + cy.wait('@getInterfaceFirewalls'); + cy.findByText(selectedFirewall.label).should('be.visible'); + + // Verify the interface type shows + cy.findByText('Public').should('be.visible'); + }); + }); + + it('allows the user to add a VPC network interface with a firewall', () => { + const linode = linodeFactory.build({ interface_generation: 'linode' }); + const firewalls = firewallFactory.buildList(3); + const subnets = subnetFactory.buildList(3); + const vpcs = vpcFactory.buildList(3, { subnets }); + const linodeInterface = linodeInterfaceFactoryVPC.build(); + + const selectedFirewall = firewalls[1]; + const selectedVPC = vpcs[1]; + const selectedSubnet = selectedVPC.subnets[0]; + + mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetLinodeInterfaces(linode.id, { interfaces: [] }).as('getInterfaces'); + mockGetFirewalls(firewalls).as('getFirewalls'); + mockGetVPCs(vpcs).as('getVPCs'); + mockCreateLinodeInterface(linode.id, linodeInterface).as('createInterface'); + mockGetLinodeInterfaceFirewalls(linode.id, linodeInterface.id, [ + selectedFirewall, + ]).as('getInterfaceFirewalls'); + + cy.visitWithLogin(`/linodes/${linode.id}/networking`); + + cy.wait(['@getLinode', '@getInterfaces']); + + ui.button.findByTitle('Add Network Interface').scrollIntoView().click(); + + ui.drawer.findByTitle('Add Network Interface').within(() => { + // Verify firewalls fetch + cy.wait('@getFirewalls'); + + cy.findByLabelText('VPC').click(); + + // Verify VPCs fetch + cy.wait('@getVPCs'); + + // Select a VPC + ui.autocomplete.findByLabel('VPC').click(); + ui.autocompletePopper.findByTitle(selectedVPC.label).click(); + + // Select a Firewall + ui.autocomplete.findByLabel('Firewall').click(); + ui.autocompletePopper.findByTitle(selectedFirewall.label).click(); + + // Submit the form + ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); + + // Verify an error shows because a subnet is not selected + cy.findByText('Subnet is required.').should('be.visible'); + + // Select a Subnet + ui.autocomplete.findByLabel('Subnet').click(); + ui.autocompletePopper.findByTitle(selectedSubnet.label).click(); + + // Verify the error goes away + cy.findByText('Subnet is required.').should('not.exist'); + + mockGetLinodeInterfaces(linode.id, { interfaces: [linodeInterface] }); + + ui.button.findByAttribute('type', 'submit').should('be.enabled').click(); + }); + + cy.wait('@createInterface').then((xhr) => { + const requestPayload = xhr.request.body; + + // Confirm that request payload includes VPC interface only + expect(requestPayload['public']).to.be.null; + expect(requestPayload['vpc']['subnet_id']).to.equal(selectedSubnet.id); + expect(requestPayload['vlan']).to.null; + }); + + ui.toast.assertMessage('Successfully added network interface.'); + + // Verify the interface row shows upon creation + cy.findByText(linodeInterface.mac_address) + .closest('tr') + .within(() => { + // Verify we fetch the interfaces firewalls and the label shows + cy.wait('@getInterfaceFirewalls'); + cy.findByText(selectedFirewall.label).should('be.visible'); + + // Verify the interface type shows + cy.findByText('VPC').should('be.visible'); + }); + }); +}); diff --git a/packages/manager/cypress/support/intercepts/firewalls.ts b/packages/manager/cypress/support/intercepts/firewalls.ts index 6ffe43ada71..490c1fb11bc 100644 --- a/packages/manager/cypress/support/intercepts/firewalls.ts +++ b/packages/manager/cypress/support/intercepts/firewalls.ts @@ -37,6 +37,29 @@ export const mockGetFirewalls = ( ); }; +/** + * Intercepts GET request to fetch a Linode Interface's Firewalls + * + * @param linodeId - The ID of the Linode + * @param interfaceId - The ID of the Linode Interface + * @param firewalls - The Firewalls assigned to the LinodeInterface + * + * @returns Cypress chainable. + */ +export const mockGetLinodeInterfaceFirewalls = ( + linodeId: number, + interfaceId: number, + firewalls: Firewall[] +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher( + `linode/instances/${linodeId}/interfaces/${interfaceId}/firewalls` + ), + paginateResponse(firewalls) + ); +}; + /** * Intercepts POST request to create a Firewall and mocks response. * diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index b9433352cab..5c3b04f2da8 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -13,6 +13,8 @@ import type { Firewall, Kernel, Linode, + LinodeInterface, + LinodeInterfaces, LinodeIPsResponse, LinodeType, Volume, @@ -641,3 +643,41 @@ export const interceptCancelLinodeBackups = ( apiMatcher(`linode/instances/${linodeId}/backups/cancel`) ); }; + +/** + * Mocks GET request to get a Linode's Interfaces. + * + * @param linodeId - ID of Linode to get interfaces associated with it + * @param interfaces - the mocked Linode interfaces + * + * @returns Cypress Chainable. + */ +export const mockGetLinodeInterfaces = ( + linodeId: number, + interfaces: LinodeInterfaces +): Cypress.Chainable => { + return cy.intercept( + 'GET', + apiMatcher(`linode/instances/${linodeId}/interfaces`), + interfaces + ); +}; + +/** + * Intercepts POST request to create a Linode Interface. + * + * @param linodeId - the Linodes ID to add the interface to. + * @param linodeInterface - a mock linode interface object. + * + * @returns Cypress chainable. + */ +export const mockCreateLinodeInterface = ( + linodeId: number, + linodeInterface: LinodeInterface +): Cypress.Chainable => { + return cy.intercept( + 'POST', + apiMatcher(`linode/instances/${linodeId}/interfaces`), + makeResponse(linodeInterface) + ); +}; diff --git a/packages/manager/src/features/Account/DefaultFirewalls.tsx b/packages/manager/src/features/Account/DefaultFirewalls.tsx index 6d8fecc4716..524d6c0198d 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.tsx @@ -1,6 +1,5 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { - useAllFirewallsQuery, useFirewallSettingsQuery, useMutateFirewallSettings, } from '@linode/queries'; @@ -12,7 +11,6 @@ import { Divider, ErrorState, Notice, - Select, Stack, Typography, } from '@linode/ui'; @@ -21,6 +19,8 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { FirewallSelect } from '../Firewalls/components/FirewallSelect'; + import type { UpdateFirewallSettings } from '@linode/api-v4'; const DEFAULT_FIREWALL_PLACEHOLDER = 'None'; @@ -36,16 +36,6 @@ export const DefaultFirewalls = () => { const { mutateAsync: updateFirewallSettings } = useMutateFirewallSettings(); - const { - data: firewalls, - error: firewallsError, - isLoading: isLoadingfirewalls, - } = useAllFirewallsQuery(); - const firewallOptions = - firewalls?.map((firewall) => { - return { label: firewall.label, value: firewall.id }; - }) ?? []; - const values = { default_firewall_ids: { ...firewallSettings?.default_firewall_ids }, }; @@ -73,7 +63,7 @@ export const DefaultFirewalls = () => { } }; - if (isLoadingFirewallSettings || isLoadingfirewalls) { + if (isLoadingFirewallSettings) { return ( @@ -81,7 +71,7 @@ export const DefaultFirewalls = () => { ); } - if (firewallSettingsError || firewallsError) { + if (firewallSettingsError) { return ( @@ -95,115 +85,89 @@ export const DefaultFirewalls = () => { {errors.root?.message && ( {errors.root.message} )} - - - Set the default firewall that is assigned to each network interface - type when creating a Linode. The same firewall (new or existing) can - be assigned to each type of interface/connection. - - ({ marginTop: theme.spacing(2) })} - variant="h3" - > - Linodes - - ( - { - field.onChange(item?.value); - }} - value={ - firewallOptions.find( - (option) => option.value === field.value - ) ?? null - } - errorText={fieldState.error?.message} - label="Linode Interfaces - Public Interface Firewall" - options={firewallOptions} - placeholder={DEFAULT_FIREWALL_PLACEHOLDER} - /> - )} - control={control} - name="default_firewall_ids.public_interface" - /> - ( - { - field.onChange(item?.value); - }} - value={ - firewallOptions.find( - (option) => option.value === field.value - ) ?? null - } - errorText={fieldState.error?.message} - label="NodeBalancers Firewall" - options={firewallOptions} - placeholder={DEFAULT_FIREWALL_PLACEHOLDER} - /> - )} - control={control} - name="default_firewall_ids.nodebalancer" - /> - ({ - marginTop: theme.spacing(2), - })} - > - - + + Set the default firewall that is assigned to each network interface + type when creating a Linode. The same firewall (new or existing) can + be assigned to each type of interface/connection. + + } spacing={2}> + + Linodes + ( + field.onChange(firewall.id)} + placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + value={field.value} + /> + )} + control={control} + name="default_firewall_ids.linode" + /> + ( + field.onChange(firewall.id)} + placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + value={field.value} + /> + )} + control={control} + name="default_firewall_ids.public_interface" + /> + ( + field.onChange(firewall.id)} + placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + value={field.value} + /> + )} + control={control} + name="default_firewall_ids.vpc_interface" + /> + + + NodeBalancers + ( + field.onChange(firewall.id)} + placeholder={DEFAULT_FIREWALL_PLACEHOLDER} + value={field.value} + /> + )} + control={control} + name="default_firewall_ids.nodebalancer" + /> + + ({ marginTop: theme.spacing(2) })}> + + ); diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx new file mode 100644 index 00000000000..7258979ff4f --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -0,0 +1,17 @@ +import { Chip, Tooltip } from '@linode/ui'; +import React from 'react'; + +interface Props { + tooltipText: React.ReactNode; +} + +export const DefaultFirewallChip = (props: Props) => { + return ( + + + + ); +}; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx new file mode 100644 index 00000000000..68fa7368856 --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.test.tsx @@ -0,0 +1,53 @@ +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { firewallFactory } from 'src/factories'; +import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { FirewallSelect } from './FirewallSelect'; + +describe('FirewallSelect', () => { + it('renders a default label', () => { + const { getByText } = renderWithTheme(); + + expect(getByText('Firewall')).toBeVisible(); + }); + + it('renders a custom label', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Assign Firewall')).toBeVisible(); + }); + + it('renders an error', () => { + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Firewall is required.')).toBeVisible(); + }); + + it('renders firewalls returned by the API', async () => { + const firewalls = firewallFactory.buildList(3); + + server.use( + http.get('*/v4/networking/firewalls', () => { + return HttpResponse.json(makeResourcePage(firewalls)); + }) + ); + + const { getByLabelText, getByText } = renderWithTheme( + + ); + + await userEvent.click(getByLabelText('Firewall')); + + for (const firewall of firewalls) { + expect(getByText(firewall.label)).toBeVisible(); + } + }); +}); diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx new file mode 100644 index 00000000000..ddd6e66190c --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx @@ -0,0 +1,103 @@ +import { + useAllFirewallsQuery, + useFirewallSettingsQuery, +} from '@linode/queries'; +import { Autocomplete } from '@linode/ui'; +import React, { useMemo } from 'react'; + +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +import { DefaultFirewallChip } from './DefaultFirewallChip'; +import { FirewallSelectOption } from './FirewallSelectOption'; +import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; + +import type { Firewall } from '@linode/api-v4'; +import type { EnhancedAutocompleteProps } from '@linode/ui'; + +interface Props + extends Omit< + EnhancedAutocompleteProps, + 'label' | 'options' | 'value' + > { + disableClearable?: DisableClearable; + /** + * Hide "Default" chips showing which firewalls are defaults + * @default false + */ + hideDefaultChips?: boolean; + /** + * The label applied to the Autocomplete's TextField. + * @default Firewall + */ + label?: string; + /** + * Optionally pass your own array of Firewalls. + * All Firewall will show if this is omitted. + */ + options?: Firewall[]; + /** + * The ID of the selected Firewall + */ + value: null | number | undefined; +} + +/** + * A shared "Firewall Select" component intended to be used when + * a user needs to choose a Firewall + * + * Currently this is only a single select, but can be extended to support more + * Autocomplete features. + */ +export const FirewallSelect = ( + props: Props +) => { + const { errorText, hideDefaultChips, loading, value, ...rest } = props; + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled && !hideDefaultChips, + }); + + const defaultDescription = + firewallSettings && + value && + getDefaultFirewallDescription(value, firewallSettings); + + const isDefault = !!defaultDescription; + + const selectedFirewall = useMemo( + () => firewalls?.find((firewall) => firewall.id === value) ?? null, + [firewalls, value] + ); + + return ( + + renderOption={({ key, ...props }, option, state) => ( + + )} + textFieldProps={{ + InputProps: { + endAdornment: isDefault && !hideDefaultChips && ( + + ), + }, + }} + errorText={errorText ?? error?.[0].reason} + label="Firewall" + loading={isLoading || loading} + noMarginTop + options={firewalls ?? []} + placeholder="None" + value={selectedFirewall!} + {...rest} + /> + ); +}; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx new file mode 100644 index 00000000000..4ebdf5f236a --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx @@ -0,0 +1,50 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; +import { Box, SelectedIcon, Stack } from '@linode/ui'; +import React from 'react'; + +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +import { DefaultFirewallChip } from './DefaultFirewallChip'; +import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; + +import type { Firewall } from '@linode/api-v4'; +import type { AutocompleteRenderOptionState } from '@mui/material'; + +interface Props { + /** + * Hide the "Default" chip from showing + * @default false + */ + hideDefaultChip?: boolean; + listItemProps: React.HTMLAttributes; + option: Firewall; + state: AutocompleteRenderOptionState; +} + +export const FirewallSelectOption = (props: Props) => { + const { hideDefaultChip, listItemProps, option, state } = props; + + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled && !hideDefaultChip, + }); + + const defaultDescription = + firewallSettings && + getDefaultFirewallDescription(option.id, firewallSettings); + + const isDefault = !!defaultDescription; + + return ( +
  • + + {option.label} + + {isDefault && !hideDefaultChip && ( + + )} + {state.selected && } + +
  • + ); +}; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx new file mode 100644 index 00000000000..0d2eef0bb6e --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx @@ -0,0 +1,108 @@ +import { firewallSettingsFactory } from 'src/factories'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { + getDefaultFirewallDescription, + getEntitiesThatFirewallIsDefaultFor, +} from './FirewallSelectOption.utils'; + +describe('getEntitiesThatFirewallIsDefaultFor', () => { + it('returns entities that a firewall is a default for', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: 4, + nodebalancer: 4, + public_interface: 4, + vpc_interface: 1, + }, + }); + + expect(getEntitiesThatFirewallIsDefaultFor(4, firewallSettings)).toEqual([ + 'linode', + 'nodebalancer', + 'public_interface', + ]); + }); + + it('returns an empty array if the firewall is not a default for anything', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: 4, + nodebalancer: 4, + public_interface: 4, + vpc_interface: 4, + }, + }); + + expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( + [] + ); + }); + + it('returns an empty array if the user has no default firewalls set', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: null, + nodebalancer: null, + public_interface: null, + vpc_interface: null, + }, + }); + + expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( + [] + ); + }); +}); + +describe('getDefaultFirewallDescription', () => { + it('returns null if a firewall is not a default for anything', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: 4, + nodebalancer: 4, + public_interface: 4, + vpc_interface: 1, + }, + }); + + expect(getDefaultFirewallDescription(2, firewallSettings)).toEqual(null); + }); + + it('returns human readable text when the firewall is a default for one type of entity', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: 4, + nodebalancer: 4, + public_interface: 4, + vpc_interface: 1, + }, + }); + + const { getByText } = renderWithTheme( + getDefaultFirewallDescription(1, firewallSettings) + ); + + expect(getByText('VPC (Linode Interfaces)')).toBeVisible(); + }); + + it('returns human readable text when the firewall is a default for three types of entities', () => { + const firewallSettings = firewallSettingsFactory.build({ + default_firewall_ids: { + linode: 4, + nodebalancer: 4, + public_interface: 4, + vpc_interface: 1, + }, + }); + + const { getByText, queryByText } = renderWithTheme( + getDefaultFirewallDescription(4, firewallSettings) + ); + + expect(getByText('NodeBalancers')).toBeVisible(); + expect(getByText('Public (Linode Interfaces)')).toBeVisible(); + expect(getByText('Configuration Profile Interfaces')).toBeVisible(); + expect(queryByText('VPC (Linode Interfaces)')).toBeNull(); + }); +}); diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx new file mode 100644 index 00000000000..5fddaeed7c9 --- /dev/null +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx @@ -0,0 +1,85 @@ +import { List, ListItem, Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import type { FirewallSettings } from '@linode/api-v4'; + +export type FirewallDefaultEntity = keyof FirewallSettings['default_firewall_ids']; + +/** + * Maps an entity that supports default firewalls to a readable name. + */ +const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< + FirewallDefaultEntity, + string +> = { + linode: 'Configuration Profile Interfaces', + nodebalancer: 'NodeBalancers', + public_interface: 'Public (Linode Interfaces)', + vpc_interface: 'VPC (Linode Interfaces)', +}; + +/** + * getEntitiesThatFirewallIsDefaultFor + * + * @param firewallId The ID of the Firewall + * @param firewallSettings The account FirewallSettings from the API + * + * @returns An array of entities that this Firewall is a default for. + * @example ['nodebalancer', 'vpc_interface'] + */ +export function getEntitiesThatFirewallIsDefaultFor( + firewallId: number, + firewallSettings: FirewallSettings +) { + const defaultFor: FirewallDefaultEntity[] = []; + + for (const key in firewallSettings.default_firewall_ids) { + const entity = key as FirewallDefaultEntity; + if (firewallSettings.default_firewall_ids[entity] === firewallId) { + defaultFor.push(entity); + } + } + + return defaultFor; +} + +/** + * getDefaultFirewallDescription + * + * @param firewallId The ID of the Firewall + * @param firewallSettings The account FirewallSettings from the API + * + * @returns A human readable list that explains what entities this Firewall is a default for. + * It will return `null` if this Firewall is not a default for anything. + */ +export function getDefaultFirewallDescription( + firewallId: number, + firewallSettings: FirewallSettings +) { + const entitiesThatFirewallIsDefaultFor = getEntitiesThatFirewallIsDefaultFor( + firewallId, + firewallSettings + ); + + if (entitiesThatFirewallIsDefaultFor.length === 0) { + // This means that a Firewall is not a default. + return null; + } + + const readableEntities = entitiesThatFirewallIsDefaultFor.map( + (entity) => FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME[entity] + ); + + return ( + + Default Firewall for: + + {readableEntities.map((entity) => ( + + {entity} + + ))} + + + ); +} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx index 1bf9cd7df3a..2b03e445eab 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Firewall.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Box, Paper, Stack, Typography } from '@linode/ui'; +import { Box, Paper, Stack, Typography } from '@linode/ui'; import React, { useState } from 'react'; import { useController, useFormContext } from 'react-hook-form'; @@ -7,11 +7,11 @@ import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/Ge import { Link } from 'src/components/Link'; import { LinkButton } from 'src/components/LinkButton'; import { FIREWALL_GET_STARTED_LINK } from 'src/constants'; +import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/CreateFirewallDrawer'; import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; -import { useAllFirewallsQuery } from '@linode/queries'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; import { useLinodeCreateQueryParams } from './utilities'; @@ -27,8 +27,6 @@ export const Firewall = () => { 'firewall_id' >({ name: 'firewall_id' }); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isGenerateDialogOpen, setIsGenerateDialogOpen] = React.useState(false); @@ -44,9 +42,6 @@ export const Firewall = () => { globalGrantType: 'add_linodes', }); - const selectedFirewall = - firewalls?.find((firewall) => firewall.id === field.value) ?? null; - const onChange = (firewallId: number | undefined) => { if (firewallId !== undefined) { clearErrors('firewallOverride'); @@ -98,7 +93,7 @@ export const Firewall = () => { /> )} - { onChange(firewall?.id); if (!firewall?.id) { @@ -118,14 +113,11 @@ export const Firewall = () => { } }} disabled={isLinodeCreateRestricted} - errorText={fieldState.error?.message ?? error?.[0].reason} + errorText={fieldState.error?.message} label="Assign Firewall" - loading={isLoading} - noMarginTop onBlur={field.onBlur} - options={firewalls ?? []} placeholder="None" - value={selectedFirewall} + value={field.value} /> { name: 'firewall_id', }); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_linodes', }); - const selectedFirewall = - firewalls?.find((firewall) => firewall.id === field.value) ?? null; - return ( - field.onChange(firewall?.id ?? null)} - options={firewalls ?? []} placeholder="None" - value={selectedFirewall} + value={field.value} /> { const { control, - setValue, resetField, + setValue, } = useFormContext(); const [isCreateDrawerOpen, setIsCreateDrawerOpen] = useState(false); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx index 922778f6154..4d4660800fc 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/AddFirewallForm.tsx @@ -1,15 +1,13 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { - useAddFirewallDeviceMutation, - useAllFirewallsQuery, -} from '@linode/queries'; -import { ActionsPanel, Autocomplete, Stack, Typography } from '@linode/ui'; +import { useAddFirewallDeviceMutation } from '@linode/queries'; +import { ActionsPanel, Stack, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import React from 'react'; import { Controller, useForm } from 'react-hook-form'; import { number, object } from 'yup'; import { Link } from 'src/components/Link'; +import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; import { formattedTypes } from 'src/features/Firewalls/FirewallDetail/Devices/constants'; import type { FirewallDeviceEntityType } from '@linode/api-v4'; @@ -35,12 +33,6 @@ export const AddFirewallForm = (props: Props) => { const entityLabel = formattedTypes[entityType] ?? entityType; - const { - data: firewalls, - error: firewallsError, - isLoading: firewallsLoading, - } = useAllFirewallsQuery(); - const { mutateAsync } = useAddFirewallDeviceMutation(); const form = useForm({ @@ -68,20 +60,15 @@ export const AddFirewallForm = (props: Props) => { ( - field.onChange(value?.id)} - options={firewalls ?? []} placeholder="Select a Firewall" - value={firewalls?.find((f) => f.id === field.value) ?? null} + value={field.value} /> )} control={form.control} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceDrawer.tsx index 87b03bf1461..d6e9d13d3bb 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceDrawer.tsx @@ -9,10 +9,11 @@ interface Props { linodeId: number; onClose: () => void; open: boolean; + regionId: string; } export const AddInterfaceDrawer = (props: Props) => { - const { linodeId, onClose, open } = props; + const { linodeId, onClose, open, regionId } = props; return ( { open={open} title="Add Network Interface" > - + ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx index 698f7023443..70473642500 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/AddInterfaceForm.tsx @@ -12,16 +12,18 @@ import { InterfaceFirewall } from './InterfaceFirewall'; import { InterfaceType } from './InterfaceType'; import { CreateLinodeInterfaceFormSchema } from './utilities'; import { VLANInterface } from './VLANInterface'; +import { VPCInterface } from './VPCInterface'; import type { CreateInterfaceFormValues } from './utilities'; interface Props { linodeId: number; onClose: () => void; + regionId: string; } export const AddInterfaceForm = (props: Props) => { - const { linodeId, onClose } = props; + const { linodeId, onClose, regionId } = props; const { enqueueSnackbar } = useSnackbar(); const { mutateAsync } = useCreateLinodeInterfaceMutation(linodeId); @@ -76,6 +78,9 @@ export const AddInterfaceForm = (props: Props) => { )} {selectedInterfacePurpose === 'vlan' && } + {selectedInterfacePurpose === 'vpc' && ( + + )} diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx index 95e370035df..e697993df09 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceFirewall.tsx @@ -1,31 +1,25 @@ -import { useAllFirewallsQuery } from '@linode/queries'; -import { Autocomplete } from '@linode/ui'; import React from 'react'; import { useController } from 'react-hook-form'; +import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; + import type { CreateInterfaceFormValues } from './utilities'; export const InterfaceFirewall = () => { - const { field, fieldState } = useController({ + const { field, fieldState } = useController< + CreateInterfaceFormValues, + 'firewall_id' + >({ name: 'firewall_id', }); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - - const selectedFirewall = - firewalls?.find((firewall) => firewall.id === field.value) ?? null; - return ( - field.onChange(firewall?.id ?? null)} - options={firewalls ?? []} placeholder="None" - value={selectedFirewall} + value={field.value} /> ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx index 740b34c1d7f..d549b840c53 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/InterfaceType.tsx @@ -1,3 +1,4 @@ +import { firewallQueries } from '@linode/queries'; import { FormControl, FormControlLabel, @@ -5,21 +6,46 @@ import { Radio, RadioGroup, } from '@linode/ui'; +import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import { useController } from 'react-hook-form'; +import { useController, useFormContext } from 'react-hook-form'; + +import { INTERFACE_PURPOSE_TO_DEFAULT_FIREWALL_KEY } from './utilities'; import type { CreateInterfaceFormValues } from './utilities'; +import type { InterfacePurpose } from '@linode/api-v4'; export const InterfaceType = () => { + const queryClient = useQueryClient(); + const { setValue } = useFormContext(); const { field, fieldState } = useController({ name: 'purpose', }); + const onChange = async (value: InterfacePurpose) => { + // Change the selected interface type (Public, VPC, VLAN) + field.onChange(value); + + // Update the form's `firewall_id` based on the defaults + const firewallSettings = await queryClient.ensureQueryData( + firewallQueries.settings + ); + const firewallSettingKey = INTERFACE_PURPOSE_TO_DEFAULT_FIREWALL_KEY[value]; + if (firewallSettingKey) { + setValue( + 'firewall_id', + firewallSettings.default_firewall_ids[firewallSettingKey] + ); + } else { + setValue('firewall_id', null); + } + }; + return ( onChange(value as InterfacePurpose)} sx={{ my: `0 !important` }} value={field.value ?? null} > diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/VPCInterface.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/VPCInterface.tsx new file mode 100644 index 00000000000..e6ab426291b --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/VPCInterface.tsx @@ -0,0 +1,82 @@ +import { useAllVPCsQuery } from '@linode/queries'; +import { Autocomplete, Stack } from '@linode/ui'; +import React from 'react'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; + +import type { CreateInterfaceFormValues } from './utilities'; + +interface Props { + regionId: string; +} + +export const VPCInterface = (props: Props) => { + const { regionId } = props; + const { + control, + resetField, + setValue, + } = useFormContext(); + + const { data: vpcs, error, isLoading } = useAllVPCsQuery({ + filter: { region: regionId }, + }); + + const [vpcId] = useWatch({ control, name: ['vpc.vpc_id'] }); + + const selectedVPC = vpcs?.find((vpc) => vpc.id === vpcId) ?? null; + + return ( + + ( + { + field.onChange(vpc?.id ?? null); + + if (vpc && vpc.subnets.length === 1) { + // If the user selectes a VPC and the VPC only has one subnet, + // preselect that subnet for the user. + setValue('vpc.subnet_id', vpc.subnets[0].id, { + shouldValidate: true, + }); + } else { + // Otherwise, just clear the selected subnet + resetField('vpc.subnet_id'); + } + }} + errorText={fieldState.error?.message ?? error?.[0].reason} + label="VPC" + loading={isLoading} + noMarginTop + onBlur={field.onBlur} + options={vpcs ?? []} + placeholder="Select a VPC" + value={selectedVPC} + /> + )} + control={control} + name="vpc.vpc_id" + /> + ( + s.id === field.value) ?? null + } + disabled={!selectedVPC} + errorText={fieldState.error?.message} + label="Subnet" + loading={isLoading} + noMarginTop + onBlur={field.onBlur} + onChange={(e, subnet) => field.onChange(subnet?.id ?? null)} + options={selectedVPC?.subnets ?? []} + placeholder="Select a Subnet" + /> + )} + control={control} + name="vpc.subnet_id" + /> + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts index 8ef16196c63..a3be2fa7ae2 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/AddInterfaceDrawer/utilities.ts @@ -1,7 +1,9 @@ +import { InterfacePurpose } from '@linode/api-v4'; import { CreateLinodeInterfaceSchema, CreateVPCInterfaceSchema, } from '@linode/validation'; +import { FirewallDefaultEntity } from 'src/features/Firewalls/components/FirewallSelectOption.utils'; import { number, object, string } from 'yup'; import type { InferType } from 'yup'; @@ -23,3 +25,12 @@ export const CreateLinodeInterfaceFormSchema = CreateLinodeInterfaceSchema.conca export type CreateInterfaceFormValues = InferType< typeof CreateLinodeInterfaceFormSchema >; + +export const INTERFACE_PURPOSE_TO_DEFAULT_FIREWALL_KEY: Record< + InterfacePurpose, + FirewallDefaultEntity | null +> = { + public: 'public_interface', + vlan: null, + vpc: 'vpc_interface', +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx index bbb1434f67f..c940b9c0304 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx @@ -7,9 +7,10 @@ import { LinodeInterfacesTable } from './LinodeInterfacesTable'; interface Props { linodeId: number; + regionId: string; } -export const LinodeInterfaces = ({ linodeId }: Props) => { +export const LinodeInterfaces = ({ linodeId, regionId }: Props) => { const [isAddDrawerOpen, setIsAddDrawerOpen] = useState(false); const [isDeleteDrawerOpen, setIsDeleteDrawerOpen] = useState(false); const [selectedInterfaceId, setSelectedInterfaceId] = useState(); @@ -41,6 +42,7 @@ export const LinodeInterfaces = ({ linodeId }: Props) => { linodeId={linodeId} onClose={() => setIsAddDrawerOpen(false)} open={isAddDrawerOpen} + regionId={regionId} /> { {showFirewallsTable && } - {showInterfacesTable && } + {showInterfacesTable && ( + + )} ); diff --git a/packages/queries/src/firewalls/firewalls.ts b/packages/queries/src/firewalls/firewalls.ts index 0e7b89a8247..fea962ac76f 100644 --- a/packages/queries/src/firewalls/firewalls.ts +++ b/packages/queries/src/firewalls/firewalls.ts @@ -5,12 +5,12 @@ import { deleteFirewallDevice, getFirewall, getFirewallDevices, + getFirewallSettings, getFirewalls, getTemplate, getTemplates, updateFirewall, updateFirewallRules, - getFirewallSettings, updateFirewallSettings, } from '@linode/api-v4/lib/firewalls'; import { getAll } from '@linode/utilities'; @@ -36,14 +36,15 @@ import type { FirewallDevice, FirewallDevicePayload, FirewallRules, + FirewallSettings, FirewallTemplate, FirewallTemplateSlug, Params, ResourcePage, UpdateFirewallRules, - FirewallSettings, UpdateFirewallSettings, } from '@linode/api-v4'; +import type { UseQueryOptions } from '@tanstack/react-query'; const getAllFirewallDevices = ( id: number, @@ -285,8 +286,13 @@ export const useFirewallsQuery = (params?: Params, filter?: Filter) => { }); }; -export const useFirewallSettingsQuery = () => { - return useQuery(firewallQueries.settings); +export const useFirewallSettingsQuery = ( + options?: Partial> +) => { + return useQuery({ + ...firewallQueries.settings, + ...options, + }); }; export const useFirewallTemplatesQuery = () => { diff --git a/packages/ui/src/components/Chip/Chip.tsx b/packages/ui/src/components/Chip/Chip.tsx index 18a6b36f144..8f5a56d1f2b 100644 --- a/packages/ui/src/components/Chip/Chip.tsx +++ b/packages/ui/src/components/Chip/Chip.tsx @@ -10,6 +10,8 @@ export interface ChipProps extends _ChipProps { component?: React.ElementType; } -export const Chip = (props: ChipProps) => { - return <_Chip {...props} />; -}; +export const Chip = React.forwardRef( + (props: ChipProps, ref) => { + return <_Chip ref={ref} {...props} />; + } +); diff --git a/packages/utilities/src/factories/linodeInterface.ts b/packages/utilities/src/factories/linodeInterface.ts index bfefe51c182..2e7ae010b73 100644 --- a/packages/utilities/src/factories/linodeInterface.ts +++ b/packages/utilities/src/factories/linodeInterface.ts @@ -18,14 +18,14 @@ export const linodeInterfaceSettingsFactory = Factory.Sync.makeFactory( { - created: '2020-01-01 00:00:00', + created: '2025-03-19T03:58:04', default_route: { ipv4: true, }, id: Factory.each((i) => i), mac_address: 'a4:ac:39:b7:6e:42', public: null, - updated: '2020-01-01 00:00:00', + updated: '2025-03-19T03:58:04', version: 1, vlan: { ipam_address: '192.168.0.1', @@ -37,14 +37,14 @@ export const linodeInterfaceFactoryVlan = Factory.Sync.makeFactory( { - created: '2020-01-01 00:00:00', + created: '2025-03-19T03:58:04', default_route: { ipv4: true, }, id: Factory.each((i) => i), mac_address: 'a4:ac:39:b7:6e:42', public: null, - updated: '2020-01-01 00:00:00', + updated: '2025-03-19T03:58:04', version: 1, vlan: null, vpc: { @@ -69,7 +69,7 @@ export const linodeInterfaceFactoryVPC = Factory.Sync.makeFactory( { - created: '2020-01-01 00:00:00', + created: '2025-03-19T03:58:04', default_route: { ipv4: true, }, @@ -91,7 +91,7 @@ export const linodeInterfaceFactoryPublic = Factory.Sync.makeFactory Date: Tue, 25 Mar 2025 18:56:45 -0400 Subject: [PATCH 24/84] test: [M3-9486, M3-9487, M3-9557] - Allow Linode create tests to pass in alternative environments (#11886) * Delete redundant Linode Create SSH key test * Add "env:premiumPlans" test tag * Apply "env:premiumPlans" tag to Linode premium plan e2e test * Only require "Premium Plans" region capability for Premium Plans Linode create test --- .../pr-11886-tests-1742401040412.md | 5 + .../pr-11886-tests-1742401071717.md | 5 + .../pr-11886-tests-1742401097314.md | 5 + .../e2e/core/linodes/create-linode.spec.ts | 282 +++++------------- packages/manager/cypress/support/util/tag.ts | 4 + 5 files changed, 101 insertions(+), 200 deletions(-) create mode 100644 packages/manager/.changeset/pr-11886-tests-1742401040412.md create mode 100644 packages/manager/.changeset/pr-11886-tests-1742401071717.md create mode 100644 packages/manager/.changeset/pr-11886-tests-1742401097314.md diff --git a/packages/manager/.changeset/pr-11886-tests-1742401040412.md b/packages/manager/.changeset/pr-11886-tests-1742401040412.md new file mode 100644 index 00000000000..983775c4b63 --- /dev/null +++ b/packages/manager/.changeset/pr-11886-tests-1742401040412.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Add `env:premiumPlans` test tag for tests which require premium plan availability ([#11886](https://github.com/linode/manager/pull/11886)) diff --git a/packages/manager/.changeset/pr-11886-tests-1742401071717.md b/packages/manager/.changeset/pr-11886-tests-1742401071717.md new file mode 100644 index 00000000000..9fc0ad99ae3 --- /dev/null +++ b/packages/manager/.changeset/pr-11886-tests-1742401071717.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix Linode create end-to-end test failures against alternative environments ([#11886](https://github.com/linode/manager/pull/11886)) diff --git a/packages/manager/.changeset/pr-11886-tests-1742401097314.md b/packages/manager/.changeset/pr-11886-tests-1742401097314.md new file mode 100644 index 00000000000..f7cfa27e4e0 --- /dev/null +++ b/packages/manager/.changeset/pr-11886-tests-1742401097314.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Delete redundant Linode create SSH key test ([#11886](https://github.com/linode/manager/pull/11886)) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index 7e01e91e1f0..016c8e70916 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -2,26 +2,17 @@ * @file Linode Create end-to-end tests. */ -import { - linodeConfigInterfaceFactory, - linodeConfigInterfaceFactoryWithVPC, - regionFactory, -} from '@linode/utilities'; +import { regionFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; -import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetUser } from 'support/intercepts/account'; -import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { interceptCreateLinode, mockCreateLinode, mockCreateLinodeError, - mockGetLinodeDisks, - mockGetLinodeType, mockGetLinodeTypes, - mockGetLinodeVolumes, } from 'support/intercepts/linodes'; import { interceptGetProfile } from 'support/intercepts/profile'; import { @@ -29,30 +20,22 @@ import { mockGetProfileGrants, } from 'support/intercepts/profile'; import { mockGetRegions } from 'support/intercepts/regions'; -import { mockGetVLANs } from 'support/intercepts/vlans'; -import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { cleanUp } from 'support/util/cleanup'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { getRegionById } from 'support/util/regions'; +import { skip } from 'support/util/skip'; import { - VLANFactory, accountFactory, accountUserFactory, grantsFactory, - linodeConfigFactory, linodeFactory, linodeTypeFactory, profileFactory, - subnetFactory, - vpcFactory, } from 'src/factories'; -import type { Config, Disk, Region, VLAN } from '@linode/api-v4'; - let username: string; authenticate(); @@ -89,11 +72,6 @@ describe('Create Linode', () => { planLabel: 'Linode 24 GB', planType: 'High Memory', }, - { - planId: 'g7-premium-2', - planLabel: 'Premium 4 GB', - planType: 'Premium CPU', - }, // TODO Include GPU plan types. // TODO Include Accelerated plan types (when they're no longer as restricted) ].forEach((planConfig) => { @@ -103,8 +81,9 @@ describe('Create Linode', () => { */ it(`creates a ${planConfig.planType} Linode`, () => { const linodeRegion = chooseRegion({ - capabilities: ['Linodes', 'Premium Plans', 'Vlans'], + capabilities: ['Linodes', 'Vlans'], }); + const linodeLabel = randomLabel(); interceptGetProfile().as('getProfile'); @@ -177,6 +156,84 @@ describe('Create Linode', () => { }); }); }); + + /* + * - Confirms Premium Plan Linode can be created end-to-end. + * - Confirms creation flow, that Linode boots, and that UI reflects status. + */ + it(`creates a Premium CPU Linode`, () => { + cy.tag('env:premiumPlans'); + + // TODO Allow `chooseRegion` to be configured not to throw. + const linodeRegion = (() => { + try { + return chooseRegion({ + capabilities: ['Linodes', 'Premium Plans', 'Vlans'], + }); + } catch { + skip(); + } + return; + })()!; + + const linodeLabel = randomLabel(); + const planId = 'g7-premium-2'; + const planLabel = 'Premium 4 GB'; + const planType = 'Premium CPU'; + + interceptGetProfile().as('getProfile'); + interceptCreateLinode().as('createLinode'); + cy.visitWithLogin('/linodes/create'); + + // Set Linode label, OS, plan type, password, etc. + linodeCreatePage.setLabel(linodeLabel); + linodeCreatePage.selectImage('Debian 12'); + linodeCreatePage.selectRegionById(linodeRegion.id); + linodeCreatePage.selectPlan(planType, planLabel); + linodeCreatePage.setRootPassword(randomString(32)); + + // Confirm information in summary is shown as expected. + cy.get('[data-qa-linode-create-summary]').scrollIntoView(); + cy.get('[data-qa-linode-create-summary]').within(() => { + cy.findByText('Debian 12').should('be.visible'); + cy.findByText(linodeRegion.label).should('be.visible'); + cy.findByText(planLabel).should('be.visible'); + }); + + // Create Linode and confirm it's provisioned as expected. + ui.button + .findByTitle('Create Linode') + .should('be.visible') + .should('be.enabled') + .click(); + + cy.wait('@createLinode').then((xhr) => { + const requestPayload = xhr.request.body; + const responsePayload = xhr.response?.body; + + // Confirm that API request and response contain expected data + expect(requestPayload['label']).to.equal(linodeLabel); + expect(requestPayload['region']).to.equal(linodeRegion.id); + expect(requestPayload['type']).to.equal(planId); + + expect(responsePayload['label']).to.equal(linodeLabel); + expect(responsePayload['region']).to.equal(linodeRegion.id); + expect(responsePayload['type']).to.equal(planId); + + // Confirm that Cloud redirects to details page + cy.url().should('endWith', `/linodes/${responsePayload['id']}`); + }); + + cy.wait('@getProfile').then((xhr) => { + username = xhr.response?.body.username; + }); + + // Confirm toast notification should appear on Linode create. + ui.toast.assertMessage(`Your Linode ${linodeLabel} is being created.`); + cy.findByText('RUNNING', { timeout: LINODE_CREATE_TIMEOUT }).should( + 'be.visible' + ); + }); }); }); @@ -280,181 +337,6 @@ describe('Create Linode', () => { }); }); - it('adds an SSH key to the linode during create flow', () => { - const rootpass = randomString(32); - const sshPublicKeyLabel = randomLabel(); - const randomKey = randomString(400, { - lowercase: true, - numbers: true, - spaces: false, - symbols: false, - uppercase: true, - }); - const sshPublicKey = `ssh-rsa e2etestkey${randomKey} e2etest@linode`; - const linodeLabel = randomLabel(); - const region: Region = getRegionById('us-southeast'); - const diskLabel: string = 'Debian 10 Disk'; - const mockLinode = linodeFactory.build({ - label: linodeLabel, - region: region.id, - type: dcPricingMockLinodeTypes[0].id, - }); - const mockVLANs: VLAN[] = VLANFactory.buildList(2); - const mockSubnet = subnetFactory.build({ - id: randomNumber(2), - label: randomLabel(), - }); - const mockVPC = vpcFactory.build({ - id: randomNumber(), - region: 'us-southeast', - subnets: [mockSubnet], - }); - const mockVPCRegion = regionFactory.build({ - capabilities: ['Linodes', 'VPCs', 'Vlans'], - id: region.id, - label: region.label, - }); - const mockPublicConfigInterface = linodeConfigInterfaceFactory.build({ - ipam_address: null, - purpose: 'public', - }); - const mockVlanConfigInterface = linodeConfigInterfaceFactory.build(); - const mockVpcConfigInterface = linodeConfigInterfaceFactoryWithVPC.build({ - active: true, - purpose: 'vpc', - vpc_id: mockVPC.id, - }); - const mockConfig: Config = linodeConfigFactory.build({ - id: randomNumber(), - interfaces: [ - // The order of this array is significant. Index 0 (eth0) should be public. - mockPublicConfigInterface, - mockVlanConfigInterface, - mockVpcConfigInterface, - ], - }); - const mockDisks: Disk[] = [ - { - created: '2020-08-21T17:26:14', - filesystem: 'ext4', - id: 44311273, - label: diskLabel, - size: 81408, - status: 'ready', - updated: '2020-08-21T17:26:30', - }, - { - created: '2020-08-21T17:26:14', - filesystem: 'swap', - id: 44311274, - label: '512 MB Swap Image', - size: 512, - status: 'ready', - updated: '2020-08-21T17:26:31', - }, - ]; - - // Mock requests to get individual types. - mockGetLinodeType(dcPricingMockLinodeTypes[0]); - mockGetLinodeType(dcPricingMockLinodeTypes[1]); - mockGetLinodeTypes(dcPricingMockLinodeTypes).as('getLinodeTypes'); - - mockGetRegions([mockVPCRegion]).as('getRegions'); - - mockGetVLANs(mockVLANs); - mockGetVPC(mockVPC).as('getVPC'); - mockGetVPCs([mockVPC]).as('getVPCs'); - mockCreateLinode(mockLinode).as('linodeCreated'); - mockGetLinodeConfigs(mockLinode.id, [mockConfig]).as('getLinodeConfigs'); - mockGetLinodeDisks(mockLinode.id, mockDisks).as('getDisks'); - mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); - - // intercept request - cy.visitWithLogin('/linodes/create'); - cy.wait('@getLinodeTypes'); - - cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); - - // Check the 'Backups' add on - cy.get('[data-testid="backups"]').should('be.visible').click(); - ui.regionSelect.find().click().type(`${region.label} {enter}`); - - // Verify VPCs get fetched once a region is selected - cy.wait('@getVPCs'); - - cy.findByText('Shared CPU').click(); - cy.get(`[id="${dcPricingMockLinodeTypes[0].id}"]`).click(); - - // the "VPC" section is present, and the VPC in the same region of - // the linode can be selected. - cy.get('[data-testid="vpc-panel"]') - .should('be.visible') - .within(() => { - cy.contains('Assign this Linode to an existing VPC.').should( - 'be.visible' - ); - // select VPC - cy.findByLabelText('Assign VPC').should('be.visible').focus(); - cy.focused().type(`${mockVPC.label}{downArrow}{enter}`); - // select subnet - cy.findByPlaceholderText('Select Subnet') - .should('be.visible') - .type(`${mockSubnet.label}{downArrow}{enter}`); - }); - - // The drawer opens when clicking "Add an SSH Key" button - ui.button - .findByTitle('Add an SSH Key') - .should('be.visible') - .should('be.enabled') - .click(); - ui.drawer - .findByTitle('Add SSH Key') - .should('be.visible') - .within(() => { - cy.get('[id="label"]').clear(); - cy.focused().type(sshPublicKeyLabel); - - // An alert displays when the format of SSH key is incorrect - cy.get('[id="ssh-public-key"]').clear(); - cy.focused().type('WrongFormatSshKey'); - ui.button - .findByTitle('Add Key') - .should('be.visible') - .should('be.enabled') - .click(); - cy.findAllByText( - 'SSH Key key-type must be ssh-dss, ssh-rsa, ecdsa-sha2-nistp, ssh-ed25519, or sk-ecdsa-sha2-nistp256.' - ).should('be.visible'); - - // Create a new ssh key - cy.get('[id="ssh-public-key"]').clear(); - cy.focused().type(sshPublicKey); - ui.button - .findByTitle('Add Key') - .should('be.visible') - .should('be.enabled') - .click(); - }); - - // When a user creates an SSH key, a toast notification appears that says "Successfully created SSH key." - ui.toast.assertMessage('Successfully created SSH key.'); - - // When a user creates an SSH key, the list of SSH keys for each user updates to show the new key for the signed in user - cy.findByText(sshPublicKeyLabel, { exact: false }).should('be.visible'); - - cy.get('#linode-label').clear(); - cy.focused().type(linodeLabel); - cy.focused().click(); - cy.get('#root-password').type(rootpass); - - ui.button.findByTitle('Create Linode').click(); - - cy.wait('@linodeCreated').its('response.statusCode').should('eq', 200); - cy.findByText(linodeLabel).should('be.visible'); - cy.contains('RUNNING', { timeout: 300000 }).should('be.visible'); - }); - /* * - Confirms error message can show up during Linode create flow. * - Confirms Linode can be created after retry. diff --git a/packages/manager/cypress/support/util/tag.ts b/packages/manager/cypress/support/util/tag.ts index 9be33d52a39..db105961963 100644 --- a/packages/manager/cypress/support/util/tag.ts +++ b/packages/manager/cypress/support/util/tag.ts @@ -7,6 +7,10 @@ const queryRegex = /(?:-|\+)?([^\s]+)/g; * Allowed test tags. */ export type TestTag = + // Environment-related tags. + // Used to identify tests where certain environment-specific features are required. + | 'env:premiumPlans' + // Feature-related tags. // Used to identify tests which deal with a certain feature or features. | 'feat:linodes' From dfa871f041628f593611334e92499e384d1e18b0 Mon Sep 17 00:00:00 2001 From: Purvesh Makode Date: Wed, 26 Mar 2025 11:12:01 +0530 Subject: [PATCH 25/84] refactor: [M3-9617] - Move `doesRegionSupportFeature` to `utilities` package (#11891) * Move `doesRegionSupporFeature` to `utilities` pkg * Added changeset: Move `doesRegionSupportFeature` from `manager` to `utilities` package * Added changeset: Move `doesRegionSupportFeature` from `manager` to `utilities` package --- .../manager/.changeset/pr-11891-removed-1742474181923.md | 5 +++++ .../src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx | 2 +- .../manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.tsx | 4 ++-- .../Linodes/LinodeCreate/VLAN/VLANAvailabilityNotice.tsx | 4 ++-- .../manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx | 4 ++-- .../Linodes/LinodesDetail/LinodeSettings/VPCPanel.tsx | 6 ++++-- .../Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx | 2 +- packages/manager/src/features/Volumes/VolumeCreate.tsx | 2 +- .../utilities/.changeset/pr-11891-added-1742474194986.md | 5 +++++ .../src/helpers}/doesRegionSupportFeature.test.ts | 4 +++- .../src/helpers}/doesRegionSupportFeature.ts | 0 packages/utilities/src/helpers/index.ts | 1 + 12 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 packages/manager/.changeset/pr-11891-removed-1742474181923.md create mode 100644 packages/utilities/.changeset/pr-11891-added-1742474194986.md rename packages/{manager/src/utilities => utilities/src/helpers}/doesRegionSupportFeature.test.ts (85%) rename packages/{manager/src/utilities => utilities/src/helpers}/doesRegionSupportFeature.ts (100%) diff --git a/packages/manager/.changeset/pr-11891-removed-1742474181923.md b/packages/manager/.changeset/pr-11891-removed-1742474181923.md new file mode 100644 index 00000000000..992d02409d4 --- /dev/null +++ b/packages/manager/.changeset/pr-11891-removed-1742474181923.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `doesRegionSupportFeature` from `manager` to `utilities` package ([#11891](https://github.com/linode/manager/pull/11891)) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx index 4365416e97a..c82a85fea5a 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx @@ -1,10 +1,10 @@ import { useRegionsQuery } from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; +import { doesRegionSupportFeature } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; import { useIsAcceleratedPlansEnabled } from 'src/features/components/PlansPanel/utils'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { extendType } from 'src/utilities/extendType'; import { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.tsx index 9a5d8646c46..fee1f68c5d9 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLAN.tsx @@ -1,3 +1,4 @@ +import { useRegionsQuery } from '@linode/queries'; import { Accordion, Stack, @@ -5,14 +6,13 @@ import { TooltipIcon, Typography, } from '@linode/ui'; +import { doesRegionSupportFeature } from '@linode/utilities'; import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { VLANSelect } from 'src/components/VLANSelect'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useRegionsQuery } from '@linode/queries'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { useLinodeCreateQueryParams } from '../utilities'; import { VLANAvailabilityNotice } from './VLANAvailabilityNotice'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLANAvailabilityNotice.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLANAvailabilityNotice.tsx index 1920778b928..cd67f72f2cd 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLANAvailabilityNotice.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VLAN/VLANAvailabilityNotice.tsx @@ -1,10 +1,10 @@ +import { useRegionsQuery } from '@linode/queries'; import { List, ListItem, Notice, Typography } from '@linode/ui'; +import { regionsWithFeature } from '@linode/utilities'; import { styled } from '@mui/material/styles'; import * as React from 'react'; import { TextTooltip } from 'src/components/TextTooltip'; -import { useRegionsQuery } from '@linode/queries'; -import { regionsWithFeature } from 'src/utilities/doesRegionSupportFeature'; import type { Region } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx index f01f31cbf2b..914e308bb7b 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/VPC/VPC.tsx @@ -1,3 +1,4 @@ +import { useAllVPCsQuery, useRegionsQuery } from '@linode/queries'; import { Autocomplete, Box, @@ -11,6 +12,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; +import { doesRegionSupportFeature } from '@linode/utilities'; import React, { useState } from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; @@ -21,9 +23,7 @@ import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP, } from 'src/features/VPCs/constants'; import { VPCCreateDrawer } from 'src/features/VPCs/VPCCreateDrawer/VPCCreateDrawer'; -import { useRegionsQuery, useAllVPCsQuery } from '@linode/queries'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { useLinodeCreateQueryParams } from '../utilities'; import { VPCRanges } from './VPCRanges'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.tsx index 2f82fb923f1..524bae19482 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/VPCPanel.tsx @@ -10,7 +10,10 @@ import { TooltipIcon, Typography, } from '@linode/ui'; -import { scrollErrorIntoView } from '@linode/utilities'; +import { + doesRegionSupportFeature, + scrollErrorIntoView, +} from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; @@ -20,7 +23,6 @@ import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP, } from 'src/features/VPCs/constants'; import { AssignIPRanges } from 'src/features/VPCs/VPCDetail/AssignIPRanges'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import type { ExtendedIP } from 'src/utilities/ipUtils'; diff --git a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx index 8ac176cd43b..08b1cdbd056 100644 --- a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -4,6 +4,7 @@ import { useVolumeTypesQuery, } from '@linode/queries'; import { ActionsPanel, Box, Notice, TextField, Typography } from '@linode/ui'; +import { doesRegionSupportFeature } from '@linode/utilities'; import { CreateVolumeSchema } from '@linode/validation/lib/volumes.schema'; import { useFormik } from 'formik'; import { useSnackbar } from 'notistack'; @@ -22,7 +23,6 @@ import { MAX_VOLUME_SIZE } from 'src/constants'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useEventsPollingActions } from 'src/queries/events/events'; import { sendCreateVolumeEvent } from 'src/utilities/analytics/customEventAnalytics'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { handleFieldErrors, diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index 07ff836f15f..dfee8173bd4 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -18,6 +18,7 @@ import { TooltipIcon, Typography, } from '@linode/ui'; +import { doesRegionSupportFeature } from '@linode/utilities'; import { CreateVolumeSchema } from '@linode/validation/lib/volumes.schema'; import { useTheme } from '@mui/material/styles'; import { useNavigate } from '@tanstack/react-router'; @@ -47,7 +48,6 @@ import { getRestrictedResourceText } from 'src/features/Account/utils'; import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; import { sendCreateVolumeEvent } from 'src/utilities/analytics/customEventAnalytics'; -import { doesRegionSupportFeature } from 'src/utilities/doesRegionSupportFeature'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { getGDPRDetails } from 'src/utilities/formatRegion'; import { diff --git a/packages/utilities/.changeset/pr-11891-added-1742474194986.md b/packages/utilities/.changeset/pr-11891-added-1742474194986.md new file mode 100644 index 00000000000..d052427e514 --- /dev/null +++ b/packages/utilities/.changeset/pr-11891-added-1742474194986.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Added +--- + +Move `doesRegionSupportFeature` from `manager` to `utilities` package ([#11891](https://github.com/linode/manager/pull/11891)) diff --git a/packages/manager/src/utilities/doesRegionSupportFeature.test.ts b/packages/utilities/src/helpers/doesRegionSupportFeature.test.ts similarity index 85% rename from packages/manager/src/utilities/doesRegionSupportFeature.test.ts rename to packages/utilities/src/helpers/doesRegionSupportFeature.test.ts index e6633f276af..366d5eb4120 100644 --- a/packages/manager/src/utilities/doesRegionSupportFeature.test.ts +++ b/packages/utilities/src/helpers/doesRegionSupportFeature.test.ts @@ -1,4 +1,6 @@ -import { regions } from '@linode/utilities'; +import { regions } from '../__data__'; + +import { describe, expect, it } from 'vitest'; import { doesRegionSupportFeature } from './doesRegionSupportFeature'; diff --git a/packages/manager/src/utilities/doesRegionSupportFeature.ts b/packages/utilities/src/helpers/doesRegionSupportFeature.ts similarity index 100% rename from packages/manager/src/utilities/doesRegionSupportFeature.ts rename to packages/utilities/src/helpers/doesRegionSupportFeature.ts diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts index 26a8fb46241..0b5106725cc 100644 --- a/packages/utilities/src/helpers/index.ts +++ b/packages/utilities/src/helpers/index.ts @@ -6,6 +6,7 @@ export * from './arrayToList'; export * from './breakpoints'; export * from './capitalize'; export * from './deepStringTransform'; +export * from './doesRegionSupportFeature'; export * from './downloadFile'; export * from './env'; export * from './escapeRegExp'; From 0111b99c95e911515f2058b10f382cc83eaf6b3b Mon Sep 17 00:00:00 2001 From: Harsh Shankar Rao Date: Wed, 26 Mar 2025 15:08:29 +0530 Subject: [PATCH 26/84] refactor: [M3-8247] - Remove ramda from Utilities (#11861) * refactor: [M3-8247] - Remove ramda from Utilities * Add changeset * Add changeset * updated comment * increase coverage for isNilorEmpty() --- .../pr-11861-removed-1742218921343.md | 5 ++ .../LinodeConfigs/LinodeConfigDialog.tsx | 1 - .../src/utilities/createStringsFromDevices.ts | 13 +++-- .../manager/src/utilities/creditCard.test.ts | 11 ++-- packages/manager/src/utilities/creditCard.ts | 5 +- .../manager/src/utilities/formikErrorUtils.ts | 14 ++--- ...etSelectedOptionFromGroupedOptions.test.ts | 53 ------------------- .../getSelectedOptionFromGroupedOptions.ts | 22 -------- .../src/utilities/isNilOrEmpty.test.ts | 39 ++++++++++++++ .../manager/src/utilities/isNilOrEmpty.ts | 11 ++-- .../src/utilities/mergeDeepRight.test.ts | 49 +++++++++++++++++ .../manager/src/utilities/mergeDeepRight.ts | 39 ++++++++++++++ .../manager/src/utilities/testHelpers.tsx | 4 +- packages/utilities/src/helpers/initWindows.ts | 3 +- 14 files changed, 165 insertions(+), 104 deletions(-) create mode 100644 packages/manager/.changeset/pr-11861-removed-1742218921343.md delete mode 100644 packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts delete mode 100644 packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts create mode 100644 packages/manager/src/utilities/isNilOrEmpty.test.ts create mode 100644 packages/manager/src/utilities/mergeDeepRight.test.ts create mode 100644 packages/manager/src/utilities/mergeDeepRight.ts diff --git a/packages/manager/.changeset/pr-11861-removed-1742218921343.md b/packages/manager/.changeset/pr-11861-removed-1742218921343.md new file mode 100644 index 00000000000..243f2ce2ef8 --- /dev/null +++ b/packages/manager/.changeset/pr-11861-removed-1742218921343.md @@ -0,0 +1,5 @@ +--- +'@linode/manager': Removed +--- + +Ramda from `Utilities` ([#11861](https://github.com/linode/manager/pull/11861)) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 58855edfcb8..15174a2a1a2 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -494,7 +494,6 @@ export const LinodeConfigDialog = (props: Props) => { */ if (config) { const devices = createStringsFromDevices(config.devices); - /* If device slots are populated out of sequential order (e.g. sda and sdb are assigned but no others are until sdf), ascertain the last assigned slot to determine how many diff --git a/packages/manager/src/utilities/createStringsFromDevices.ts b/packages/manager/src/utilities/createStringsFromDevices.ts index 4fe6ffeee5a..509d6eb29d3 100644 --- a/packages/manager/src/utilities/createStringsFromDevices.ts +++ b/packages/manager/src/utilities/createStringsFromDevices.ts @@ -1,7 +1,9 @@ -import { DiskDevice, VolumeDevice } from '@linode/api-v4/lib/linodes'; -import { compose, reduce, toPairs } from 'ramda'; - -import { DevicesAsStrings } from 'src/utilities/createDevicesFromStrings'; +import type { + Devices, + DiskDevice, + VolumeDevice, +} from '@linode/api-v4/lib/linodes'; +import type { DevicesAsStrings } from 'src/utilities/createDevicesFromStrings'; const rdx = ( result: DevicesAsStrings, @@ -31,4 +33,5 @@ const isVolume = ( return typeof (device as VolumeDevice).volume_id === 'number'; }; -export const createStringsFromDevices = compose(reduce(rdx, {}), toPairs); +export const createStringsFromDevices = (devices: Devices) => + Object.entries(devices).reduce(rdx, {}); diff --git a/packages/manager/src/utilities/creditCard.test.ts b/packages/manager/src/utilities/creditCard.test.ts index 5787fab5623..ef4f41196bc 100644 --- a/packages/manager/src/utilities/creditCard.test.ts +++ b/packages/manager/src/utilities/creditCard.test.ts @@ -1,6 +1,5 @@ import { CreditCardSchema } from '@linode/validation'; import { Settings } from 'luxon'; -import { take, takeLast } from 'ramda'; import { formatExpiry, @@ -9,7 +8,7 @@ import { } from './creditCard'; const currentYear = new Date().getFullYear(); -const currentYearFirstTwoDigits = take(2, String(currentYear)); +const currentYearFirstTwoDigits = String(currentYear).slice(0, 2); describe('isCreditCardExpired', () => { describe('given today is 01/01/2019', () => { @@ -89,7 +88,7 @@ describe('credit card expiry date parsing and validation', () => { data: { card_number: '1111111111111111', cvv: '123', - expiry: `09/${takeLast(2, String(currentYear + 19))}`, + expiry: `09/${String(currentYear + 19).slice(-2)}`, }, result: true, }, @@ -97,7 +96,7 @@ describe('credit card expiry date parsing and validation', () => { data: { card_number: '1111111111111111', cvv: '123', - expiry: `09/${takeLast(2, String(currentYear + 1))}`, + expiry: `09/${String(currentYear + 1).slice(-2)}`, }, result: true, }, @@ -108,8 +107,8 @@ describe('credit card expiry date parsing and validation', () => { // We also use currentYear to make sure this test does not fail in many // years down the road. cvv: '123', - // Using takeLast to simulate a user entering the year in a 2 digit format. - expiry: `09/${takeLast(2, String(currentYear + 21))}`, + // Using slice() to simulate a user entering the year in a 2 digit format. + expiry: `09/${String(currentYear + 21).slice(-2)}`, }, result: 'Expiry too far in the future.', }, diff --git a/packages/manager/src/utilities/creditCard.ts b/packages/manager/src/utilities/creditCard.ts index 921c68cae5c..729512996ac 100644 --- a/packages/manager/src/utilities/creditCard.ts +++ b/packages/manager/src/utilities/creditCard.ts @@ -1,5 +1,4 @@ import { DateTime } from 'luxon'; -import { take, takeLast } from 'ramda'; /** * Credit cards generally are valid through the expiry month (inclusive). * @@ -41,7 +40,7 @@ export const isCreditCardExpired = (expDate: string) => { export const formatExpiry = (expiry: string): string => { const expiryData = expiry.split('/'); return expiryData[1].length > 2 - ? `${expiryData[0]}/${takeLast(2, expiryData[1])}` + ? `${expiryData[0]}/${expiryData[1].slice(-2)}` : expiry; }; @@ -56,5 +55,5 @@ export const parseExpiryYear = ( return expiryYear; } - return take(2, String(new Date().getFullYear())) + expiryYear; + return String(new Date().getFullYear()).slice(0, 2) + expiryYear; }; diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index 2ae6cbcb776..a26f1695c2e 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -1,5 +1,3 @@ -import { reverse } from 'ramda'; - import { getAPIErrorOrDefault } from './errorUtils'; import { isNilOrEmpty } from './isNilOrEmpty'; @@ -87,11 +85,13 @@ export const handleFieldErrors = ( callback: (error: unknown) => void, fieldErrors: APIError[] = [] ) => { - const mappedFieldErrors = reverse(fieldErrors).reduce( - (result, { field, reason }) => - field ? { ...result, [field]: reason } : result, - {} - ); + const mappedFieldErrors = [...fieldErrors] + .reverse() + .reduce( + (result, { field, reason }) => + field ? { ...result, [field]: reason } : result, + {} + ); if (!isNilOrEmpty(mappedFieldErrors)) { return callback(mappedFieldErrors); diff --git a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts b/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts deleted file mode 100644 index a08e01ba19d..00000000000 --- a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getSelectedOptionFromGroupedOptions } from './getSelectedOptionFromGroupedOptions'; - -const option1 = { - label: 'Option 1', - value: 'Option 1', -}; - -const option2 = { - label: 'Option 2', - value: 'Option 2', -}; - -const option3 = { - label: 'Volumes Option 1', - value: 'Volumes Option 1', -}; - -const option4 = { - label: 'Volumes Option 2', - value: 'Volumes Option 2', -}; - -const fakeDeviceList = [ - { - label: 'Disks', - options: [option1, option2], - value: 'disks', - }, - { - label: 'Volumes', - options: [option3, option4], - value: 'volumes', - }, -]; - -describe('DeviceSelection', () => { - describe('getSelectedOptionFromGroupedOptions helper method', () => { - it('should retrieve an Item from a set of grouped options', () => { - expect( - getSelectedOptionFromGroupedOptions(option3.value, fakeDeviceList) - ).toBe(option3); - expect( - getSelectedOptionFromGroupedOptions(option2.value, fakeDeviceList) - ).toBe(option2); - }); - - it("should return null if the option isn't found", () => { - expect( - getSelectedOptionFromGroupedOptions('not a real value', fakeDeviceList) - ).toBeNull(); - }); - }); -}); diff --git a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts b/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts deleted file mode 100644 index 4d48704df21..00000000000 --- a/packages/manager/src/utilities/getSelectedOptionFromGroupedOptions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { flatten } from 'ramda'; - -import type { SelectOption } from '@linode/ui'; - -type OptionGroup = { - label: string; - options: SelectOption[]; -}; - -export const getSelectedOptionFromGroupedOptions = ( - selectedValue: string, - options: OptionGroup[] -) => { - if (!selectedValue) { - return null; - } - // Ramda's flatten doesn't seem able to handle the typing issues here, but this returns an array of Item. - const optionsList = (flatten( - options.map((group) => group.options) - ) as unknown) as SelectOption[]; - return optionsList.find((option) => option.value === selectedValue) || null; -}; diff --git a/packages/manager/src/utilities/isNilOrEmpty.test.ts b/packages/manager/src/utilities/isNilOrEmpty.test.ts new file mode 100644 index 00000000000..df210411af6 --- /dev/null +++ b/packages/manager/src/utilities/isNilOrEmpty.test.ts @@ -0,0 +1,39 @@ +import { isNilOrEmpty } from './isNilOrEmpty'; + +describe('isNilOrEmpty function', () => { + it('should return true if variable is null or undefined or empty object', () => { + const x = null; + const y = undefined; + const obj = {}; + const arr: number[] = []; + const set = new Set(); + const map = new Map(); + + expect(isNilOrEmpty(x)).toBe(true); + expect(isNilOrEmpty(y)).toBe(true); + expect(isNilOrEmpty(obj)).toBe(true); + expect(isNilOrEmpty(arr)).toBe(true); + expect(isNilOrEmpty(set)).toBe(true); + expect(isNilOrEmpty(map)).toBe(true); + }); + + it('should return false if variable is of not empty', () => { + const str = 'test'; + const num = 15; + const obj = { key: 'value' }; + + expect(isNilOrEmpty(str)).toBe(false); + expect(isNilOrEmpty(num)).toBe(false); + expect(isNilOrEmpty(obj)).toBe(false); + }); + + it('should return false if an array, set or map is of not empty', () => { + const arr: number[] = [1, 2, 3]; + const set = new Set([1, 2, 3]); + const map = new Map([['key', 'value']]); + + expect(isNilOrEmpty(arr)).toBe(false); + expect(isNilOrEmpty(set)).toBe(false); + expect(isNilOrEmpty(map)).toBe(false); + }); +}); diff --git a/packages/manager/src/utilities/isNilOrEmpty.ts b/packages/manager/src/utilities/isNilOrEmpty.ts index a2189a87b0e..c8535cf0a0f 100644 --- a/packages/manager/src/utilities/isNilOrEmpty.ts +++ b/packages/manager/src/utilities/isNilOrEmpty.ts @@ -1,3 +1,8 @@ -import { isEmpty, isNil } from 'ramda'; - -export const isNilOrEmpty = (v: any) => isNil(v) || isEmpty(v); +export const isNilOrEmpty = (v: null | number | object | string | undefined) => + v === null || + v === undefined || + v === '' || + (typeof v === 'object' && + (v instanceof Set || v instanceof Map + ? v.size === 0 + : Object.keys(v || {}).length === 0)); diff --git a/packages/manager/src/utilities/mergeDeepRight.test.ts b/packages/manager/src/utilities/mergeDeepRight.test.ts new file mode 100644 index 00000000000..9c02cf51499 --- /dev/null +++ b/packages/manager/src/utilities/mergeDeepRight.test.ts @@ -0,0 +1,49 @@ +import { mergeDeepRight } from './mergeDeepRight'; + +describe('mergeDeepRight function', () => { + it('should be able to merge simple objects', () => { + const obj1 = { + errors: { id: 25 }, + }; + + const obj2 = { + errors: { reason: 'error 2' }, + }; + + expect(mergeDeepRight(obj1, obj2)).toStrictEqual({ + errors: { id: 25, reason: 'error 2' }, + }); + }); + it('should be able to pick the second object value for a non object values', () => { + const obj1 = { + data: { id: 25 }, + errors: [{ reason: 'error 1' }, { reason: 'error 2' }], + }; + + const obj2 = { + data: { region: 'us-east' }, + errors: [{ reason: 'error 3' }, { reason: 'error 4' }], + }; + + expect(mergeDeepRight(obj1, obj2)).toStrictEqual({ + data: { id: 25, region: 'us-east' }, + errors: [{ reason: 'error 3' }, { reason: 'error 4' }], + }); + }); + it('should be able to merge deeply nested objects', () => { + const obj1 = { + data: { address: { ipv4: '10.0.0.24', ipv6: '' }, id: 25 }, + errors: [{ reason: 'error 1' }], + }; + + const obj2 = { + data: { address: { ipv4: '192.168.0.2' }, id: 28 }, + errors: [{ reason: 'error 2' }, { reason: 'error 3' }], + }; + + expect(mergeDeepRight(obj1, obj2)).toStrictEqual({ + data: { address: { ipv4: '192.168.0.2', ipv6: '' }, id: 28 }, + errors: [{ reason: 'error 2' }, { reason: 'error 3' }], + }); + }); +}); diff --git a/packages/manager/src/utilities/mergeDeepRight.ts b/packages/manager/src/utilities/mergeDeepRight.ts new file mode 100644 index 00000000000..f4c6d397938 --- /dev/null +++ b/packages/manager/src/utilities/mergeDeepRight.ts @@ -0,0 +1,39 @@ +/** + * Creates a new object with the own properties of the first object merged + * with the own properties of the second object. If a key exists in both objects: and both values are objects, + * the two values will be recursively merged otherwise the value from the second object will be used. + */ +export const mergeDeepRight = < + T extends Record, + U extends Record +>( + obj1: T, + obj2: U +): T & U => { + if (!isObject(obj2)) { + if (!obj2) { + return obj1 as T & U; + } else { + return obj2 as T & U; + } + } + if (!isObject(obj1)) { + if (!obj1) { + return obj2 as T & U; + } else { + return obj1 as T & U; + } + } + + return Object.keys({ ...obj1, ...obj2 }).reduce((acc: any, key: string) => { + const val1 = obj1[key]; + const val2 = obj2[key]; + + acc[key] = mergeDeepRight(val1, val2); + return acc; + }, {} as T & U); +}; + +// using a custom function to check for an object since typescript classifies arrays, dates and maps as type 'object' +const isObject = (obj: object) => + Object.prototype.toString.call(obj) === '[object Object]'; diff --git a/packages/manager/src/utilities/testHelpers.tsx b/packages/manager/src/utilities/testHelpers.tsx index deca3e62169..862628256ee 100644 --- a/packages/manager/src/utilities/testHelpers.tsx +++ b/packages/manager/src/utilities/testHelpers.tsx @@ -12,7 +12,6 @@ import mediaQuery from 'css-mediaquery'; import { Formik } from 'formik'; import { LDProvider } from 'launchdarkly-react-client-sdk'; import { SnackbarProvider } from 'notistack'; -import { mergeDeepRight } from 'ramda'; import * as React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { Provider } from 'react-redux'; @@ -26,6 +25,8 @@ import { setupInterceptors } from 'src/request'; import { migrationRouteTree } from 'src/routes'; import { defaultState, storeFactory } from 'src/store'; +import { mergeDeepRight } from './mergeDeepRight'; + import type { QueryClient } from '@tanstack/react-query'; // TODO: Tanstack Router - replace AnyRouter once migration is complete. import type { AnyRootRoute, AnyRouter } from '@tanstack/react-router'; @@ -78,7 +79,6 @@ interface Options { routePath?: string; theme?: 'dark' | 'light'; } - /** * preference state is necessary for all tests using the * renderWithTheme() helper function, since the whole app is wrapped with diff --git a/packages/utilities/src/helpers/initWindows.ts b/packages/utilities/src/helpers/initWindows.ts index 27780f36fe9..26ef3ec0597 100644 --- a/packages/utilities/src/helpers/initWindows.ts +++ b/packages/utilities/src/helpers/initWindows.ts @@ -1,6 +1,5 @@ import { evenizeNumber } from '@linode/utilities'; import { DateTime } from 'luxon'; -import { sortBy } from 'ramda'; export const initWindows = (timezone: string, unshift?: boolean) => { let windows = [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22].map((hour) => { @@ -14,7 +13,7 @@ export const initWindows = (timezone: string, unshift?: boolean) => { ]; }); - windows = sortBy((window) => window[0], windows); + windows = windows.sort((a, b) => a[0].localeCompare(b[0])); if (unshift) { windows.unshift(['Choose a time', 'Scheduling']); From ac5fd9322dd4bfd5e74ad9082f54cc3ed4d8d374 Mon Sep 17 00:00:00 2001 From: aaleksee-akamai Date: Wed, 26 Mar 2025 14:48:05 +0100 Subject: [PATCH 27/84] feat: [UIE-8600] - IAM RBAC: add new drawer for unassigning role flow (#11893) * feat: [UIE-8600] - IAM RBAC: add new drawer for unassigning role flow * Added changeset: Add a new confirmation dialog for the unassigning role flow in IAM * fix the chip's color for dark theme * fix conflict and small improvements --- ...r-11893-upcoming-features-1742481856731.md | 5 + .../AssignedRolesActionMenu.test.tsx | 76 ++++++++++ .../AssignedRolesActionMenu.tsx | 64 ++++++++ .../AssignedRolesTable/AssignedRolesTable.tsx | 78 ++++------ .../UnassignRoleConfirmationDialog.test.tsx | 139 ++++++++++++++++++ .../UnassignRoleConfirmationDialog.tsx | 93 ++++++++++++ .../src/features/IAM/Shared/utilities.test.ts | 81 ++++++++++ .../src/features/IAM/Shared/utilities.ts | 44 ++++++ .../IAM/Users/UserRoles/AssignedEntities.tsx | 10 +- 9 files changed, 537 insertions(+), 53 deletions(-) create mode 100644 packages/manager/.changeset/pr-11893-upcoming-features-1742481856731.md create mode 100644 packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.test.tsx create mode 100644 packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.tsx create mode 100644 packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx create mode 100644 packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx diff --git a/packages/manager/.changeset/pr-11893-upcoming-features-1742481856731.md b/packages/manager/.changeset/pr-11893-upcoming-features-1742481856731.md new file mode 100644 index 00000000000..ca997e614c9 --- /dev/null +++ b/packages/manager/.changeset/pr-11893-upcoming-features-1742481856731.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add a new confirmation dialog for the unassigning role flow in IAM ([#11893](https://github.com/linode/manager/pull/11893)) diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.test.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.test.tsx new file mode 100644 index 00000000000..79587a4e0d9 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.test.tsx @@ -0,0 +1,76 @@ +import { fireEvent } from '@testing-library/react'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { AssignedRolesActionMenu } from './AssignedRolesActionMenu'; + +import type { ExtendedRoleMap } from '../utilities'; + +const mockOnChangeRole = vi.fn(); +const mockOnUnassignRole = vi.fn(); +const mockOnViewEntities = vi.fn(); + +const mockAccountRole: ExtendedRoleMap = { + access: 'account_access', + description: + 'Access to perform any supported action on all resources in the account', + id: 'account_admin', + name: 'account_admin', + permissions: ['create_linode', 'update_linode', 'update_firewall'], + resource_ids: null, + resource_type: 'account', +}; + +const mockEntityRole: ExtendedRoleMap = { + access: 'resource_access', + description: 'Access to update a linode instance', + id: 'linode_contributor', + name: 'linode_contributor', + permissions: ['update_linode', 'view_linode'], + resource_ids: [12345678], + resource_type: 'linode', +}; + +describe('AssignedRolesActionMenu', () => { + it('should render actions for account access roles correctly', () => { + const { getByRole, queryByText } = renderWithTheme( + + ); + + const actionBtn = getByRole('button'); + expect(actionBtn).toBeInTheDocument(); + fireEvent.click(actionBtn); + + expect(queryByText('Change Role')).toBeInTheDocument(); + expect(queryByText('Unassign Role')).toBeInTheDocument(); + expect(queryByText('Update List of Entities')).not.toBeInTheDocument(); + expect(queryByText('View Entities')).not.toBeInTheDocument(); + }); + + it('should render actions for entity access roles correctly', () => { + const { getByRole, queryByText } = renderWithTheme( + + ); + + // Check if "Manage Access" action is present + const actionBtn = getByRole('button'); + expect(actionBtn).toBeInTheDocument(); + fireEvent.click(actionBtn); + + expect(queryByText('View Entities')).toBeInTheDocument(); + expect(queryByText('Update List of Entities')).toBeInTheDocument(); + expect(queryByText('Change Role')).toBeInTheDocument(); + expect(queryByText('Unassign Role')).toBeInTheDocument(); + }); +}); diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.tsx new file mode 100644 index 00000000000..b79e160a9ae --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesActionMenu.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; + +import type { ExtendedRoleMap } from '../utilities'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; + +interface Props { + handleChangeRole: (role: ExtendedRoleMap) => void; + handleUnassignRole: (role: ExtendedRoleMap) => void; + handleViewEntities: (role: string) => void; + role: ExtendedRoleMap; +} + +export const AssignedRolesActionMenu = ({ + handleChangeRole, + handleUnassignRole, + handleViewEntities, + role, +}: Props) => { + const accountMenu: Action[] = [ + { + onClick: () => { + handleChangeRole(role); + }, + title: 'Change Role', + }, + { + onClick: () => { + handleUnassignRole(role); + }, + title: 'Unassign Role', + }, + ]; + + const entitiesMenu: Action[] = [ + { + onClick: () => handleViewEntities(role.name), + title: 'View Entities', + }, + { + onClick: () => { + // mock + }, + title: 'Update List of Entities', + }, + { + onClick: () => { + handleChangeRole(role); + }, + title: 'Change Role', + }, + { + onClick: () => { + handleUnassignRole(role); + }, + title: 'Unassign Role', + }, + ]; + + const actions = role.access === 'account_access' ? accountMenu : entitiesMenu; + + return ; +}; diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx index f4ed21636f7..4766c84c54c 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/AssignedRolesTable.tsx @@ -4,11 +4,11 @@ import { StyledLinkButton, Typography, } from '@linode/ui'; +import { capitalize, truncate } from '@linode/utilities'; import { Grid, useTheme } from '@mui/material'; import React from 'react'; import { useHistory, useParams } from 'react-router-dom'; -import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { CollapsibleTable } from 'src/components/CollapsibleTable/CollapsibleTable'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { TableCell } from 'src/components/TableCell'; @@ -22,6 +22,7 @@ import { } from 'src/queries/iam/iam'; import { useAccountResources } from 'src/queries/resources/resources'; +import { AssignedEntities } from '../../Users/UserRoles/AssignedEntities'; import { Permissions } from '../Permissions/Permissions'; import { addResourceNamesToRoles, @@ -30,14 +31,13 @@ import { mapEntityTypes, mapRolesToPermissions, } from '../utilities'; -import { AssignedEntities } from '../../Users/UserRoles/AssignedEntities'; +import { AssignedRolesActionMenu } from './AssignedRolesActionMenu'; +import { ChangeRoleDrawer } from './ChangeRoleDrawer'; +import { UnassignRoleConfirmationDialog } from './UnassignRoleConfirmationDialog'; import type { EntitiesType, ExtendedRoleMap, RoleMap } from '../utilities'; import type { AccountAccessType, RoleType } from '@linode/api-v4'; -import type { Action } from 'src/components/ActionMenu/ActionMenu'; import type { TableItem } from 'src/components/CollapsibleTable/CollapsibleTable'; -import { capitalize, truncate } from '@linode/utilities'; -import { ChangeRoleDrawer } from './ChangeRoleDrawer'; export const AssignedRolesTable = () => { const { username } = useParams<{ username: string }>(); @@ -50,12 +50,21 @@ export const AssignedRolesTable = () => { setIsChangeRoleDrawerOpen, ] = React.useState(false); const [selectedRole, setSelectedRole] = React.useState(); + const [ + isUnassignRoleDialogOpen, + setIsUnassignRoleDialogOpen, + ] = React.useState(false); const handleChangeRole = (role: ExtendedRoleMap) => { setIsChangeRoleDrawerOpen(true); setSelectedRole(role); }; + const handleUnassignRole = (role: ExtendedRoleMap) => { + setIsUnassignRoleDialogOpen(true); + setSelectedRole(role); + }; + const { data: accountPermissions, isLoading: accountPermissionsLoading, @@ -92,7 +101,7 @@ export const AssignedRolesTable = () => { const [showFullDescription, setShowFullDescription] = React.useState(false); - const handleClick = (roleName: AccountAccessType | RoleType) => { + const handleViewEntities = (roleName: AccountAccessType | RoleType) => { const selectedRole = roleName; history.push({ pathname: `/iam/users/${username}/entities`, @@ -109,49 +118,6 @@ export const AssignedRolesTable = () => { }); return filteredRoles.map((role: ExtendedRoleMap) => { - const accountMenu: Action[] = [ - { - onClick: () => { - handleChangeRole(role); - }, - title: 'Change Role', - }, - { - onClick: () => { - // mock - }, - title: 'Unassign Role', - }, - ]; - - const entitiesMenu: Action[] = [ - { - onClick: () => handleClick(role.name), - title: 'View Entities', - }, - { - onClick: () => { - // mock - }, - title: 'Update List of Entities', - }, - { - onClick: () => { - handleChangeRole(role); - }, - title: 'Change Role', - }, - { - onClick: () => { - // mock - }, - title: 'Unassign Role', - }, - ]; - - const actions = - role.access === 'account_access' ? accountMenu : entitiesMenu; - const OuterTableCells = ( <> {role.access === 'account_access' ? ( @@ -166,13 +132,18 @@ export const AssignedRolesTable = () => { )} - + ); @@ -307,6 +278,11 @@ export const AssignedRolesTable = () => { open={isChangeRoleDrawerOpen} role={selectedRole} /> + setIsUnassignRoleDialogOpen(false)} + open={isUnassignRoleDialogOpen} + role={selectedRole} + /> ); }; diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx new file mode 100644 index 00000000000..91a5c295ec7 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.test.tsx @@ -0,0 +1,139 @@ +import { fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { accountPermissionsFactory } from 'src/factories/accountPermissions'; +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { UnassignRoleConfirmationDialog } from './UnassignRoleConfirmationDialog'; + +import type { ExtendedRoleMap } from '../utilities'; + +const mockRole: ExtendedRoleMap = { + access: 'account_access', + description: + 'Access to perform any supported action on all resources in the account', + id: 'account_admin', + name: 'account_admin', + permissions: ['create_linode', 'update_linode', 'update_firewall'], + resource_ids: null, + resource_type: 'account', +}; + +const props = { + onClose: vi.fn(), + onSuccess: vi.fn(), + open: true, + role: mockRole, +}; + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ username: 'test_user' }), + }; +}); + +const queryMocks = vi.hoisted(() => ({ + useAccountPermissions: vi.fn().mockReturnValue({}), + useAccountUserPermissions: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/iam/iam', async () => { + const actual = await vi.importActual('src/queries/iam/iam'); + return { + ...actual, + useAccountPermissions: queryMocks.useAccountPermissions, + useAccountUserPermissions: queryMocks.useAccountUserPermissions, + }; +}); + +const mockDeleteUserRole = vi.fn(); +vi.mock('@linode/api-v4', async () => { + return { + ...(await vi.importActual('@linode/api-v4')), + updateUserPermissions: (username: string, data: any) => { + mockDeleteUserRole(data); + return Promise.resolve(props); + }, + }; +}); + +describe('UnassignRoleConfirmationDialog', () => { + it('should render', () => { + const { getAllByRole, getByText } = renderWithTheme( + + {' '} + + ); + + const headerText = getByText('Unassign the account_admin role?'); + expect(headerText).toBeVisible(); + + const paragraph = getByText(/You’re about to remove the/i).closest('p'); + + expect(paragraph).toBeInTheDocument(); + expect(paragraph).toHaveTextContent(/account_admin/i); + expect(paragraph).toHaveTextContent(/test_user/i); + expect( + getByText(/The change will be applied immediately./i) + ).toBeInTheDocument(); + + const buttons = getAllByRole('button'); + expect(buttons?.length).toBe(3); + }); + + it('calls the corresponding functions when buttons are clicked', async () => { + const { getByText } = renderWithTheme( + + ); + + const deleteButton = getByText('Remove'); + expect(deleteButton).toBeVisible(); + + const cancelButton = getByText('Cancel'); + expect(cancelButton).toBeVisible(); + fireEvent.click(cancelButton); + expect(props.onClose).toHaveBeenCalled(); + }); + + it('should allow unassign `account_admin` role', async () => { + queryMocks.useAccountUserPermissions.mockReturnValue({ + data: { + account_access: ['account_linode_admin', 'account_admin'], + resource_access: [ + { + resource_id: 12345678, + resource_type: 'linode', + roles: ['linode_contributor'], + }, + ], + }, + }); + + queryMocks.useAccountPermissions.mockReturnValue({ + data: accountPermissionsFactory.build(), + }); + + const { getByText } = renderWithTheme( + + ); + + await userEvent.click(getByText('Remove')); + + await waitFor(() => { + expect(mockDeleteUserRole).toHaveBeenCalledWith({ + account_access: ['account_linode_admin'], + resource_access: [ + { + resource_id: 12345678, + resource_type: 'linode', + roles: ['linode_contributor'], + }, + ], + }); + }); + }); +}); diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx new file mode 100644 index 00000000000..cfec1cb9a86 --- /dev/null +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/UnassignRoleConfirmationDialog.tsx @@ -0,0 +1,93 @@ +import { ActionsPanel, Notice, Typography } from '@linode/ui'; +import { useSnackbar } from 'notistack'; +import React from 'react'; +import { useParams } from 'react-router-dom'; + +import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; +import { + useAccountUserPermissions, + useAccountUserPermissionsMutation, +} from 'src/queries/iam/iam'; + +import { deleteUserRole } from '../utilities'; + +import type { ExtendedRoleMap } from '../utilities'; + +interface Props { + onClose: () => void; + onSuccess?: () => void; + open: boolean; + role: ExtendedRoleMap | undefined; +} + +export const UnassignRoleConfirmationDialog = (props: Props) => { + const { onClose: _onClose, onSuccess, open, role } = props; + const { username } = useParams<{ username: string }>(); + + const { enqueueSnackbar } = useSnackbar(); + + const { + error, + isPending, + mutateAsync: updateUserPermissions, + reset, + } = useAccountUserPermissionsMutation(username); + + const { data: assignedRoles } = useAccountUserPermissions(username ?? ''); + + const onClose = () => { + reset(); // resets the error state of the useMutation + _onClose(); + }; + + const onDelete = async () => { + const initialRole = role?.name; + const access = role?.access; + + const updatedUserRoles = deleteUserRole({ + access, + assignedRoles, + initialRole, + }); + + await updateUserPermissions(updatedUserRoles); + + enqueueSnackbar(`Role ${role?.name} has been deleted successfully.`, { + variant: 'success', + }); + if (onSuccess) { + onSuccess(); + } + onClose(); + }; + + return ( + + } + error={error?.[0].reason} + onClose={onClose} + open={open} + title={`Unassign the ${role?.name} role?`} + > + + + You’re about to remove the {role?.name} role from{' '} + {username}. The change will be applied immediately. + + + + ); +}; diff --git a/packages/manager/src/features/IAM/Shared/utilities.test.ts b/packages/manager/src/features/IAM/Shared/utilities.test.ts index cc1fd2ac7f0..011168a1bd2 100644 --- a/packages/manager/src/features/IAM/Shared/utilities.test.ts +++ b/packages/manager/src/features/IAM/Shared/utilities.test.ts @@ -1,5 +1,8 @@ +import { userPermissionsFactory } from 'src/factories/userPermissions'; + import { combineRoles, + deleteUserRole, getAllRoles, getRoleByName, mapRolesToPermissions, @@ -201,3 +204,81 @@ describe('updateUserRoles', () => { ).toEqual(expectedRoles); }); }); + +describe('deleteUserRole', () => { + it('should return an object of updated users roles with resource access', () => { + const initialRole = 'linode_contributor'; + + const expectedRoles = { + account_access: ['account_linode_admin', 'linode_creator'], + resource_access: [], + }; + + expect( + deleteUserRole({ + access: resourceAccess, + assignedRoles: userPermissions, + initialRole, + }) + ).toEqual(expectedRoles); + }); + + it('should return an object of updated users roles with resource access', () => { + const initialRole = 'linode_contributor'; + + const userPermissions = userPermissionsFactory.build(); + + const expectedRoles = { + account_access: [ + 'account_linode_admin', + 'linode_creator', + 'firewall_creator', + 'account_admin', + 'account_viewer', + ], + resource_access: [ + { + resource_id: 23456789, + resource_type: 'linode', + roles: ['linode_viewer'], + }, + { + resource_id: 45678901, + resource_type: 'firewall', + roles: ['update_firewall'], + }, + ], + }; + + expect( + deleteUserRole({ + access: resourceAccess, + assignedRoles: userPermissions, + initialRole, + }) + ).toEqual(expectedRoles); + }); + + it('should return an object of updated users roles with account access', () => { + const initialRole = 'account_linode_admin'; + + const expectedRoles = { + account_access: ['linode_creator'], + resource_access: [ + { + resource_id: 12345678, + resource_type: 'linode', + roles: ['linode_contributor'], + }, + ], + }; + + expect( + deleteUserRole({ + access: accountAccess, + assignedRoles: userPermissions, + initialRole, + }) + ).toEqual(expectedRoles); + }); +}); diff --git a/packages/manager/src/features/IAM/Shared/utilities.ts b/packages/manager/src/features/IAM/Shared/utilities.ts index 171e8eaeca0..8f65642f3e5 100644 --- a/packages/manager/src/features/IAM/Shared/utilities.ts +++ b/packages/manager/src/features/IAM/Shared/utilities.ts @@ -435,3 +435,47 @@ export interface AssignNewRoleFormValues { role: RolesType | null; }[]; } +interface DeleteUserRolesProps { + access?: 'account_access' | 'resource_access'; + assignedRoles?: IamUserPermissions; + initialRole?: string; +} + +export const deleteUserRole = ({ + access, + assignedRoles, + initialRole, +}: DeleteUserRolesProps): IamUserPermissions => { + if (!assignedRoles) { + return { + account_access: [], + resource_access: [], + }; + } + + if (access === 'account_access') { + return { + ...assignedRoles, + account_access: assignedRoles.account_access.filter( + (role: AccountAccessType) => role !== initialRole + ), + }; + } + + if (access === 'resource_access') { + return { + ...assignedRoles, + resource_access: assignedRoles.resource_access + .map((resource: ResourceAccess) => ({ + ...resource, + roles: resource.roles.filter( + (role: RoleType) => role !== initialRole + ), + })) + .filter((resource: ResourceAccess) => resource.roles.length > 0), + }; + } + + // If access type is invalid, return unchanged object + return assignedRoles; +}; diff --git a/packages/manager/src/features/IAM/Users/UserRoles/AssignedEntities.tsx b/packages/manager/src/features/IAM/Users/UserRoles/AssignedEntities.tsx index 94cbb7fdbce..a31a94e2d51 100644 --- a/packages/manager/src/features/IAM/Users/UserRoles/AssignedEntities.tsx +++ b/packages/manager/src/features/IAM/Users/UserRoles/AssignedEntities.tsx @@ -54,7 +54,10 @@ export const AssignedEntities = ({ > Date: Wed, 26 Mar 2025 14:53:44 +0100 Subject: [PATCH 28/84] upcoming: [UIE-8515] - DBaaS: Advanced Configuration - Drawer with existing configs (#11812) * feat: [UIE-8515] - DBaaS: Advanced Configuration - Drawer with existing configs * feat: [UIE-8515] - update mock data * Added changeset: DBaaS Advanced Configurations: added `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs * Added changeset: DBaaS Advanced Configurations: added UI for existing engine options in the drawer * upcoming: [UIE-8515] - review fix * upcoming: [UIE-8515] - style update, add link * update styles after token re-organization --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> --- .../pr-11812-added-1741611699926.md | 5 + packages/api-v4/src/databases/databases.ts | 13 ++ packages/api-v4/src/databases/types.ts | 50 +++--- ...r-11812-upcoming-features-1741612097921.md | 5 + packages/manager/src/factories/databases.ts | 113 ++++++++++++- .../DatabaseAdvancedConfiguration.style.ts | 29 +++- .../DatabaseAdvancedConfiguration.tsx | 7 +- .../DatabaseAdvancedConfigurationDrawer.tsx | 158 +++++++++++++++--- .../DatabaseConfigurationItem.style.ts | 25 +++ .../DatabaseConfigurationItem.tsx | 142 ++++++++++++++++ .../DatabaseConfigurationSelect.tsx | 118 ++++++------- .../src/features/Databases/utilities.test.ts | 106 ++++++++++++ .../src/features/Databases/utilities.ts | 81 +++++++++ packages/manager/src/mocks/serverHandlers.ts | 4 + .../src/queries/databases/databases.ts | 15 ++ 15 files changed, 754 insertions(+), 117 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11812-added-1741611699926.md create mode 100644 packages/manager/.changeset/pr-11812-upcoming-features-1741612097921.md create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.style.ts create mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx diff --git a/packages/api-v4/.changeset/pr-11812-added-1741611699926.md b/packages/api-v4/.changeset/pr-11812-added-1741611699926.md new file mode 100644 index 00000000000..ca5da6062ef --- /dev/null +++ b/packages/api-v4/.changeset/pr-11812-added-1741611699926.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +DBaaS Advanced Configurations: added `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs ([#11812](https://github.com/linode/manager/pull/11812)) diff --git a/packages/api-v4/src/databases/databases.ts b/packages/api-v4/src/databases/databases.ts index 374ed3c6519..08edf17bf50 100644 --- a/packages/api-v4/src/databases/databases.ts +++ b/packages/api-v4/src/databases/databases.ts @@ -23,6 +23,7 @@ import { SSLFields, UpdateDatabasePayload, DatabaseFork, + DatabaseEngineConfig, } from './types'; /** @@ -346,3 +347,15 @@ export const resumeDatabase = (engine: Engine, databaseID: number) => ), setMethod('POST') ); + +/** + * getConfig + * + * Return detailed list of all the configuration options + * + */ +export const getDatabaseEngineConfig = (engine: Engine) => + Request( + setURL(`${API_ROOT}/databases/${encodeURIComponent(engine)}/config`), + setMethod('GET') + ); diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 180cf098dc6..c941fede132 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -50,7 +50,31 @@ export interface DatabaseBackup { label: string; created: string; } - +export interface ConfigurationItem { + description?: string; + example?: string | number | boolean; + minimum?: number; // min value for the number input + maximum?: number; // max value for the number input + maxLength?: number; // max length for the text input + minLength?: number; // min length for the text input + pattern?: string; + type?: string | number | boolean | [string, null] | string[]; + enum?: string[]; + restart_cluster?: boolean; +} + +export type ConfigValue = number | string | boolean; + +export type ConfigCategoryValues = Record; +export interface DatabaseEngineConfig { + engine_config: Record< + string, + Record | ConfigurationItem + >; +} +export interface DatabaseInstanceAdvancedConfig { + [category: string]: ConfigCategoryValues | ConfigValue; +} export interface DatabaseFork { source: number; restore_time?: string; @@ -70,26 +94,7 @@ interface DatabaseHosts { export interface SSLFields { ca_certificate: string; } -// TODO: This will be changed in the next PR -export interface MySQLAdvancedConfig { - binlog_retention_period?: number; - advanced?: { - connect_timeout?: number; - default_time_zone?: string; - group_concat_max_len?: number; - information_schema_stats_expiry?: number; - innodb_print_all_deadlocks?: boolean; - sql_mode?: string; - }; -} -// TODO: This will be changed in the next PR -export interface PostgresAdvancedConfig { - advanced?: { - max_files_per_process?: number; - timezone?: string; - pg_stat_monitor_enable?: boolean; - }; -} + type MemberType = 'primary' | 'failover'; // DatabaseInstance is the interface for the shape of data returned by the /databases/instances endpoint. @@ -118,7 +123,7 @@ export interface DatabaseInstance { updated: string; updates: UpdatesSchedule; version: string; - engine_config?: MySQLAdvancedConfig | PostgresAdvancedConfig; + engine_config: DatabaseInstanceAdvancedConfig; } export type ClusterSize = 1 | 2 | 3; @@ -231,4 +236,5 @@ export interface UpdateDatabasePayload { updates?: UpdatesSchedule; type?: string; version?: string; + engine_config?: DatabaseInstanceAdvancedConfig; } diff --git a/packages/manager/.changeset/pr-11812-upcoming-features-1741612097921.md b/packages/manager/.changeset/pr-11812-upcoming-features-1741612097921.md new file mode 100644 index 00000000000..63acc4803f7 --- /dev/null +++ b/packages/manager/.changeset/pr-11812-upcoming-features-1741612097921.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +DBaaS Advanced Configurations: added UI for existing engine options in the drawer ([#11812](https://github.com/linode/manager/pull/11812)) diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index 5495121a1f5..40289545a2f 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -6,6 +6,7 @@ import type { Database, DatabaseBackup, DatabaseEngine, + DatabaseEngineConfig, DatabaseInstance, DatabaseStatus, DatabaseType, @@ -207,13 +208,16 @@ export const databaseFactory = Factory.Sync.makeFactory({ advanced: { connect_timeout: 10, default_time_zone: '+03:00', - group_concat_max_len: 4, - information_schema_stats_expiry: 900, innodb_print_all_deadlocks: true, + service_log: false, sql_mode: 'ANSI,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,STRICT_ALL_TABLES', }, binlog_retention_period: 600, + pg_stat_statements: { + track: 'all', + }, + password_encryption: 'scram-sha-256', }, hosts: Factory.each((i) => adb10(i) @@ -275,3 +279,108 @@ export const databaseEngineFactory = Factory.Sync.makeFactory({ id: Factory.each((i) => `test/${i}`), version: Factory.each((i) => `${i}`), }); + +export const databaseEngineConfigFactory = Factory.Sync.makeFactory( + { + engine_config: { + advanced: { + connect_timeout: { + description: + 'The number of seconds that the mysqld server waits for a connect packet before responding with Bad handshake', + example: 10, + maximum: 3600, + minimum: 2, + restart_cluster: false, + type: 'integer', + }, + default_time_zone: { + description: + "Default server time zone as an offset from UTC (from -12:00 to +12:00), a time zone name, or 'SYSTEM' to use the MySQL server default.", + example: '+03:00', + maxLength: 100, + minLength: 2, + pattern: '^([-+][\\d:]*|[\\w/]*)$', + restart_cluster: false, + type: 'string', + }, + innodb_print_all_deadlocks: { + description: + 'When enabled, information about all deadlocks in InnoDB user transactions is recorded in the error log. Disabled by default.', + example: true, + restart_cluster: false, + type: 'boolean', + }, + log_output: { + description: + 'The slow log output destination when slow_query_log is ON. To enable MySQL AI Insights, choose INSIGHTS. To use MySQL AI Insights and the mysql.slow_log table at the same time, choose INSIGHTS,TABLE. To only use the mysql.slow_log table, choose TABLE. To silence slow logs, choose NONE.', + enum: ['INSIGHTS', 'NONE', 'TABLE', 'INSIGHTS,TABLE'], + example: 'INSIGHTS', + restart_cluster: false, + type: 'string', + }, + sql_mode: { + description: + 'Global SQL mode. Set to empty to use MySQL server defaults. When creating a new service and not setting this field Aiven default SQL mode (strict, SQL standard compliant) will be assigned.', + example: 'ANSI,TRADITIONAL', + maxLength: 1024, + pattern: '^[A-Z_]*(,[A-Z_]+)*$', + restart_cluster: true, + type: 'string', + }, + }, + binlog_retention_period: { + description: + 'The minimum amount of time in seconds to keep binlog entries before deletion. This may be extended for services that require binlog entries for longer than the default for example if using the MySQL Debezium Kafka connector.', + example: 600, + maximum: 86400, + minimum: 600, + restart_cluster: false, + type: 'integer', + }, + password_encryption: { + description: 'Chooses the algorithm for encrypting passwords.', + enum: ['md5', 'scram-sha-256'], + example: 'scram-sha-256', + restart_cluster: false, + type: ['string', 'null'], + }, + pg_stat_monitor_enable: { + description: + 'Enable the pg_stat_monitor extension. Enabling this extension will cause the cluster to be restarted. When this extension is enabled, pg_stat_statements results for utility commands are unreliable', + restart_cluster: true, + type: 'boolean', + }, + pg_stat_statements: { + track: { + description: + 'Controls which statements are counted. Specify top to track top-level statements (those issued directly by clients), all to also track nested statements (such as statements invoked within functions), or none to disable statement statistics collection. The default value is top.', + enum: ['all', 'top', 'none'], + restart_cluster: false, + type: ['string'], + }, + }, + pgbouncer: { + autodb_idle_timeout: { + example: 3600, + maximum: 86400, + minimum: 0, + restart_cluster: false, + type: 'integer', + }, + autodb_pool_mode: { + enum: ['transaction', 'session', 'statement'], + example: 'session', + restart_cluster: false, + type: 'string', + }, + }, + service_log: { + description: + 'Store logs for the service so that they are available in the HTTP API and console.', + example: true, + restart_cluster: false, + type: ['boolean', 'null'], + }, + }, + } +); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.style.ts index 1e7d6347f28..43d8e2811d7 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.style.ts +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.style.ts @@ -5,8 +5,29 @@ import { StyledValueGrid } from '../DatabaseSummary/DatabaseSummaryClusterConfig export const StyledConfigValue = styled(StyledValueGrid, { label: 'StyledValueGrid', })(({ theme }) => ({ - padding: `${theme.spacing(0.5)} - ${theme.spacing(1.9)} - ${theme.spacing(0.5)} - ${theme.spacing(0.8)}`, + padding: `${theme.tokens.spacing.S4} ${theme.tokens.spacing.S6}`, +})); + +export const GroupHeader = styled('div')(({ theme }) => ({ + background: theme.tokens.alias.Background.Neutral, + color: + theme.palette.mode === 'dark' + ? theme.tokens.color.Neutrals[5] + : theme.tokens.color.Neutrals[100], + font: theme.tokens.alias.Typography.Label.Bold.Xs, + padding: '8px 12px', + position: 'sticky', + textTransform: 'uppercase', + top: 0, + zIndex: 1, +})); +export const GroupItems = styled('ul')(({ theme }) => ({ + '& li': { + color: + theme.palette.mode === 'dark' + ? theme.tokens.color.Neutrals[5] + : theme.tokens.color.Neutrals[100], + font: theme.tokens.alias.Typography.Label.Regular.Xs, + }, + padding: 0, })); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx index de94c2ff13d..b2bbc24618f 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfiguration.tsx @@ -35,8 +35,9 @@ export const DatabaseAdvancedConfiguration = ({ database }: Props) => { Advanced Configuration Advanced parameters to configure your database cluster.{' '} - {/* TODO: update link when it's ready */} - Learn more. + + Learn more. + + + - {!engineConfigs && ( + {existingConfigsArray.length > 0 && + existingConfigsArray.map((option) => ( + ( + + )} + control={control} + key={option.label} + name={option.label} + /> + ))} + {existingConfigsArray.length === 0 && ( No advanced configurations have been added. @@ -59,7 +175,9 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { ({ + marginBottom: theme.tokens.spacing.S12, +})); + +export const StyledBox = styled(Box, { + label: 'StyledBox', +})(({ theme }) => ({ + background: theme.tokens.alias.Background.Neutral, + padding: theme.tokens.spacing.S8, + width: '100%', +})); + +export const StyledChip = styled(Chip, { + label: 'StyledChip', +})(({ theme }) => ({ + backgroundColor: theme.tokens.color.Amber[5], + color: theme.tokens.alias.Accent.Warning.Primary, + font: theme.tokens.alias.Typography.Heading.Overline, + textTransform: theme.tokens.alias.Typography.Heading.OverlineTextCase, +})); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx new file mode 100644 index 00000000000..272bb1f1da6 --- /dev/null +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx @@ -0,0 +1,142 @@ +import { + Autocomplete, + FormControlLabel, + TextField, + Toggle, + Typography, +} from '@linode/ui'; +import React from 'react'; + +import { formatConfigValue } from '../../utilities'; +import { + StyledBox, + StyledChip, + StyledWrapper, +} from './DatabaseConfigurationItem.style'; + +import type { ConfigurationOption } from './DatabaseConfigurationSelect'; +import type { ConfigValue } from '@linode/api-v4'; + +interface Props { + configItem?: ConfigurationOption; + configValue?: ConfigValue; + engine: string; + errorText: string | undefined; + onChange: (config: ConfigValue) => void; +} + +export const DatabaseConfigurationItem = (props: Props) => { + const { configItem, configValue, engine, errorText, onChange } = props; + const configLabel = configItem?.label || ''; + + const renderInputField = () => { + if ( + configItem?.type === 'boolean' || + (Array.isArray(configItem?.type) && configItem?.type.includes('boolean')) + ) { + return ( + onChange(e.target.checked)} + /> + } + label={formatConfigValue(String(configValue))} + /> + ); + } + if ( + (configItem?.type === 'string' && configItem.enum) || + (Array.isArray(configItem?.type) && + configItem?.type.includes('string') && + configItem.enum) + ) { + const options = + configItem.enum?.map((option) => ({ label: option })) || []; + const selectedValue = options.find( + (option) => option.label === String(configValue) + ); + return ( + { + onChange(selected?.label ?? ''); + }} + renderInput={(params) => ( + + )} + disableClearable + filterOptions={(options) => options} + isOptionEqualToValue={(option, value) => option.label === value.label} + label={''} + options={options} + value={selectedValue ?? options[0]} + /> + ); + } + if (configItem?.type === 'number' || configItem?.type === 'integer') { + return ( + onChange(Number(e.target.value))} + type="number" + value={Number(configValue)} + /> + ); + } + + if (configItem?.type === 'string') { + return ( + onChange(e.target.value)} + type="text" + value={configValue ? String(configValue) : ''} + /> + ); + } + + return null; + }; + + return ( + + + ({ + font: theme.tokens.alias.Typography.Body.Bold, + mr: 0.5, + })} + > + {`${engine}.${configLabel}`} + + {configItem?.restart_cluster && ( + + )} + {configItem?.description && ( + {configItem?.description} + )} + {renderInputField()} + + + ); +}; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx index 056c4449695..bd86cb64583 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx @@ -1,84 +1,70 @@ -import { Autocomplete, Button, TextField } from '@linode/ui'; -import Grid from '@mui/material/Grid2'; +import { Autocomplete, TextField } from '@linode/ui'; import React from 'react'; -interface ConfigurationOption { +import type { ConfigValue, ConfigurationItem } from '@linode/api-v4'; + +export interface ConfigurationOption extends ConfigurationItem { category: string; - description: string; label: string; + value?: ConfigValue; } interface Props { configurations: ConfigurationOption[]; errorText: string | undefined; - onChange: (value: string) => void; - value: string; + label: string; + onChange: (value: ConfigurationOption) => void; } export const DatabaseConfigurationSelect = (props: Props) => { - const { configurations, errorText, onChange, value } = props; + const { configurations, errorText, label, onChange } = props; - const selectedConfiguration = React.useMemo(() => { - return configurations.find((val) => val.label === value); - }, [value, configurations]); + const selectedConfig = configurations.find((val) => val.label === label); return ( - - - { - if (option.category === 'Other') { - return 'Other'; - } - return option.category; - }} - isOptionEqualToValue={(option, selectedValue) => - option.label === selectedValue.label - } - onChange={(_, selected) => { - onChange(selected.label); - }} - renderInput={(params) => ( - - )} - renderOption={(props, option) => ( -
  • -
    - {option.label} - {/* TODO: Add description if needed */} - {/* {option.description &&
    {option.description}
    } */} -
    -
  • - )} - autoHighlight - disableClearable - getOptionLabel={(option) => option.label} - label={''} - options={configurations} - sx={{ width: '336px' }} - value={selectedConfiguration} + { + if (option.category === 'Other') { + return 'Other'; + } + return option.category; + }} + isOptionEqualToValue={(option, selectedValue) => + option.label === selectedValue.label + } + onChange={(_, selected) => { + onChange(selected!); + }} + renderInput={(params) => ( + -
    - - - -
    + )} + renderOption={(props, option) => ( +
  • + {option.label} +
  • + )} + slotProps={{ + listbox: { + style: { + padding: 0, + }, + }, + }} + sx={{ + width: '316px', + }} + autoHighlight + clearIcon={null} + getOptionLabel={(option) => option.label} + label={''} + options={configurations} + value={selectedConfig ?? null} + /> ); }; diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index 2f251e5e373..b8b0d78bd0d 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -3,10 +3,13 @@ import { DateTime } from 'luxon'; import { accountFactory, + databaseEngineConfigFactory, databaseFactory, databaseTypeFactory, } from 'src/factories'; import { + convertExistingConfigsToArray, + findConfigItem, formatConfigValue, getDatabasesDescription, hasPendingUpdates, @@ -25,10 +28,13 @@ import { wrapWithTheme } from 'src/utilities/testHelpers'; import type { AccountCapability, Database, + DatabaseEngineConfig, + DatabaseInstanceAdvancedConfig, Engine, PendingUpdates, } from '@linode/api-v4'; import type { TimeOption } from 'src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups'; +import { ConfigurationOption } from './DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect'; const setup = (capabilities: AccountCapability[], flags: any) => { const account = accountFactory.build({ capabilities }); @@ -586,3 +592,103 @@ describe('formatConfigValue', () => { expect(result).toBe('+03:00'); }); }); + +describe('findConfigItem', () => { + const mockConfigs: DatabaseEngineConfig = databaseEngineConfigFactory.build(); + const expectedConfig = { + description: + 'The minimum amount of time in seconds to keep binlog entries before deletion. This may be extended for services that require binlog entries for longer than the default for example if using the MySQL Debezium Kafka connector.', + example: 600, + maximum: 86400, + minimum: 600, + restart_cluster: false, + type: 'integer', + }; + + const expectedNestedConfig = { + description: + 'Enable the pg_stat_monitor extension. Enabling this extension will cause the cluster to be restarted. When this extension is enabled, pg_stat_statements results for utility commands are unreliable', + restart_cluster: true, + type: 'boolean', + }; + it('should return the correct ConfigurationItem for a given targetKey', () => { + const result = findConfigItem( + mockConfigs.engine_config, + 'binlog_retention_period' + ); + expect(result).toEqual(expectedConfig); + }); + + it('should return the correct ConfigurationItem for a nested key', () => { + const result = findConfigItem( + mockConfigs.engine_config, + 'pg_stat_monitor_enable' + ); + expect(result).toEqual(expectedNestedConfig); + }); + + it('should return undefined if the targetKey does not exist', () => { + const result = findConfigItem( + mockConfigs.engine_config, + 'non_existing_key' + ); + expect(result).toBeUndefined(); + }); +}); + +describe('convertExistingConfigsToArray', () => { + const mockConfigs: DatabaseEngineConfig = databaseEngineConfigFactory.build(); + + const existingConfigs: DatabaseInstanceAdvancedConfig = { + advanced: { + connect_timeout: 10, + default_time_zone: '+03:00', + }, + binlog_retention_period: 600, + }; + + const expectedOptions: ConfigurationOption[] = [ + { + category: '', + description: + 'The number of seconds that the mysqld server waits for a connect packet before responding with Bad handshake', + example: 10, + label: 'connect_timeout', + maximum: 3600, + minimum: 2, + restart_cluster: false, + type: 'integer', + value: 10, + }, + { + category: '', + description: + "Default server time zone as an offset from UTC (from -12:00 to +12:00), a time zone name, or 'SYSTEM' to use the MySQL server default.", + example: '+03:00', + label: 'default_time_zone', + maxLength: 100, + minLength: 2, + pattern: '^([-+][\\d:]*|[\\w/]*)$', + restart_cluster: false, + type: 'string', + value: '+03:00', + }, + { + category: '', + description: + 'The minimum amount of time in seconds to keep binlog entries before deletion. This may be extended for services that require binlog entries for longer than the default for example if using the MySQL Debezium Kafka connector.', + example: 600, + label: 'binlog_retention_period', + maximum: 86400, + minimum: 600, + restart_cluster: false, + type: 'integer', + value: 600, + }, + ]; + + it('should convert configs to array of ConfigurationOptions with label and current value', () => { + const result = convertExistingConfigsToArray(existingConfigs, mockConfigs); + expect(result).toEqual(expectedOptions); + }); +}); diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 1d4dda6805b..1031c88dbd4 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -5,10 +5,14 @@ import { DateTime } from 'luxon'; import { useFlags } from 'src/hooks/useFlags'; import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; +import type { ConfigurationOption } from './DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect'; import type { + ConfigurationItem, DatabaseEngine, DatabaseFork, + DatabaseEngineConfig, DatabaseInstance, + DatabaseInstanceAdvancedConfig, Engine, PendingUpdates, } from '@linode/api-v4'; @@ -264,3 +268,80 @@ export const formatConfigValue = (configValue: string) => : configValue === 'undefined' ? ' - ' : configValue; + +/** + * Recursively searches for a configuration item by its key within a nested configuration object. + * + * @param configObject + * @param targetKey + * @returns The found configuration option or `undefined` if not found. + */ +export const findConfigItem = ( + configs: + | Record | ConfigurationItem> + | undefined, + targetKey: string +): ConfigurationItem | undefined => { + for (const key in configs) { + const value = configs[key]; + + if (key === targetKey) { + return value as ConfigurationItem; + } + + if (typeof value === 'object' && value !== null) { + const found = findConfigItem( + value as Record, + targetKey + ); + if (found) return found; + } + } + + return undefined; +}; + +/** + * Converts existing database configurations into an array of configuration options. + * + * @param configs + * @param allConfigs + * @returns An array of structured configuration options with metadata from `allConfigs`. + */ +export const convertExistingConfigsToArray = ( + configs: DatabaseInstanceAdvancedConfig, + allConfigs: DatabaseEngineConfig | undefined +): ConfigurationOption[] => { + const options: ConfigurationOption[] = []; + + for (const key in configs) { + const value = configs[key]; + + if (typeof value === 'object' && value !== null) { + for (const subKey in value) { + const subValue = value[subKey]; + + const foundConfig = findConfigItem(allConfigs?.engine_config, subKey); + if (foundConfig) { + options.push({ + ...foundConfig, + category: '', + label: subKey, + value: subValue, + }); + } + } + } else { + const foundConfig = findConfigItem(allConfigs?.engine_config, key); + if (foundConfig) { + options.push({ + ...foundConfig, + category: '', + label: key, + value: value, + }); + } + } + } + return options; +}; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 7b488e2ef81..43589df80c3 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -33,6 +33,7 @@ import { creditPaymentResponseFactory, dashboardFactory, databaseBackupFactory, + databaseEngineConfigFactory, databaseEngineFactory, databaseFactory, databaseInstanceFactory, @@ -371,6 +372,9 @@ const databases = [ http.post('*/databases/:engine/instances/:databaseId/resume', () => { return HttpResponse.json({}); }), + http.get('*/databases/:engine/config', () => { + return HttpResponse.json(databaseEngineConfigFactory.build()); + }), ]; const vpc = [ diff --git a/packages/manager/src/queries/databases/databases.ts b/packages/manager/src/queries/databases/databases.ts index fab0a6c11d0..5c5ce04ee20 100644 --- a/packages/manager/src/queries/databases/databases.ts +++ b/packages/manager/src/queries/databases/databases.ts @@ -3,6 +3,7 @@ import { deleteDatabase, getDatabaseBackups, getDatabaseCredentials, + getDatabaseEngineConfig, getDatabases, getEngineDatabase, legacyRestoreWithBackup, @@ -36,6 +37,7 @@ import type { DatabaseBackup, DatabaseCredentials, DatabaseEngine, + DatabaseEngineConfig, DatabaseFork, DatabaseInstance, DatabaseType, @@ -47,6 +49,10 @@ import type { } from '@linode/api-v4'; export const databaseQueries = createQueryKeys('databases', { + configs: (engine: Engine) => ({ + queryFn: () => getDatabaseEngineConfig(engine), + queryKey: ['configs', engine], + }), database: (engine: Engine, id: number) => ({ contextQueries: { backups: { @@ -265,6 +271,15 @@ export const useDatabaseTypesQuery = ( enabled, }); +export const useDatabaseEngineConfig = ( + engine: Engine, + enabled: boolean = true +) => + useQuery({ + ...databaseQueries.configs(engine), + enabled, + }); + export const useDatabaseCredentialsQuery = ( engine: Engine, id: number, From ff2bd34dc64446b3e1256e45bac4e8e36b9a25ac Mon Sep 17 00:00:00 2001 From: aaleksee-akamai Date: Wed, 26 Mar 2025 18:26:17 +0100 Subject: [PATCH 29/84] feat: [UIE-8141] - IAM RBAC: remove preselected role from autocomplete (#11926) * feat: [UIE-8141] - IAM RBAC: remove preselected role from autocomplete * Added changeset: remove preselected role from Change Role drawer * use errorText directly in the autocomplete --- ...r-11926-upcoming-features-1743002530149.md | 5 ++++ .../ChangeRoleDrawer.test.tsx | 7 ----- .../AssignedRolesTable/ChangeRoleDrawer.tsx | 28 +++++-------------- 3 files changed, 12 insertions(+), 28 deletions(-) create mode 100644 packages/manager/.changeset/pr-11926-upcoming-features-1743002530149.md diff --git a/packages/manager/.changeset/pr-11926-upcoming-features-1743002530149.md b/packages/manager/.changeset/pr-11926-upcoming-features-1743002530149.md new file mode 100644 index 00000000000..1c0dc80f187 --- /dev/null +++ b/packages/manager/.changeset/pr-11926-upcoming-features-1743002530149.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +remove preselected role from Change Role drawer ([#11926](https://github.com/linode/manager/pull/11926)) diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.test.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.test.tsx index 78ffa5bad44..a359ac34652 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.test.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.test.tsx @@ -95,13 +95,6 @@ describe('ChangeRoleDrawer', () => { const autocomplete = screen.getByRole('combobox'); - await waitFor( - () => { - expect(autocomplete).toHaveValue('account_admin'); - }, - { interval: 100, timeout: 5000 } - ); - act(() => { // Open the dropdown fireEvent.click(autocomplete); diff --git a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx index d96af9517f5..21a1f7bcc23 100644 --- a/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx +++ b/packages/manager/src/features/IAM/Shared/AssignedRolesTable/ChangeRoleDrawer.tsx @@ -76,24 +76,9 @@ export const ChangeRoleDrawer = ({ onClose, open, role }: Props) => { watch, } = useForm<{ roleName: RolesType }>({ defaultValues: { - roleName: { - access: role?.access, - label: role?.name, - resource_type: role?.resource_type, - value: role?.name, - }, + roleName: undefined, }, mode: 'onBlur', - values: role - ? { - roleName: { - access: role.access, - label: role.name, - resource_type: role.resource_type, - value: role.name, - }, - } - : undefined, }); // Watch the selected role @@ -157,25 +142,26 @@ export const ChangeRoleDrawer = ({ onClose, open, role }: Props) => { Learn more about roles and permissions. - - Change from role{' '} - {role?.name} to: + + Change from role {role?.name} to: ( + render={({ field, fieldState }) => ( field.onChange(value)} options={allRoles} placeholder="Select a Role" textFieldProps={{ hideLabel: true, noMarginTop: true }} - value={field.value} + value={field.value || null} /> )} control={control} name="roleName" + rules={{ required: 'Role is required.' }} /> {selectedRole && ( From 462cb04e1b9eb8ef9e9151f0dafb994c202d51a5 Mon Sep 17 00:00:00 2001 From: Alban Bailly <130582365+abailly-akamai@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:41:32 -0400 Subject: [PATCH 30/84] DX: [M3-9254] - Dev Tools Tokens (#11908) * initial commit - save work * render all the things * render all the things * Styling and copyable behavior * loading pattern * Theme switcher * Search * Moar Styling and formatting * Add token notices * cleanup * feedback @jaalah-akamai * feedback @jaalah-akamai * Update packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> * Update packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --------- Co-authored-by: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> --- .../src/dev-tools/DesignTokensTool.tsx | 240 ++++++++++++++++++ packages/manager/src/dev-tools/DevTools.tsx | 38 ++- .../src/dev-tools/EnvironmentToggleTool.tsx | 2 +- .../manager/src/dev-tools/FeatureFlagTool.tsx | 4 +- .../src/dev-tools/ServiceWorkerTool.tsx | 8 +- .../manager/src/dev-tools/ThemeSelector.tsx | 6 +- .../dev-tools/components/DevToolSelect.tsx | 4 +- .../src/dev-tools/components/Draggable.tsx | 4 +- .../components/ExtraPresetAccount.tsx | 6 +- .../components/ExtraPresetOptionSelect.tsx | 2 +- .../components/ExtraPresetProfile.tsx | 6 +- .../components/Tokens/ColorSwatch.tsx | 21 ++ .../dev-tools/components/Tokens/TokenCopy.tsx | 67 +++++ .../dev-tools/components/Tokens/TokenInfo.tsx | 52 ++++ .../components/Tokens/TokenSection.tsx | 125 +++++++++ .../src/dev-tools/components/Tokens/utils.ts | 87 +++++++ packages/manager/src/dev-tools/dev-tools.css | 16 +- 17 files changed, 660 insertions(+), 28 deletions(-) create mode 100644 packages/manager/src/dev-tools/DesignTokensTool.tsx create mode 100644 packages/manager/src/dev-tools/components/Tokens/ColorSwatch.tsx create mode 100644 packages/manager/src/dev-tools/components/Tokens/TokenCopy.tsx create mode 100644 packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx create mode 100644 packages/manager/src/dev-tools/components/Tokens/TokenSection.tsx create mode 100644 packages/manager/src/dev-tools/components/Tokens/utils.ts diff --git a/packages/manager/src/dev-tools/DesignTokensTool.tsx b/packages/manager/src/dev-tools/DesignTokensTool.tsx new file mode 100644 index 00000000000..b54d77fa106 --- /dev/null +++ b/packages/manager/src/dev-tools/DesignTokensTool.tsx @@ -0,0 +1,240 @@ +import { ThemeProvider } from '@emotion/react'; +import { + Box, + CircleProgress, + Notice, + Select, + Stack, + Typography, + light, +} from '@linode/ui'; +import { capitalize } from '@linode/utilities'; +import * as React from 'react'; + +import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; +import { Link } from 'src/components/Link'; +import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; +import { Tab } from 'src/components/Tabs/Tab'; +import { TabList } from 'src/components/Tabs/TabList'; +import { TabPanels } from 'src/components/Tabs/TabPanels'; +import { Tabs } from 'src/components/Tabs/Tabs'; +import { themes } from 'src/utilities/theme'; + +import { TokenSection } from './components/Tokens/TokenSection'; +import { countTokens, filterTokenObject } from './components/Tokens/utils'; + +import type { ThemeName } from '@linode/ui'; + +const _tokens = Object.entries(light.tokens ?? {}); + +export type TokenObjects = typeof _tokens; +export type TokenObject = TokenObjects[number][1]; +export type RecursiveTokenObject = { + [key: string]: RecursiveTokenObject | string; +}; +export type TokenCategory = keyof NonNullable; + +const TokenPanelContent = ({ + searchValue, + tokenCategory, + tokenObject, +}: { + searchValue: string; + tokenCategory: TokenCategory; + tokenObject: TokenObject; +}) => { + const [ + renderedContent, + setRenderedContent, + ] = React.useState(null); + const [isSearching, setIsSearching] = React.useState(false); + + React.useEffect(() => { + const computeTokens = async () => { + setIsSearching(true); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const filteredObject = searchValue + ? filterTokenObject(tokenObject, searchValue.toLowerCase()) + : tokenObject; + + const totalResults = filteredObject ? countTokens(filteredObject) : 0; + + const content = + totalResults > 0 ? ( + + {Object.entries(filteredObject).map(([key, value], index) => ( + + ))} + + ) : ( + + + No matching tokens found for "{searchValue}" + + + ); + setRenderedContent(content); + setIsSearching(false); + }; + + computeTokens(); + }, [tokenCategory, tokenObject, searchValue]); + + if (!renderedContent || isSearching) { + return ( + + + + ); + } + + return renderedContent; +}; + +export const DesignTokensTool = () => { + const [selectedTheme, setSelectedTheme] = React.useState('light'); + const [selectedTab, setSelectedTab] = React.useState(0); + const [isLoading, setIsLoading] = React.useState(false); + const [searchValue, setSearchValue] = React.useState(''); + + const handleThemeChange = React.useCallback( + async ( + _e: React.SyntheticEvent, + value: { value: ThemeName } + ) => { + setIsLoading(true); + await new Promise((resolve) => setTimeout(resolve, 100)); + + const newTokens = Object.entries(themes[value.value].tokens ?? {}); + setSelectedTheme(value.value); + setIsLoading(false); + + return newTokens; + }, + [] + ); + + const filteredTokens = Object.entries(themes[selectedTheme].tokens ?? {}); + return ( + + ({ + backgroundColor: theme.tokens.alias.Background.Normal, + flex: 1, + height: '100%', + overflow: 'auto', + position: 'absolute', + width: '100%', + })} + className="dev-tools__design-tokens" + > + {isLoading ? ( + + + + ) : ( + + + + + {filteredTokens.map(([tokenCategory, _tokenObject]) => ( + + {capitalize(tokenCategory)} + + ))} + + + + {props.children} +
    ); }; diff --git a/packages/manager/src/dev-tools/components/Draggable.tsx b/packages/manager/src/dev-tools/components/Draggable.tsx index 6cb9ec864ec..e6d312ad0a7 100644 --- a/packages/manager/src/dev-tools/components/Draggable.tsx +++ b/packages/manager/src/dev-tools/components/Draggable.tsx @@ -117,14 +117,14 @@ export const Draggable = ({ children, draggable }: DraggableProps) => { {draggable && ( <> diff --git a/packages/manager/src/dev-tools/components/ExtraPresetOptionSelect.tsx b/packages/manager/src/dev-tools/components/ExtraPresetOptionSelect.tsx index ccbd50dfba4..51fb95e4a5d 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetOptionSelect.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetOptionSelect.tsx @@ -27,7 +27,7 @@ export const ExtraPresetOptionSelect = ( ?.id ) || '' } - className="dev-tools__select thin" + className="dt-select dev-tools__select thin" onChange={(e) => onSelectChange(e, group)} style={{ width: 125 }} > diff --git a/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx b/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx index bced5b34bb5..317810062f3 100644 --- a/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx +++ b/packages/manager/src/dev-tools/components/ExtraPresetProfile.tsx @@ -109,7 +109,7 @@ export const ExtraPresetProfile = ({ {isEnabled && (
    diff --git a/packages/manager/src/dev-tools/components/Tokens/ColorSwatch.tsx b/packages/manager/src/dev-tools/components/Tokens/ColorSwatch.tsx new file mode 100644 index 00000000000..cc932ae404e --- /dev/null +++ b/packages/manager/src/dev-tools/components/Tokens/ColorSwatch.tsx @@ -0,0 +1,21 @@ +import { Border } from '@linode/design-language-system'; +import { Box } from '@linode/ui'; +import React from 'react'; + +interface ColorSwatchProps { + color: string; +} + +export const ColorSwatch = ({ color }: ColorSwatchProps) => { + return ( + + ); +}; diff --git a/packages/manager/src/dev-tools/components/Tokens/TokenCopy.tsx b/packages/manager/src/dev-tools/components/Tokens/TokenCopy.tsx new file mode 100644 index 00000000000..1dd934b65bc --- /dev/null +++ b/packages/manager/src/dev-tools/components/Tokens/TokenCopy.tsx @@ -0,0 +1,67 @@ +import { Border, Color, Font } from '@linode/design-language-system'; +import { Box, Typography } from '@linode/ui'; +import React from 'react'; + +import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; + +interface TokenCopyProps { + format: 'CSS' | 'JS' | 'SCSS' | 'Val'; + isLowerCase?: boolean; + value: string; +} + +export const TokenValue = ({ + format, + isLowerCase = false, + value, +}: TokenCopyProps) => { + if (isLowerCase) { + value = value.toLowerCase(); + } + + return ( + + + + {format}: + + {' '} + + {value} + + + + ); +}; diff --git a/packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx b/packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx new file mode 100644 index 00000000000..e55283f48a0 --- /dev/null +++ b/packages/manager/src/dev-tools/components/Tokens/TokenInfo.tsx @@ -0,0 +1,52 @@ +import { Stack } from '@linode/ui'; +import React from 'react'; + +import { ColorSwatch } from './ColorSwatch'; +import { TokenValue } from './TokenCopy'; +import { formatValue } from './utils'; + +import type { TokenCategory } from '../../DesignTokensTool'; + +interface TokenInfoProps { + category: TokenCategory; + path: string[]; + value: string; + variant: string; +} + +export const TokenInfo = (props: TokenInfoProps) => { + const { category, path = [], value, variant } = props; + + const jsPath = + path.length > 0 + ? path + .flatMap((segment) => segment.split('.')) + .map((segment) => formatValue(segment, category)) + .reduce((result, segment, index) => { + // First segment never gets a dot + if (index === 0) { + return segment; + } + + // If this segment contains a bracket, don't add a dot + if (segment.includes('[')) { + return result + segment; + } + + // Otherwise add a dot + return result + '.' + segment; + }, '') + : formatValue(variant, category); + + return ( + + {(value.startsWith('#') || value.startsWith('lch')) && ( + + )} + + + + + + ); +}; diff --git a/packages/manager/src/dev-tools/components/Tokens/TokenSection.tsx b/packages/manager/src/dev-tools/components/Tokens/TokenSection.tsx new file mode 100644 index 00000000000..ebcd31f7846 --- /dev/null +++ b/packages/manager/src/dev-tools/components/Tokens/TokenSection.tsx @@ -0,0 +1,125 @@ +import { Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { TokenInfo } from './TokenInfo'; + +import type { + RecursiveTokenObject, + TokenCategory, +} from '../../DesignTokensTool'; + +export interface TokenSectionProps { + category: TokenCategory; + title: string; + value: RecursiveTokenObject | string; + variant: string; +} + +export const TokenSection = ({ + category, + title, + value, + variant, +}: TokenSectionProps) => { + const isColorValueString = typeof value === 'string'; + + const renderTokenGroup = ( + groupValue: RecursiveTokenObject | string, + parentPath: string[] = [] + ) => { + if (typeof groupValue === 'string') { + return ( + + ); + } + + return Object.entries(groupValue).map(([key, value]) => ( + + {parentPath.length === 0 && ( + ({ + borderBottom: `1px solid ${theme.tokens.alias.Border.Normal}`, + font: theme.font.bold, + py: 1, + })} + > + {key} + + )} + {typeof value === 'object' ? ( + + {parentPath.length > 0 && ( + ({ + font: theme.font.semibold, + })} + > + {key} + + )} + {renderTokenGroup(value, [...parentPath, key])} + + ) : typeof value === 'string' ? ( + + ) : null} + + )); + }; + + if (isColorValueString) { + return ( + + ({ + backgroundColor: theme.tokens.alias.Background.Normal, + position: 'sticky', + top: 0, + zIndex: 2, + })} + variant="h3" + > + {title} + + + + ); + } + + return ( + + ({ + backgroundColor: theme.tokens.alias.Background.Normal, + position: 'sticky', + top: 0, + zIndex: 2, + })} + variant="h3" + > + {title} + + {renderTokenGroup(value)} + + ); +}; diff --git a/packages/manager/src/dev-tools/components/Tokens/utils.ts b/packages/manager/src/dev-tools/components/Tokens/utils.ts new file mode 100644 index 00000000000..893c68a1730 --- /dev/null +++ b/packages/manager/src/dev-tools/components/Tokens/utils.ts @@ -0,0 +1,87 @@ +import type { + RecursiveTokenObject, + TokenCategory, + TokenObject, +} from 'src/dev-tools/DesignTokensTool'; + +export const filterTokenObject = ( + obj: RecursiveTokenObject | TokenObject | string, + searchTerm: string, + path: string[] = [] +): RecursiveTokenObject | string => { + if (searchTerm.length < 3) { + return {}; + } + + if (typeof obj === 'string') { + // If it's a color value, check if it matches + return obj.toLowerCase().includes(searchTerm) ? obj : {}; + } + + const filtered: Record = {}; + + Object.entries(obj).forEach(([key, value]) => { + const currentPath = [...path, key]; + + // Check if the key or path matches + const keyMatches = key.toLowerCase().includes(searchTerm); + const pathMatches = currentPath + .join('.') + .toLowerCase() + .includes(searchTerm); + + if (keyMatches || pathMatches) { + // If key matches, include the whole subtree + filtered[key] = value; + } else if (typeof value === 'object') { + // Recursively filter nested objects + const filteredValue = filterTokenObject(value, searchTerm, currentPath); + if (filteredValue && Object.keys(filteredValue).length > 0) { + filtered[key] = filteredValue; + } + } else if ( + typeof value === 'string' && + value.toLowerCase().includes(searchTerm) + ) { + // If value matches (e.g., color code) + filtered[key] = value; + } + }); + + return Object.keys(filtered).length > 0 ? filtered : {}; +}; + +export const countTokens = ( + obj: RecursiveTokenObject | TokenObject | string +): number => { + if (typeof obj === 'string') { + return 1; + } + + return Object.values(obj).reduce((count, value) => { + if (typeof value === 'object') { + return count + countTokens(value); + } + return count + 1; + }, 0); +}; + +export const formatValue = (value: string, category: TokenCategory) => { + if (category === 'spacing') { + return value; + } + + // If it's a pure number, wrap in brackets + if (!isNaN(Number(value))) { + return `[${value}]`; + } + + // For any string containing a number + const match = value.match(/(\d+)/); + if (match) { + const parts = value.split(match[0]); + return `${parts[0]}[${match[0]}]${parts[1]}`; + } + + return value; +}; diff --git a/packages/manager/src/dev-tools/dev-tools.css b/packages/manager/src/dev-tools/dev-tools.css index 3565f87269d..e742b313f72 100644 --- a/packages/manager/src/dev-tools/dev-tools.css +++ b/packages/manager/src/dev-tools/dev-tools.css @@ -112,7 +112,7 @@ transform: rotate(135deg); } -.dev-tools__select select { +.dev-tools__select select.dt-select { background: transparent; border: none; color: white; @@ -124,7 +124,7 @@ } /* avoid overriding TanStack React Query Devtools styles */ -.dev-tools__body button:not(.tsqd-parent-container button) { +.dev-tools__body button.dev-tools-button:not(.tsqd-parent-container button) { background: transparent; border: 2px solid rgba(255, 255, 255, 0.5); border-radius: 1000px; @@ -155,18 +155,18 @@ } } -.dev-tools__body .dev-tools__content button:hover { +.dev-tools__body .dev-tools__content button.dev-tools-button:hover { background: rgba(255, 255, 255, 0.1); } -.dev-tools__body .dev-tools__content button:not(:disabled).green:hover { +.dev-tools__body .dev-tools__content button.dev-tools-button:not(:disabled).green:hover { background-color: #60e9a4; color: #080808; } -.dev-tools__body .dev-tools__content button:disabled, -.dev-tools__body .dev-tools__content button:disabled:active { +.dev-tools__body .dev-tools__content button.dev-tools-button:disabled, +.dev-tools__body .dev-tools__content button.dev-tools-button:disabled:active { background: transparent; cursor: not-allowed; border-color: rgba(255, 255, 255, 0.3); @@ -232,7 +232,7 @@ text-shadow: 0px -1px 0px black; } -.dev-tools__body .dev-tools__content button:active { +.dev-tools__body .dev-tools__content button.dev-tools-button:active { background: rgb(50, 50, 50); border-color: rgba(255, 255, 255, 0.65); } @@ -327,7 +327,7 @@ } .dev-tools__body input[type="number"], -.dev-tools__body input[type="text"], +.dev-tools__body input[type="text"]:not(.MuiInput-input), .dev-tools__body textarea { border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.5); From b8fa65b8350da7ec234267746f7b93d0b05d178f Mon Sep 17 00:00:00 2001 From: bill-akamai Date: Wed, 26 Mar 2025 14:18:35 -0500 Subject: [PATCH 31/84] refactor: [M3-9616] - Move `ramda` dependent utils (#11913) * Move isNilOrEmpty * Move createDevicesFromStrings * Resolve TS errors in createDevicesFromStrings.text.ts * Move createStringsFromDevices * Move maybeCastToNumber * Consolidate imports * Fix TS error * Added changeset: Move ramda dependent utils * Keep DevicesAsStrings type import separate for better organization * Added changeset: Migrated ramda dependent utils to @linode/utilities package * Avoid importing from @linode/utilities from within the same package * Update packages/manager/.changeset/pr-11913-added-1742915642212.md Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> * Update packages/manager/.changeset/pr-11913-added-1742915642212.md Co-authored-by: Purvesh Makode * Move utils changeset to correct spot * upcoming: [M3-9534] - Initial VPC Support in the `Add Network Interface` Drawer (#11887) * initial vpc and subnet select * add a shared firewallselect component * use firewall select globally * add jsdoc comments * add some testing * finish up testing for now * Added changeset: Add VPC support to the Add Network Interface Drawer * Added changeset: Added `FirewallSelect` component * Added changeset: Add test for Add Linode Interface drawer * clean up changesets * support default chips in the Firewall Select * fix spacing regression * properly handle disableClearable in the new Firewall Select * support default firewalls in the Add Interface drawer * use newer copy @coliu-akamai * fix unit test after UX tooltip changes --------- Co-authored-by: Banks Nussman * test: [M3-9486, M3-9487, M3-9557] - Allow Linode create tests to pass in alternative environments (#11886) * Delete redundant Linode Create SSH key test * Add "env:premiumPlans" test tag * Apply "env:premiumPlans" tag to Linode premium plan e2e test * Only require "Premium Plans" region capability for Premium Plans Linode create test * refactor: [M3-9617] - Move `doesRegionSupportFeature` to `utilities` package (#11891) * Move `doesRegionSupporFeature` to `utilities` pkg * Added changeset: Move `doesRegionSupportFeature` from `manager` to `utilities` package * Added changeset: Move `doesRegionSupportFeature` from `manager` to `utilities` package * refactor: [M3-8247] - Remove ramda from Utilities (#11861) * refactor: [M3-8247] - Remove ramda from Utilities * Add changeset * Add changeset * updated comment * increase coverage for isNilorEmpty() * feat: [UIE-8600] - IAM RBAC: add new drawer for unassigning role flow (#11893) * feat: [UIE-8600] - IAM RBAC: add new drawer for unassigning role flow * Added changeset: Add a new confirmation dialog for the unassigning role flow in IAM * fix the chip's color for dark theme * fix conflict and small improvements * upcoming: [UIE-8515] - DBaaS: Advanced Configuration - Drawer with existing configs (#11812) * feat: [UIE-8515] - DBaaS: Advanced Configuration - Drawer with existing configs * feat: [UIE-8515] - update mock data * Added changeset: DBaaS Advanced Configurations: added `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs * Added changeset: DBaaS Advanced Configurations: added UI for existing engine options in the drawer * upcoming: [UIE-8515] - review fix * upcoming: [UIE-8515] - style update, add link * update styles after token re-organization --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> * Resolve merge conflict * Move createDevicesFromStrings * Move createStringsFromDevices * Avoid importing from @linode/utilities from within the same package * refactor: [M3-8247] - Remove ramda from Utilities (#11861) * refactor: [M3-8247] - Remove ramda from Utilities * Add changeset * Add changeset * updated comment * increase coverage for isNilorEmpty() * Move isNilOrEmpty * Move createDevicesFromStrings * Move createStringsFromDevices * refactor: [M3-8247] - Remove ramda from Utilities (#11861) * refactor: [M3-8247] - Remove ramda from Utilities * Add changeset * Add changeset * updated comment * increase coverage for isNilorEmpty() * Fix post merge conflict issues --------- Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> Co-authored-by: Purvesh Makode Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Co-authored-by: Banks Nussman Co-authored-by: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Co-authored-by: Harsh Shankar Rao Co-authored-by: aaleksee-akamai Co-authored-by: mpolotsk-akamai <157619599+mpolotsk-akamai@users.noreply.github.com> Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> --- .../.changeset/pr-11913-removed-1742847225104.md | 5 +++++ .../DomainRecords/DomainRecordDrawerUtils.tsx | 2 +- .../LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx | 10 ++++++---- .../LinodeRescue/StandardRescueDialog.tsx | 5 ++--- .../Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx | 2 +- packages/manager/src/features/Volumes/VolumeCreate.tsx | 3 +-- packages/manager/src/utilities/formikErrorUtils.ts | 2 +- .../.changeset/pr-11913-added-1742915642212.md | 5 +++++ .../src/helpers}/createDevicesFromStrings.test.ts | 2 ++ .../src/helpers}/createDevicesFromStrings.ts | 0 .../src/helpers}/createStringsFromDevices.test.ts | 2 ++ .../src/helpers}/createStringsFromDevices.ts | 2 +- packages/utilities/src/helpers/index.ts | 4 ++++ .../src/helpers}/isNilOrEmpty.test.ts | 2 ++ .../src/helpers}/isNilOrEmpty.ts | 0 .../src/helpers}/maybeCastToNumber.ts | 0 16 files changed, 33 insertions(+), 13 deletions(-) create mode 100644 packages/manager/.changeset/pr-11913-removed-1742847225104.md create mode 100644 packages/utilities/.changeset/pr-11913-added-1742915642212.md rename packages/{manager/src/utilities => utilities/src/helpers}/createDevicesFromStrings.test.ts (97%) rename packages/{manager/src/utilities => utilities/src/helpers}/createDevicesFromStrings.ts (100%) rename packages/{manager/src/utilities => utilities/src/helpers}/createStringsFromDevices.test.ts (96%) rename packages/{manager/src/utilities => utilities/src/helpers}/createStringsFromDevices.ts (91%) rename packages/{manager/src/utilities => utilities/src/helpers}/isNilOrEmpty.test.ts (96%) rename packages/{manager/src/utilities => utilities/src/helpers}/isNilOrEmpty.ts (100%) rename packages/{manager/src/utilities => utilities/src/helpers}/maybeCastToNumber.ts (100%) diff --git a/packages/manager/.changeset/pr-11913-removed-1742847225104.md b/packages/manager/.changeset/pr-11913-removed-1742847225104.md new file mode 100644 index 00000000000..fce4029e643 --- /dev/null +++ b/packages/manager/.changeset/pr-11913-removed-1742847225104.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move ramda dependent utils ([#11913](https://github.com/linode/manager/pull/11913)) diff --git a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecordDrawerUtils.tsx b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecordDrawerUtils.tsx index 42b2c0e6aff..2370882a7f8 100644 --- a/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecordDrawerUtils.tsx +++ b/packages/manager/src/features/Domains/DomainDetail/DomainRecords/DomainRecordDrawerUtils.tsx @@ -1,6 +1,6 @@ import produce from 'immer'; -import { maybeCastToNumber } from 'src/utilities/maybeCastToNumber'; +import { maybeCastToNumber } from '@linode/utilities'; import { getInitialIPs } from '../../domainUtils'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 15174a2a1a2..d780d22cc5e 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -29,7 +29,11 @@ import { Typography, omitProps, } from '@linode/ui'; -import { scrollErrorIntoViewV2 } from '@linode/utilities'; +import { + createDevicesFromStrings, + createStringsFromDevices, + scrollErrorIntoViewV2, +} from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import { useTheme } from '@mui/material/styles'; import { useQueryClient } from '@tanstack/react-query'; @@ -49,8 +53,6 @@ import { NOT_NATTED_HELPER_TEXT, } from 'src/features/VPCs/constants'; import { useKubernetesClusterQuery } from 'src/queries/kubernetes'; -import { createDevicesFromStrings } from 'src/utilities/createDevicesFromStrings'; -import { createStringsFromDevices } from 'src/utilities/createStringsFromDevices'; import { handleFieldErrors, handleGeneralErrors, @@ -76,7 +78,7 @@ import type { Interface, LinodeConfigCreationData, } from '@linode/api-v4'; -import type { DevicesAsStrings } from 'src/utilities/createDevicesFromStrings'; +import type { DevicesAsStrings } from '@linode/utilities'; import type { ExtendedIP } from 'src/utilities/ipUtils'; interface Helpers { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx index 78942da9251..5d7bf10b6a6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/StandardRescueDialog.tsx @@ -15,13 +15,12 @@ import { Paper, clamp, } from '@linode/ui'; -import { usePrevious } from '@linode/utilities'; +import { usePrevious, createDevicesFromStrings } from '@linode/utilities'; import { styled, useTheme } from '@mui/material/styles'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { useEventsPollingActions } from 'src/queries/events/events'; -import { createDevicesFromStrings } from 'src/utilities/createDevicesFromStrings'; import { LinodePermissionsError } from '../LinodePermissionsError'; import { DeviceSelection } from './DeviceSelection'; @@ -29,7 +28,7 @@ import { RescueDescription } from './RescueDescription'; import type { ExtendedDisk } from './DeviceSelection'; import type { APIError } from '@linode/api-v4/lib/types'; -import type { DevicesAsStrings } from 'src/utilities/createDevicesFromStrings'; +import type { DevicesAsStrings } from '@linode/utilities'; interface Props { linodeId: number | undefined; diff --git a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx index 08b1cdbd056..d1dc61b1c34 100644 --- a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeCreateForm.tsx @@ -28,7 +28,7 @@ import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { maybeCastToNumber } from 'src/utilities/maybeCastToNumber'; +import { maybeCastToNumber } from '@linode/utilities'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { ConfigSelect } from './ConfigSelect'; diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index dfee8173bd4..7384e3d5aa1 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -54,8 +54,7 @@ import { handleFieldErrors, handleGeneralErrors, } from 'src/utilities/formikErrorUtils'; -import { isNilOrEmpty } from 'src/utilities/isNilOrEmpty'; -import { maybeCastToNumber } from 'src/utilities/maybeCastToNumber'; +import { isNilOrEmpty, maybeCastToNumber } from '@linode/utilities'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { reportAgreementSigningError } from 'src/utilities/reportAgreementSigningError'; diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index a26f1695c2e..04bd35ac562 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -1,5 +1,5 @@ import { getAPIErrorOrDefault } from './errorUtils'; -import { isNilOrEmpty } from './isNilOrEmpty'; +import { isNilOrEmpty } from '@linode/utilities'; import type { APIError } from '@linode/api-v4/lib/types'; import type { FormikErrors } from 'formik'; diff --git a/packages/utilities/.changeset/pr-11913-added-1742915642212.md b/packages/utilities/.changeset/pr-11913-added-1742915642212.md new file mode 100644 index 00000000000..3eed4b941c3 --- /dev/null +++ b/packages/utilities/.changeset/pr-11913-added-1742915642212.md @@ -0,0 +1,5 @@ +--- +"@linode/utilities": Added +--- + +Migrate ramda dependent utils to @linode/utilities package ([#11913](https://github.com/linode/manager/pull/11913)) diff --git a/packages/manager/src/utilities/createDevicesFromStrings.test.ts b/packages/utilities/src/helpers/createDevicesFromStrings.test.ts similarity index 97% rename from packages/manager/src/utilities/createDevicesFromStrings.test.ts rename to packages/utilities/src/helpers/createDevicesFromStrings.test.ts index b212c0a3782..2129fcd9a18 100644 --- a/packages/manager/src/utilities/createDevicesFromStrings.test.ts +++ b/packages/utilities/src/helpers/createDevicesFromStrings.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { createDevicesFromStrings } from './createDevicesFromStrings'; describe('LinodeRescue', () => { diff --git a/packages/manager/src/utilities/createDevicesFromStrings.ts b/packages/utilities/src/helpers/createDevicesFromStrings.ts similarity index 100% rename from packages/manager/src/utilities/createDevicesFromStrings.ts rename to packages/utilities/src/helpers/createDevicesFromStrings.ts diff --git a/packages/manager/src/utilities/createStringsFromDevices.test.ts b/packages/utilities/src/helpers/createStringsFromDevices.test.ts similarity index 96% rename from packages/manager/src/utilities/createStringsFromDevices.test.ts rename to packages/utilities/src/helpers/createStringsFromDevices.test.ts index b255f7f92c8..2b851ec87e6 100644 --- a/packages/manager/src/utilities/createStringsFromDevices.test.ts +++ b/packages/utilities/src/helpers/createStringsFromDevices.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { createStringsFromDevices } from './createStringsFromDevices'; describe('LinodeRescue', () => { diff --git a/packages/manager/src/utilities/createStringsFromDevices.ts b/packages/utilities/src/helpers/createStringsFromDevices.ts similarity index 91% rename from packages/manager/src/utilities/createStringsFromDevices.ts rename to packages/utilities/src/helpers/createStringsFromDevices.ts index 509d6eb29d3..d31f27dff46 100644 --- a/packages/manager/src/utilities/createStringsFromDevices.ts +++ b/packages/utilities/src/helpers/createStringsFromDevices.ts @@ -3,7 +3,7 @@ import type { DiskDevice, VolumeDevice, } from '@linode/api-v4/lib/linodes'; -import type { DevicesAsStrings } from 'src/utilities/createDevicesFromStrings'; +import type { DevicesAsStrings } from './createDevicesFromStrings'; const rdx = ( result: DevicesAsStrings, diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts index 0b5106725cc..056390a60df 100644 --- a/packages/utilities/src/helpers/index.ts +++ b/packages/utilities/src/helpers/index.ts @@ -5,6 +5,8 @@ export * from './arePropsEqual'; export * from './arrayToList'; export * from './breakpoints'; export * from './capitalize'; +export * from './createDevicesFromStrings'; +export * from './createStringsFromDevices'; export * from './deepStringTransform'; export * from './doesRegionSupportFeature'; export * from './downloadFile'; @@ -20,10 +22,12 @@ export * from './getNewRegionLabel'; export * from './groupByTags'; export * from './getDisplayName'; export * from './initWindows'; +export * from './isNilOrEmpty'; export * from './isNumber'; export * from './isToday'; export * from './link'; export * from './manuallySetVPCConfigInterfacesToActive'; +export * from './maybeCastToNumber'; export * from './metadata'; export * from './minute-conversion'; export * from './mockLocalStorage'; diff --git a/packages/manager/src/utilities/isNilOrEmpty.test.ts b/packages/utilities/src/helpers/isNilOrEmpty.test.ts similarity index 96% rename from packages/manager/src/utilities/isNilOrEmpty.test.ts rename to packages/utilities/src/helpers/isNilOrEmpty.test.ts index df210411af6..33487b36c01 100644 --- a/packages/manager/src/utilities/isNilOrEmpty.test.ts +++ b/packages/utilities/src/helpers/isNilOrEmpty.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, it } from 'vitest'; + import { isNilOrEmpty } from './isNilOrEmpty'; describe('isNilOrEmpty function', () => { diff --git a/packages/manager/src/utilities/isNilOrEmpty.ts b/packages/utilities/src/helpers/isNilOrEmpty.ts similarity index 100% rename from packages/manager/src/utilities/isNilOrEmpty.ts rename to packages/utilities/src/helpers/isNilOrEmpty.ts diff --git a/packages/manager/src/utilities/maybeCastToNumber.ts b/packages/utilities/src/helpers/maybeCastToNumber.ts similarity index 100% rename from packages/manager/src/utilities/maybeCastToNumber.ts rename to packages/utilities/src/helpers/maybeCastToNumber.ts From d73c9123be18226c1fb1fb684bb7073178bdee7e Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:34:11 -0400 Subject: [PATCH 32/84] refactor: [M3-9214] - Introduce the `(at)linode/shared` package (#11844) * Create new linode/shared package * Fix imports * Appease typescript * Update imports * Move nodebalancer factory * Test fixes * Update package scripts * Added changeset: Move `LinodeSelect` to new `shared` package * Added changeset: New `shared` package with `LinodeSelect` as the first component * Linting fixes * Add shared package to storybook * Add missing dependencies * Update lockfile * Feedback @bnussman-akamai @coliu-akamai * Fix unit test * Add validation artifact to ci.yml * Fix cannabalizing imports * Use in LinodeSelect --- .github/workflows/ci.yml | 49 ++ README.md | 1 + package.json | 4 +- .../pr-11844-removed-1742228203939.md | 5 + packages/manager/.storybook/main.ts | 1 + .../account/account-linode-managed.spec.ts | 2 +- .../e2e/core/account/service-transfer.spec.ts | 11 +- .../dbaas-widgets-verification.spec.ts | 3 +- .../linode-widget-verification.spec.ts | 3 +- .../core/firewalls/create-firewall.spec.ts | 4 +- .../migrate-linode-with-firewall.spec.ts | 9 +- .../core/firewalls/update-firewall.spec.ts | 2 +- .../e2e/core/general/gdpr-agreement.spec.ts | 3 +- .../open-support-ticket.spec.ts | 2 +- .../support-tickets-landing-page.spec.ts | 3 +- .../images/create-linode-from-image.spec.ts | 3 +- .../core/images/smoke-create-image.spec.ts | 2 +- .../e2e/core/kubernetes/lke-create.spec.ts | 15 +- .../e2e/core/kubernetes/lke-update.spec.ts | 6 +- .../e2e/core/linodes/backup-linode.spec.ts | 4 +- .../e2e/core/linodes/clone-linode.spec.ts | 8 +- .../create-linode-in-core-region.spec.ts | 4 +- ...reate-linode-in-distributed-region.spec.ts | 8 +- .../core/linodes/create-linode-mobile.spec.ts | 3 +- .../create-linode-with-add-ons.spec.ts | 3 +- ...te-linode-with-dc-specific-pricing.spec.ts | 2 +- ...create-linode-with-disk-encryption.spec.ts | 6 +- .../create-linode-with-firewall.spec.ts | 7 +- .../create-linode-with-ssh-key.spec.ts | 7 +- .../create-linode-with-user-data.spec.ts | 4 +- .../linodes/create-linode-with-vlan.spec.ts | 4 +- .../linodes/create-linode-with-vpc.spec.ts | 8 +- .../e2e/core/linodes/create-linode.spec.ts | 13 +- .../e2e/core/linodes/linode-config.spec.ts | 5 +- .../e2e/core/linodes/linode-network.spec.ts | 2 +- .../e2e/core/linodes/migrate-linode.spec.ts | 6 +- .../e2e/core/linodes/plan-selection.spec.ts | 8 +- .../e2e/core/linodes/rebuild-linode.spec.ts | 6 +- .../e2e/core/linodes/rescue-linode.spec.ts | 2 +- .../core/linodes/smoke-delete-linode.spec.ts | 2 +- .../smoke-linode-landing-table.spec.ts | 2 +- .../nodebalancer-settings.spec.ts | 7 +- ...debalancers-create-in-complex-form.spec.ts | 3 +- .../smoke-create-nodebal.spec.ts | 8 +- .../core/oneClickApps/one-click-apps.spec.ts | 3 +- ...reate-linode-with-placement-groups.spec.ts | 8 +- .../delete-placement-groups.spec.ts | 7 +- .../placement-groups-landing-page.spec.ts | 7 +- ...placement-groups-linode-assignment.spec.ts | 8 +- .../stackscripts/create-stackscripts.spec.ts | 2 +- .../e2e/core/volumes/attach-volume.spec.ts | 2 +- .../core/volumes/create-volume.smoke.spec.ts | 7 +- .../e2e/core/volumes/create-volume.spec.ts | 13 +- .../e2e/core/volumes/upgrade-volume.spec.ts | 2 +- .../cypress/e2e/core/vpc/vpc-create.spec.ts | 4 +- .../e2e/core/vpc/vpc-details-page.spec.ts | 11 +- .../e2e/core/vpc/vpc-linodes-update.spec.ts | 8 +- .../manager/cypress/support/api/linodes.ts | 2 +- .../cypress/support/api/nodebalancers.ts | 3 +- .../support/constants/dc-specific-pricing.ts | 2 +- .../manager/cypress/support/util/linodes.ts | 2 +- packages/manager/package.json | 1 + .../EntityDetail/EntityDetail.stories.tsx | 2 +- .../components/StackScript/StackScript.tsx | 2 +- packages/manager/src/factories/disk.ts | 3 +- packages/manager/src/factories/index.ts | 2 - .../features/Backups/BackupDrawer.test.tsx | 32 +- .../features/Backups/BackupLinodeRow.test.tsx | 2 +- .../src/features/Backups/BackupsCTA.test.tsx | 7 +- .../Alerts/AlertsDetail/AlertDetail.test.tsx | 3 +- .../AlertsResources/AlertsResources.test.tsx | 3 +- .../CreateAlert/Criteria/DimensionFilter.tsx | 3 +- .../ResourceMultiSelect.test.tsx | 2 +- .../CloudPulseModifyAlertResources.test.tsx | 4 +- .../EditAlert/EditAlertResources.test.tsx | 4 +- .../shared/CloudPulseResourcesSelect.test.tsx | 2 +- .../shared/CloudPulseTagsFilter.test.tsx | 2 +- .../Domains/CreateDomain/CreateDomain.tsx | 2 +- .../Domains/EditDomainDrawer.test.tsx | 2 +- .../Devices/AddLinodeDrawer.tsx | 2 +- .../FirewallLanding/CustomFirewallFields.tsx | 2 +- .../features/IAM/Users/UsersTable/UserRow.tsx | 2 +- .../ImagesCreate/CreateImageTab.test.tsx | 4 +- .../Images/ImagesCreate/CreateImageTab.tsx | 14 +- .../ImagesLanding/RebuildImageDrawer.test.tsx | 3 +- .../ImagesLanding/RebuildImageDrawer.tsx | 2 +- .../src/features/Images/utils.test.tsx | 4 +- .../CreateCluster/ClusterTierPanel.tsx | 2 +- .../NodePoolsDisplay/NodeRow.tsx | 2 +- .../NodePoolsDisplay/NodeTable.test.tsx | 2 +- .../src/features/Kubernetes/kubeUtils.test.ts | 2 +- .../src/features/Linodes/AccessTable.test.tsx | 2 +- .../features/Linodes/CloneLanding/Details.tsx | 4 +- .../Linodes/LinodeCreate/Region.test.tsx | 14 +- .../LinodeCreate/Summary/utilities.test.ts | 2 +- .../Tabs/Backups/BackupSelect.test.tsx | 2 +- .../shared/LinodeSelectTable.test.tsx | 2 +- .../shared/LinodeSelectTableRow.test.tsx | 4 +- .../shared/SelectLinodeCard.test.tsx | 2 +- .../Linodes/LinodeCreate/utilities.test.tsx | 3 +- .../Linodes/LinodeEntityDetail.test.tsx | 2 +- .../LinodeBackup/CaptureSnapshot.test.tsx | 2 +- .../LinodeBackup/EnableBackupsDialog.test.tsx | 2 +- .../LinodeBackup/LinodeBackups.test.tsx | 2 +- .../RestoreToLinodeDrawer.test.tsx | 2 +- .../LinodeBackup/ScheduleSettings.test.tsx | 4 +- .../LinodeConfigs/LinodeConfigDialog.test.tsx | 11 +- .../LinodeConfigs/LinodeConfigs.test.tsx | 2 +- .../LinodeIPAddressRow.test.tsx | 6 +- .../NetworkTransfer.test.tsx | 4 +- .../LinodeRescue/RescueDialog.test.tsx | 2 +- .../LinodeSettingsLabelPanel.test.tsx | 2 +- .../LinodeStorage/CreateDiskDrawer.test.tsx | 3 +- .../LinodeStorage/LinodeDisks.test.tsx | 4 +- .../LinodeStorage/LinodeVolumes.test.tsx | 3 +- .../LinodeStorage/ResizeDiskDrawer.test.tsx | 3 +- .../LinodeActionMenu.test.tsx | 3 +- .../LinodeRow/LinodeRow.test.tsx | 2 +- .../Linodes/SMTPRestrictionText.test.tsx | 2 +- .../ConfigNodeIPSelect.utils.test.ts | 2 +- .../NodeBalancerDeleteDialog.test.tsx | 2 +- .../NodeBalancerConfigurations.test.tsx | 8 +- .../NodeBalancerSettings.test.tsx | 3 +- .../NodeBalancerSummary/SummaryPanel.test.tsx | 10 +- .../NodeBalancerSummary/TablesPanel.test.tsx | 2 +- .../NodeBalancers/NodeBalancerSelect.test.tsx | 2 +- .../NodeBalancers/NodeBalancerSelect.tsx | 5 +- .../NodeBalancerTableRow.test.tsx | 2 +- .../NodeBalancersLanding.test.tsx | 2 +- ...lacementGroupsAssignLinodesDrawer.test.tsx | 4 +- .../PlacementGroupsAssignLinodesDrawer.tsx | 2 +- .../PlacementGroupsDeleteModal.test.tsx | 3 +- .../PlacementGroupsDetail.test.tsx | 4 +- .../PlacementGroupsLinodesTable.test.tsx | 2 +- .../PlacementGroupsLinodesTableRow.test.tsx | 5 +- .../PlacementGroupsRow.test.tsx | 4 +- .../PlacementGroupsUnassignModal.test.tsx | 2 +- .../features/PlacementGroups/utils.test.ts | 4 +- .../AuthenticationSettings/SMSMessaging.tsx | 2 +- .../TopMenu/CreateMenu/ProductFamilyGroup.tsx | 3 +- .../SubnetAssignLinodesDrawer.test.tsx | 2 +- .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 2 +- .../VPCs/VPCDetail/SubnetLinodeRow.test.tsx | 2 +- .../Volumes/Drawers/AttachVolumeDrawer.tsx | 2 +- .../LinodeVolumeAddDrawer.test.tsx | 3 +- .../src/features/Volumes/VolumeCreate.tsx | 2 +- .../mocks/presets/crud/handlers/linodes.ts | 15 +- .../src/mocks/presets/crud/seeds/linodes.ts | 3 +- packages/manager/src/mocks/serverHandlers.ts | 20 +- .../codesnippets/generate-cURL.test.ts | 2 +- .../codesnippets/generate-cli.test.ts | 2 +- .../manager/src/utilities/linodes.test.ts | 3 +- .../src/utilities/pricing/backups.test.tsx | 2 +- .../src/utilities/pricing/kubernetes.test.tsx | 4 +- .../src/utilities/pricing/linodes.test.ts | 2 +- packages/shared/.changeset/README.md | 18 + .../pr-11844-added-1742228260491.md | 5 + packages/shared/.eslintrc.json | 5 + packages/shared/.prettierrc | 4 + packages/shared/CHANGELOG.md | 0 packages/shared/README.md | 11 + packages/shared/package.json | 72 ++ .../LinodeSelect/LinodeSelect.stories.tsx | 2 +- .../src}/LinodeSelect/LinodeSelect.test.tsx | 33 +- .../src}/LinodeSelect/LinodeSelect.tsx | 10 +- packages/shared/src/LinodeSelect/index.ts | 1 + packages/shared/src/env.d.ts | 4 + packages/shared/src/index.ts | 1 + packages/shared/src/utilities/wrap.tsx | 29 + packages/shared/testSetup.ts | 9 + packages/shared/tsconfig.json | 20 + packages/shared/vitest.config.ts | 11 + packages/ui/src/assets/icons/index.ts | 1 + packages/utilities/package.json | 2 +- packages/utilities/src/factories/index.ts | 4 +- ...aceFactory.ts => linodeConfigInterface.ts} | 0 .../src/factories/linodes.ts | 2 +- .../src/factories/nodebalancer.ts | 2 +- packages/utilities/src/helpers/index.ts | 1 + .../src/helpers}/mapIdsToDevices.test.ts | 7 +- .../src/helpers}/mapIdsToDevices.ts | 2 +- .../utilities/src/helpers/sort-by.test.ts | 2 +- pnpm-lock.yaml | 617 ++++++++++-------- scripts/changelog/utils/constants.mjs | 1 + scripts/package-versions/index.js | 4 +- vitest.workspace.ts | 1 + 186 files changed, 951 insertions(+), 635 deletions(-) create mode 100644 packages/manager/.changeset/pr-11844-removed-1742228203939.md create mode 100644 packages/shared/.changeset/README.md create mode 100644 packages/shared/.changeset/pr-11844-added-1742228260491.md create mode 100644 packages/shared/.eslintrc.json create mode 100644 packages/shared/.prettierrc create mode 100644 packages/shared/CHANGELOG.md create mode 100644 packages/shared/README.md create mode 100644 packages/shared/package.json rename packages/{manager/src/features/Linodes => shared/src}/LinodeSelect/LinodeSelect.stories.tsx (96%) rename packages/{manager/src/features/Linodes => shared/src}/LinodeSelect/LinodeSelect.test.tsx (86%) rename packages/{manager/src/features/Linodes => shared/src}/LinodeSelect/LinodeSelect.tsx (96%) create mode 100644 packages/shared/src/LinodeSelect/index.ts create mode 100644 packages/shared/src/env.d.ts create mode 100644 packages/shared/src/index.ts create mode 100644 packages/shared/src/utilities/wrap.tsx create mode 100644 packages/shared/testSetup.ts create mode 100644 packages/shared/tsconfig.json create mode 100644 packages/shared/vitest.config.ts rename packages/utilities/src/factories/{linodeConfigInterfaceFactory.ts => linodeConfigInterface.ts} (100%) rename packages/{manager => utilities}/src/factories/linodes.ts (99%) rename packages/{manager => utilities}/src/factories/nodebalancer.ts (97%) rename packages/{manager/src/utilities => utilities/src/helpers}/mapIdsToDevices.test.ts (86%) rename packages/{manager/src/utilities => utilities/src/helpers}/mapIdsToDevices.ts (91%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2f83577c65..4e2590d5c19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: "linode-manager", "@linode/api-v4", "@linode/queries", + "@linode/shared", "@linode/ui", "@linode/utilities", "@linode/validation", @@ -233,6 +234,30 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run --filter @linode/queries test + test-shared: + runs-on: ubuntu-latest + needs: build-sdk + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + run_install: false + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: "20.17" + cache: "pnpm" + - uses: actions/download-artifact@v4 + with: + name: packages-api-v4-lib + path: packages/api-v4/lib + - uses: actions/download-artifact@v4 + with: + name: packages-validation-lib + path: packages/validation/lib + - run: pnpm install --frozen-lockfile + - run: pnpm run --filter @linode/shared test + typecheck-ui: runs-on: ubuntu-latest needs: build-sdk @@ -289,6 +314,30 @@ jobs: - run: pnpm install --frozen-lockfile - run: pnpm run --filter @linode/queries typecheck + typecheck-shared: + runs-on: ubuntu-latest + needs: build-sdk + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + run_install: false + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: "20.17" + cache: "pnpm" + - uses: actions/download-artifact@v4 + with: + name: packages-api-v4-lib + path: packages/api-v4/lib + - uses: actions/download-artifact@v4 + with: + name: packages-validation-lib + path: packages/validation/lib + - run: pnpm install --frozen-lockfile + - run: pnpm run --filter @linode/shared typecheck + typecheck-manager: runs-on: ubuntu-latest needs: build-sdk diff --git a/README.md b/README.md index cc7d036486a..991b48cfd68 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This repository is home to the Akamai Connected **[Cloud Manager](https://cloud. - [`@linode/api-v4`](packages/api-v4/) - [`@linode/queries`](packages/queries/) - [`@linode/search`](packages/search/) +- [`@linode/shared`](packages/shared/) - [`@linode/ui`](packages/ui/) - [`@linode/utilities`](packages/utilities/) - [`@linode/validation`](packages/validation/) diff --git a/package.json b/package.json index 63c9c5dd3f5..9a15c097c27 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,8 @@ "build:analyze": "pnpm run --filter linode-manager build:analyze", "bootstrap": "pnpm install:all && pnpm build:validation && pnpm build:sdk", "up:expose": "npm_config_package_import_method=clone-or-copy pnpm install:all && pnpm build:validation && pnpm build:sdk && pnpm start:all:expose", - "dev": "concurrently -n api-v4,validation,ui,utilities,queries,manager -c blue,yellow,magenta,cyan,gray,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter linode-manager start\"", - "start:all:expose": "concurrently -n api-v4,validation,ui,utilities,queries,manager -c blue,yellow,magenta,cyan,gray,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter linode-manager start:expose\"", + "dev": "concurrently -n api-v4,validation,ui,utilities,queries,shared,manager -c blue,yellow,magenta,cyan,gray,blue,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter @linode/shared start\" \"pnpm run --filter linode-manager start\"", + "start:all:expose": "concurrently -n api-v4,validation,ui,utilities,queries,shared,manager -c blue,yellow,magenta,cyan,gray,blue,green \"pnpm run --filter @linode/api-v4 start\" \"pnpm run --filter @linode/validation start\" \"pnpm run --filter @linode/ui start\" \"pnpm run --filter @linode/utilities start\" \"pnpm run --filter @linode/queries start\" \"pnpm run --filter @linode/shared start\" \"pnpm run --filter linode-manager start:expose\"", "start:manager": "pnpm --filter linode-manager start", "start:manager:ci": "pnpm run --filter linode-manager start:ci", "docs": "bunx vitepress@1.0.0-rc.44 dev docs", diff --git a/packages/manager/.changeset/pr-11844-removed-1742228203939.md b/packages/manager/.changeset/pr-11844-removed-1742228203939.md new file mode 100644 index 00000000000..6297dcc8964 --- /dev/null +++ b/packages/manager/.changeset/pr-11844-removed-1742228203939.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Move `LinodeSelect` to new `shared` package ([#11844](https://github.com/linode/manager/pull/11844)) diff --git a/packages/manager/.storybook/main.ts b/packages/manager/.storybook/main.ts index 09b512e1a99..b1270f5ba05 100644 --- a/packages/manager/.storybook/main.ts +++ b/packages/manager/.storybook/main.ts @@ -5,6 +5,7 @@ const config: StorybookConfig = { stories: [ '../src/components/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', '../src/features/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', + '../../shared/src/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', '../../ui/src/components/**/*.@(mdx|stories.@(js|ts|jsx|tsx))', ], addons: [ diff --git a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts index ba7e837cebc..118eb70095d 100644 --- a/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts +++ b/packages/manager/cypress/e2e/core/account/account-linode-managed.spec.ts @@ -2,6 +2,7 @@ * @file Integration tests for Cloud Manager account enable Linode Managed flows. */ +import { linodeFactory } from '@linode/utilities'; import { visitUrlWithManagedDisabled, visitUrlWithManagedEnabled, @@ -21,7 +22,6 @@ import { ui } from 'support/ui'; import { chooseRegion } from 'support/util/regions'; import { accountFactory } from 'src/factories/account'; -import { linodeFactory } from 'src/factories/linodes'; import { profileFactory } from 'src/factories/profile'; import type { Linode } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts index ddb33dc2639..be0c71fc660 100644 --- a/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts +++ b/packages/manager/cypress/e2e/core/account/service-transfer.spec.ts @@ -3,6 +3,7 @@ */ import { getProfile } from '@linode/api-v4/lib/profile'; +import { createLinodeRequestFactory, linodeFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { visitUrlWithManagedEnabled } from 'support/api/managed'; import { @@ -21,13 +22,15 @@ import { pollLinodeStatus } from 'support/util/polling'; import { randomLabel, randomUuid } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { linodeFactory } from 'src/factories'; import { entityTransferFactory } from 'src/factories/entityTransfers'; -import { createLinodeRequestFactory } from 'src/factories/linodes'; import { formatDate } from 'src/utilities/formatDate'; -import type { EntityTransferStatus } from '@linode/api-v4'; -import type { EntityTransfer, Linode, Profile } from '@linode/api-v4'; +import type { + EntityTransfer, + EntityTransferStatus, + Linode, + Profile, +} from '@linode/api-v4'; // Service transfer empty state message. const serviceTransferEmptyState = 'No data to display.'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts index e56c52232f3..1b877d9f04b 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/dbaas-widgets-verification.spec.ts @@ -1,7 +1,7 @@ /** * @file Integration Tests for CloudPulse Dbass Dashboard. */ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -27,7 +27,6 @@ import { dashboardMetricFactory, databaseFactory, kubeLinodeFactory, - linodeFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; diff --git a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts index b3323f2b5b3..1b76d04fc66 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/linode-widget-verification.spec.ts @@ -1,7 +1,7 @@ /** * @file Integration Tests for CloudPulse Linode Dashboard. */ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { widgetDetails } from 'support/constants/widgets'; import { mockGetAccount } from 'support/intercepts/account'; import { @@ -25,7 +25,6 @@ import { dashboardFactory, dashboardMetricFactory, kubeLinodeFactory, - linodeFactory, widgetFactory, } from 'src/factories'; import { generateGraphData } from 'src/features/CloudPulse/Utils/CloudPulseWidgetUtils'; diff --git a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts index 26e8b9f1bdf..6e2956c4c98 100644 --- a/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/create-firewall.spec.ts @@ -1,3 +1,4 @@ +import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptCreateFirewall } from 'support/intercepts/firewalls'; import { ui } from 'support/ui'; @@ -5,9 +6,6 @@ import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; - -import { createLinodeRequestFactory } from 'src/factories/linodes'; - authenticate(); describe.skip('create firewall', () => { before(() => { diff --git a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts index 6856938f28e..0070a252647 100644 --- a/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/migrate-linode-with-firewall.spec.ts @@ -1,10 +1,10 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { regionFactory } from '@linode/utilities'; import { createLinodeRequestFactory, - firewallFactory, linodeFactory, -} from '@src/factories'; + regionFactory, +} from '@linode/utilities'; +import { firewallFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { interceptCreateFirewall, @@ -21,8 +21,7 @@ import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomNumber } from 'support/util/random'; -import { extendRegion } from 'support/util/regions'; -import { chooseRegions } from 'support/util/regions'; +import { chooseRegions, extendRegion } from 'support/util/regions'; import type { Linode, Region } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts index e3422517b7e..e574a469199 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -1,4 +1,5 @@ import { createFirewall, createLinode } from '@linode/api-v4'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptUpdateFirewallLinodes, @@ -10,7 +11,6 @@ import { randomItem, randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { - createLinodeRequestFactory, firewallFactory, firewallRuleFactory, firewallRulesFactory, diff --git a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts index 529867889bc..18755ff73db 100644 --- a/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts +++ b/packages/manager/cypress/e2e/core/general/gdpr-agreement.spec.ts @@ -1,5 +1,4 @@ -import { regionFactory } from '@linode/utilities'; -import { linodeFactory } from '@src/factories'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccountAgreements } from 'support/intercepts/account'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts index 2efb32ab3d2..5d81c61c9d8 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/open-support-ticket.spec.ts @@ -1,5 +1,6 @@ // must turn off sort-objects rule in this file bc mockTicket.description is set by formatDescription fn in which attribute order is nonalphabetical and affects test result /* eslint-disable perfectionist/sort-objects */ +import { linodeFactory } from '@linode/utilities'; /* eslint-disable sonarjs/no-duplicate-string */ import 'cypress-file-upload'; import { mockGetAccount } from 'support/intercepts/account'; @@ -33,7 +34,6 @@ import { chooseRegion } from 'support/util/regions'; import { accountFactory, domainFactory, - linodeFactory, supportTicketFactory, } from 'src/factories'; import { diff --git a/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts b/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts index d1dbab44d9b..48f2f3c4250 100644 --- a/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/helpAndSupport/support-tickets-landing-page.spec.ts @@ -1,4 +1,4 @@ -import { linodeConfigInterfaceFactory } from '@linode/utilities'; +import { linodeConfigInterfaceFactory, linodeFactory } from '@linode/utilities'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { @@ -22,7 +22,6 @@ import { import { entityFactory, linodeConfigFactory, - linodeFactory, supportTicketFactory, volumeFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts index efcf8d1224f..22e776ed49e 100644 --- a/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/create-linode-from-image.spec.ts @@ -1,4 +1,5 @@ -import { imageFactory, linodeFactory } from '@src/factories'; +import { linodeFactory } from '@linode/utilities'; +import { imageFactory } from '@src/factories'; import { mockGetAllImages } from 'support/intercepts/images'; import { ui } from 'support/ui'; import { apiMatcher } from 'support/util/intercepts'; diff --git a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts index 1c469ca07a4..00d9f4c319a 100644 --- a/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts +++ b/packages/manager/cypress/e2e/core/images/smoke-create-image.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockGetUser } from 'support/intercepts/account'; import { mockGetEvents } from 'support/intercepts/events'; import { mockCreateImage } from 'support/intercepts/images'; @@ -13,7 +14,6 @@ import { accountUserFactory, eventFactory, grantsFactory, - linodeFactory, profileFactory, } from 'src/factories'; import { linodeDiskFactory } from 'src/factories/disk'; diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 920ad23698b..d9efd24c5df 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -1,7 +1,12 @@ /** * @file LKE creation end-to-end tests. */ -import { pluralize, regionFactory } from '@linode/utilities'; +import { + dedicatedTypeFactory, + linodeTypeFactory, + pluralize, + regionFactory, +} from '@linode/utilities'; import { dcPricingDocsLabel, dcPricingDocsUrl, @@ -38,18 +43,16 @@ import { } from 'support/intercepts/regions'; import { ui } from 'support/ui'; import { randomItem, randomLabel, randomNumber } from 'support/util/random'; -import { getRegionById } from 'support/util/regions'; -import { chooseRegion } from 'support/util/regions'; +import { chooseRegion, getRegionById } from 'support/util/regions'; -import { accountBetaFactory, lkeEnterpriseTypeFactory } from 'src/factories'; import { + accountBetaFactory, accountFactory, - dedicatedTypeFactory, kubeLinodeFactory, kubernetesClusterFactory, kubernetesControlPlaneACLFactory, kubernetesControlPlaneACLOptionsFactory, - linodeTypeFactory, + lkeEnterpriseTypeFactory, lkeHighAvailabilityTypeFactory, nodePoolFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 0f87c101f01..042570fcc64 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory, linodeTypeFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import { dcPricingMockLinodeTypes } from 'support/constants/dc-specific-pricing'; import { latestKubernetesVersion } from 'support/constants/lke'; @@ -32,8 +33,7 @@ import { } from 'support/intercepts/lke'; import { ui } from 'support/ui'; import { buildArray } from 'support/util/arrays'; -import { randomString } from 'support/util/random'; -import { randomIp, randomLabel } from 'support/util/random'; +import { randomIp, randomLabel, randomString } from 'support/util/random'; import { getRegionById } from 'support/util/regions'; import { @@ -42,8 +42,6 @@ import { kubernetesClusterFactory, kubernetesControlPlaneACLFactory, kubernetesControlPlaneACLOptionsFactory, - linodeFactory, - linodeTypeFactory, nodePoolFactory, } from 'src/factories'; import { extendType } from 'src/utilities/extendType'; diff --git a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts index ab5ea1349d0..ada334e759f 100644 --- a/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/backup-linode.spec.ts @@ -1,10 +1,10 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { - accountSettingsFactory, createLinodeRequestFactory, linodeBackupsFactory, linodeFactory, -} from '@src/factories'; +} from '@linode/utilities'; +import { accountSettingsFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { expectManagedDisabled } from 'support/api/managed'; import { dcPricingMockLinodeTypesForBackups } from 'support/constants/dc-specific-pricing'; diff --git a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts index 883fc888add..5bd831eddaf 100644 --- a/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/clone-linode.spec.ts @@ -1,9 +1,11 @@ -import { linodeConfigInterfaceFactory } from '@linode/utilities'; import { - VLANFactory, createLinodeRequestFactory, - linodeConfigFactory, + linodeConfigInterfaceFactory, linodeFactory, +} from '@linode/utilities'; +import { + VLANFactory, + linodeConfigFactory, volumeFactory, } from '@src/factories'; import { authenticate } from 'support/api/authentication'; diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts index 71ae9f83058..df59f00ebe3 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-core-region.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { @@ -9,8 +9,6 @@ import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; -import { linodeFactory } from 'src/factories'; - describe('Create Linode in a Core Region', () => { /* * - Confirms Linode create flow can be completed with a core region diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts index ac52798d3c4..8f1ccfd6833 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-in-distributed-region.spec.ts @@ -1,4 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { + linodeFactory, + linodeTypeFactory, + regionFactory, +} from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode, @@ -13,8 +17,6 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import { linodeFactory, linodeTypeFactory } from 'src/factories'; - import type { Region } from '@linode/api-v4'; describe('Create Linode in Distributed Region', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts index cc1fd3f1dda..ffc2939ce95 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-mobile.spec.ts @@ -2,6 +2,7 @@ * @file Smoke tests for Linode Create flow across common mobile viewport sizes. */ +import { linodeFactory } from '@linode/utilities'; import { MOBILE_VIEWPORTS } from 'support/constants/environment'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; @@ -9,8 +10,6 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { linodeFactory } from 'src/factories'; - describe('Linode create mobile smoke', () => { MOBILE_VIEWPORTS.forEach((viewport) => { /* diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts index 589c9e76465..125d3673317 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-add-ons.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockCreateLinode, mockGetLinodeDetails, @@ -7,8 +8,6 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { linodeFactory } from 'src/factories'; - describe('Create Linode with Add-ons', () => { /* * - Confirms UI flow to create a Linode with backups using mock API data. diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts index ed5ee08e51d..8a7efe8c078 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-dc-specific-pricing.spec.ts @@ -1,4 +1,4 @@ -import { linodeFactory } from '@src/factories'; +import { linodeFactory } from '@linode/utilities'; import { dcPricingDocsLabel, dcPricingDocsUrl, diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts index 9e5a22346b8..8f2e413fc3f 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-disk-encryption.spec.ts @@ -1,9 +1,9 @@ -import { regionFactory } from '@linode/utilities'; import { - accountFactory, linodeFactory, linodeTypeFactory, -} from '@src/factories'; + regionFactory, +} from '@linode/utilities'; +import { accountFactory } from '@src/factories'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts index 504e0acbb64..35865e33a6e 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-firewall.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateFirewall, @@ -14,11 +15,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - firewallFactory, - firewallTemplateFactory, - linodeFactory, -} from 'src/factories'; +import { firewallFactory, firewallTemplateFactory } from 'src/factories'; describe('Create Linode with Firewall', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts index 1deaa3b4f09..f3812319191 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-ssh-key.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockGetUser, mockGetUsers } from 'support/intercepts/account'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockCreateSSHKey } from 'support/intercepts/profile'; @@ -6,11 +7,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - accountUserFactory, - linodeFactory, - sshKeyFactory, -} from 'src/factories'; +import { accountUserFactory, sshKeyFactory } from 'src/factories'; describe('Create Linode with SSH Key', () => { /* diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts index c500e0f5a94..9fb53cba649 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-user-data.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; import { mockCreateLinode, @@ -10,7 +10,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory, linodeFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; describe('Create Linode with user data', () => { /* diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts index 8a1b4cdc67f..3a3be95d502 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vlan.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { mockGetRegions } from 'support/intercepts/regions'; @@ -13,7 +13,7 @@ import { } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { VLANFactory, linodeFactory } from 'src/factories'; +import { VLANFactory } from 'src/factories'; describe('Create Linode with VLANs', () => { beforeEach(() => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts index 1aa3877ae42..b7ffa62f9d2 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-with-vpc.spec.ts @@ -1,5 +1,6 @@ import { linodeConfigInterfaceFactoryWithVPC, + linodeFactory, regionFactory, } from '@linode/utilities'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; @@ -27,12 +28,7 @@ import { } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - linodeConfigFactory, - linodeFactory, - subnetFactory, - vpcFactory, -} from 'src/factories'; +import { linodeConfigFactory, subnetFactory, vpcFactory } from 'src/factories'; import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; describe('Create Linode with VPCs', () => { diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts index 016c8e70916..357cb202f2b 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode.spec.ts @@ -2,11 +2,14 @@ * @file Linode Create end-to-end tests. */ -import { regionFactory } from '@linode/utilities'; +import { + linodeFactory, + linodeTypeFactory, + regionFactory, +} from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetUser } from 'support/intercepts/account'; +import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { interceptCreateLinode, @@ -14,8 +17,8 @@ import { mockCreateLinodeError, mockGetLinodeTypes, } from 'support/intercepts/linodes'; -import { interceptGetProfile } from 'support/intercepts/profile'; import { + interceptGetProfile, mockGetProfile, mockGetProfileGrants, } from 'support/intercepts/profile'; @@ -31,8 +34,6 @@ import { accountFactory, accountUserFactory, grantsFactory, - linodeFactory, - linodeTypeFactory, profileFactory, } from 'src/factories'; diff --git a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts index 4668a9ed2e1..3d4ecc47a8e 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-config.spec.ts @@ -1,12 +1,12 @@ import { linodeConfigInterfaceFactory, linodeConfigInterfaceFactoryWithVPC, + linodeFactory, } from '@linode/utilities'; import { VLANFactory, kernelFactory, linodeConfigFactory, - linodeFactory, subnetFactory, vpcFactory, } from '@src/factories'; @@ -35,8 +35,7 @@ import { mockGetVPC, mockGetVPCs } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { fetchAllKernels, findKernelById } from 'support/util/kernels'; -import { fetchLinodeConfigs } from 'support/util/linodes'; -import { createTestLinode } from 'support/util/linodes'; +import { createTestLinode, fetchLinodeConfigs } from 'support/util/linodes'; import { randomIp, randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion, getRegionById } from 'support/util/regions'; diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts index 794e3b868ce..3bd3cd70996 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { linodeInterfaceFactoryPublic, linodeInterfaceFactoryVPC, @@ -6,7 +7,6 @@ import { firewallDeviceFactory, firewallFactory, ipAddressFactory, - linodeFactory, subnetFactory, vpcFactory, } from '@src/factories'; diff --git a/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts index 36abe9cc6b9..a9c45bc260f 100644 --- a/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/migrate-linode.spec.ts @@ -1,18 +1,18 @@ +import { linodeFactory } from '@linode/utilities'; import { linodeDiskFactory } from '@src/factories'; -import { linodeFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { dcPricingCurrentPriceLabel, dcPricingMockLinodeTypes, dcPricingNewPriceLabel, } from 'support/constants/dc-specific-pricing'; -import { mockGetLinodeDetails } from 'support/intercepts/linodes'; import { + mockGetLinodeDetails, mockGetLinodeDisks, + mockGetLinodeType, mockGetLinodeVolumes, mockMigrateLinode, } from 'support/intercepts/linodes'; -import { mockGetLinodeType } from 'support/intercepts/linodes'; import { ui } from 'support/ui'; import { apiMatcher } from 'support/util/intercepts'; import { getRegionById } from 'support/util/regions'; diff --git a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts index bad5a19b2ce..07ef3c82d45 100644 --- a/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/plan-selection.spec.ts @@ -1,6 +1,10 @@ // TODO: Cypress -import { regionAvailabilityFactory, regionFactory } from '@linode/utilities'; -import { accountFactory, linodeTypeFactory } from '@src/factories'; +import { + linodeTypeFactory, + regionAvailabilityFactory, + regionFactory, +} from '@linode/utilities'; +import { accountFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { mockGetAccount } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; diff --git a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts index 7426806d295..e3a4d5fa827 100644 --- a/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rebuild-linode.spec.ts @@ -1,10 +1,10 @@ import { createStackScript } from '@linode/api-v4/lib'; -import { regionFactory } from '@linode/utilities'; import { createLinodeRequestFactory, - imageFactory, linodeFactory, -} from '@src/factories'; + regionFactory, +} from '@linode/utilities'; +import { imageFactory } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { mockGetAllImages, mockGetImage } from 'support/intercepts/images'; diff --git a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts index fed76acfa20..7d3cb5354f9 100644 --- a/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/rescue-linode.spec.ts @@ -1,4 +1,4 @@ -import { createLinodeRequestFactory, linodeFactory } from '@src/factories'; +import { createLinodeRequestFactory, linodeFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { LINODE_CREATE_TIMEOUT } from 'support/constants/linodes'; import { diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts index 2286564fb84..61ecc709e97 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-delete-linode.spec.ts @@ -1,5 +1,5 @@ +import { createLinodeRequestFactory } from '@linode/utilities'; import { accountSettingsFactory } from '@src/factories/accountSettings'; -import { createLinodeRequestFactory } from '@src/factories/linodes'; import { authenticate } from 'support/api/authentication'; import { mockGetAccountSettings } from 'support/intercepts/account'; import { interceptDeleteLinode } from 'support/intercepts/linodes'; diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index ba4affc1c48..66f6a126dab 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable sonarjs/no-duplicate-string */ +import { linodeFactory } from '@linode/utilities'; import { profileFactory, userPreferencesFactory } from '@src/factories'; import { accountSettingsFactory } from '@src/factories/accountSettings'; import { accountUserFactory } from '@src/factories/accountUsers'; import { grantsFactory } from '@src/factories/grants'; -import { linodeFactory } from '@src/factories/linodes'; import { makeResourcePage } from '@src/mocks/serverHandlers'; import { authenticate } from 'support/api/authentication'; import { mockGetUser } from 'support/intercepts/account'; diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts index 32675f582fe..5090e7004eb 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancer-settings.spec.ts @@ -1,3 +1,4 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import { mockAddFirewallDevice, mockGetFirewalls, @@ -8,11 +9,7 @@ import { } from 'support/intercepts/nodebalancers'; import { ui } from 'support/ui'; -import { - firewallDeviceFactory, - firewallFactory, - nodeBalancerFactory, -} from 'src/factories'; +import { firewallDeviceFactory, firewallFactory } from 'src/factories'; describe('Firewalls', () => { it('allows the user to assign a Firewall from the NodeBalancer settings page', () => { diff --git a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts index 0579597a751..dd797581165 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/nodebalancers-create-in-complex-form.spec.ts @@ -1,3 +1,4 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { entityTag } from 'support/constants/cypress'; import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; @@ -7,8 +8,6 @@ import { createTestLinode } from 'support/util/linodes'; import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { nodeBalancerFactory } from 'src/factories'; - import type { Linode } from '@linode/api-v4'; authenticate(); diff --git a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts index 690377d60cf..2cd99a49615 100644 --- a/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts +++ b/packages/manager/cypress/e2e/core/nodebalancers/smoke-create-nodebal.spec.ts @@ -16,13 +16,15 @@ const deployNodeBalancer = () => { cy.get('[data-qa-deploy-nodebalancer]').click(); }; -import { regionFactory } from '@linode/utilities'; +import { + linodeFactory, + nodeBalancerFactory, + regionFactory, +} from '@linode/utilities'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { interceptCreateNodeBalancer } from 'support/intercepts/nodebalancers'; import { mockGetRegions } from 'support/intercepts/regions'; -import { linodeFactory, nodeBalancerFactory } from 'src/factories'; - const createNodeBalancerWithUI = ( nodeBal: NodeBalancer, isDcPricingTest = false diff --git a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts index 428858ef6ea..c1e069cec3f 100644 --- a/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts +++ b/packages/manager/cypress/e2e/core/oneClickApps/one-click-apps.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockGetAllImages } from 'support/intercepts/images'; import { mockCreateLinode } from 'support/intercepts/linodes'; import { @@ -10,7 +11,7 @@ import { getRandomOCAId } from 'support/util/one-click-apps'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { imageFactory, linodeFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; import { stackScriptFactory } from 'src/factories/stackscripts'; import { getMarketplaceAppLabel } from 'src/features/Linodes/LinodeCreate/Tabs/Marketplace/utilities'; import { oneClickApps } from 'src/features/OneClickApps/oneClickApps'; diff --git a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts index dbc412e08d9..aaef31b0af9 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/create-linode-with-placement-groups.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockCreateLinode, @@ -14,11 +14,7 @@ import { linodeCreatePage } from 'support/ui/pages'; import { randomNumber, randomString } from 'support/util/random'; import { extendRegion } from 'support/util/regions'; -import { - accountFactory, - linodeFactory, - placementGroupFactory, -} from 'src/factories'; +import { accountFactory, placementGroupFactory } from 'src/factories'; import { CANNOT_CHANGE_PLACEMENT_GROUP_POLICY_MESSAGE } from 'src/features/PlacementGroups/constants'; const mockAccount = accountFactory.build(); diff --git a/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts index 93e8c050573..1d7911d1280 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/delete-placement-groups.spec.ts @@ -2,6 +2,7 @@ * @file Cypress integration tests for VM Placement Groups deletion flows. */ +import { linodeFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { @@ -17,11 +18,7 @@ import { buildArray } from 'support/util/arrays'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - accountFactory, - linodeFactory, - placementGroupFactory, -} from 'src/factories'; +import { accountFactory, placementGroupFactory } from 'src/factories'; import { headers as emptyStatePageHeaders } from 'src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLandingEmptyStateData'; // Mock an account with 'Placement Group' capability. diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts index 6e910ff4cd2..efe8709cf31 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-landing-page.spec.ts @@ -1,3 +1,4 @@ +import { linodeFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockGetPlacementGroups } from 'support/intercepts/placement-groups'; @@ -5,11 +6,7 @@ import { ui } from 'support/ui'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - accountFactory, - linodeFactory, - placementGroupFactory, -} from 'src/factories'; +import { accountFactory, placementGroupFactory } from 'src/factories'; const mockAccount = accountFactory.build(); diff --git a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts index 6934266d06d..d328c6444bb 100644 --- a/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts +++ b/packages/manager/cypress/e2e/core/placementGroups/placement-groups-linode-assignment.spec.ts @@ -1,4 +1,4 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { mockGetAccount } from 'support/intercepts/account'; import { mockGetLinodeDetails, @@ -18,11 +18,7 @@ import { buildArray } from 'support/util/arrays'; import { randomLabel, randomNumber } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { - accountFactory, - linodeFactory, - placementGroupFactory, -} from 'src/factories'; +import { accountFactory, placementGroupFactory } from 'src/factories'; import type { Linode } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts index 6cef52543ac..a0e95b46b21 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/create-stackscripts.spec.ts @@ -1,4 +1,5 @@ import { createImage, getLinodeDisks, resizeLinodeDisk } from '@linode/api-v4'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptGetAccountAvailability } from 'support/intercepts/account'; import { interceptGetAllImages } from 'support/intercepts/images'; @@ -20,7 +21,6 @@ import { randomLabel, randomPhrase, randomString } from 'support/util/random'; import { chooseRegion, getRegionByLabel } from 'support/util/regions'; import { getFilteredImagesForImageSelect } from 'src/components/ImageSelect/utilities'; -import { createLinodeRequestFactory } from 'src/factories'; import type { Image } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts index e52fe6cc512..17d552e1620 100644 --- a/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/attach-volume.spec.ts @@ -1,4 +1,5 @@ import { createVolume } from '@linode/api-v4/lib/volumes'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { authenticate } from 'support/api/authentication'; import { interceptGetLinodeConfigs } from 'support/intercepts/configs'; import { @@ -11,7 +12,6 @@ import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { createLinodeRequestFactory } from 'src/factories/linodes'; import { volumeRequestPayloadFactory } from 'src/factories/volume'; import type { Linode, Volume } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts index c4aecfc2eaa..4edd06c7471 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts @@ -1,9 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import { - linodeFactory, - volumeFactory, - volumeTypeFactory, -} from '@src/factories'; +import { linodeFactory } from '@linode/utilities'; +import { volumeFactory, volumeTypeFactory } from '@src/factories'; import { mockGetLinodeDetails, mockGetLinodeVolumes, diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index 49c42bebf4d..5804c9f2d70 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,4 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { + createLinodeRequestFactory, + linodeFactory, + regionFactory, +} from '@linode/utilities'; import { accountUserFactory, grantsFactory, @@ -6,8 +10,7 @@ import { } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { entityTag } from 'support/constants/cypress'; -import { mockGetAccount } from 'support/intercepts/account'; -import { mockGetUser } from 'support/intercepts/account'; +import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockGetLinodeDetails, @@ -30,10 +33,6 @@ import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { accountFactory, volumeFactory } from 'src/factories'; -import { - createLinodeRequestFactory, - linodeFactory, -} from 'src/factories/linodes'; import type { Linode, Region } from '@linode/api-v4'; diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index 14c5f8b7b0f..09b03d3f971 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -1,6 +1,6 @@ +import { linodeFactory } from '@linode/utilities'; import { eventFactory, - linodeFactory, notificationFactory, volumeFactory, } from '@src/factories'; diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts index dde410c921c..a355f77a294 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-create.spec.ts @@ -2,8 +2,8 @@ * @file Integration tests for VPC create flow. */ -import { regionFactory } from '@linode/utilities'; -import { linodeFactory, subnetFactory, vpcFactory } from '@src/factories'; +import { linodeFactory, regionFactory } from '@linode/utilities'; +import { subnetFactory, vpcFactory } from '@src/factories'; import { mockGetRegions } from 'support/intercepts/regions'; import { mockCreateVPC, diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index affd4240eae..dcc5323773a 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -1,13 +1,9 @@ import { linodeConfigInterfaceFactory, linodeConfigInterfaceFactoryWithVPC, -} from '@linode/utilities'; -import { - linodeConfigFactory, linodeFactory, - subnetFactory, - vpcFactory, -} from '@src/factories'; +} from '@linode/utilities'; +import { linodeConfigFactory, subnetFactory, vpcFactory } from '@src/factories'; import { mockGetLinodeConfigs } from 'support/intercepts/configs'; import { mockGetLinodeDetails } from 'support/intercepts/linodes'; import { @@ -22,8 +18,7 @@ import { } from 'support/intercepts/vpc'; import { ui } from 'support/ui'; import { randomLabel, randomNumber, randomPhrase } from 'support/util/random'; -import { getRegionById } from 'support/util/regions'; -import { chooseRegion } from 'support/util/regions'; +import { chooseRegion, getRegionById } from 'support/util/regions'; import { WARNING_ICON_UNRECOMMENDED_CONFIG } from 'src/features/VPCs/constants'; diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index 25a1a075727..840b1d7f09b 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -2,13 +2,11 @@ * @file Integration tests for VPC assign/unassign Linodes flows. */ -import { linodeConfigInterfaceFactoryWithVPC } from '@linode/utilities'; import { - linodeConfigFactory, + linodeConfigInterfaceFactoryWithVPC, linodeFactory, - subnetFactory, - vpcFactory, -} from '@src/factories'; +} from '@linode/utilities'; +import { linodeConfigFactory, subnetFactory, vpcFactory } from '@src/factories'; import { vpcAssignLinodeRebootNotice, vpcUnassignLinodeRebootNotice, diff --git a/packages/manager/cypress/support/api/linodes.ts b/packages/manager/cypress/support/api/linodes.ts index 9fbe30f3302..65e7a30bcc2 100644 --- a/packages/manager/cypress/support/api/linodes.ts +++ b/packages/manager/cypress/support/api/linodes.ts @@ -1,5 +1,5 @@ import { Linode, deleteLinode, getLinodes } from '@linode/api-v4'; -import { linodeFactory } from '@src/factories'; +import { linodeFactory } from '@linode/utilities'; import { makeResourcePage } from '@src/mocks/serverHandlers'; import { pageSize } from 'support/constants/api'; import { depaginate } from 'support/util/paginate'; diff --git a/packages/manager/cypress/support/api/nodebalancers.ts b/packages/manager/cypress/support/api/nodebalancers.ts index 37ff6c420d2..d8c55b8922c 100644 --- a/packages/manager/cypress/support/api/nodebalancers.ts +++ b/packages/manager/cypress/support/api/nodebalancers.ts @@ -1,4 +1,5 @@ import { deleteNodeBalancer, getNodeBalancers } from '@linode/api-v4'; +import { nodeBalancerFactory } from '@linode/utilities'; import { oauthToken, pageSize } from 'support/constants/api'; import { entityTag } from 'support/constants/cypress'; import { depaginate } from 'support/util/paginate'; @@ -6,7 +7,7 @@ import { randomLabel } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; import { isTestLabel } from './common'; -import { nodeBalancerFactory } from 'src/factories'; + import type { NodeBalancer } from '@linode/api-v4'; export const makeNodeBalCreateReq = (nodeBal: NodeBalancer) => { diff --git a/packages/manager/cypress/support/constants/dc-specific-pricing.ts b/packages/manager/cypress/support/constants/dc-specific-pricing.ts index 584fee1378f..9c8ea564874 100644 --- a/packages/manager/cypress/support/constants/dc-specific-pricing.ts +++ b/packages/manager/cypress/support/constants/dc-specific-pricing.ts @@ -2,7 +2,7 @@ * @file Constants related to DC-specific pricing. */ -import { linodeTypeFactory } from '@src/factories'; +import { linodeTypeFactory } from '@linode/utilities'; import type { LkePlanDescription } from 'support/api/lke'; diff --git a/packages/manager/cypress/support/util/linodes.ts b/packages/manager/cypress/support/util/linodes.ts index d6b07f2c131..e278818edd5 100644 --- a/packages/manager/cypress/support/util/linodes.ts +++ b/packages/manager/cypress/support/util/linodes.ts @@ -1,5 +1,5 @@ import { createLinode, getLinodeConfigs } from '@linode/api-v4'; -import { createLinodeRequestFactory } from '@src/factories'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { findOrCreateDependencyFirewall } from 'support/api/firewalls'; import { findOrCreateDependencyVlan } from 'support/api/vlans'; import { pageSize } from 'support/constants/api'; diff --git a/packages/manager/package.json b/packages/manager/package.json index 082faf1ead2..5c3b8257edd 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -28,6 +28,7 @@ "@linode/validation": "workspace:*", "@linode/queries": "workspace:*", "@linode/search": "workspace:*", + "@linode/shared": "workspace:*", "@linode/ui": "workspace:*", "@linode/utilities": "workspace:*", "@lukemorales/query-key-factory": "^1.3.4", diff --git a/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx b/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx index 478b59d667d..37b71485bba 100644 --- a/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx +++ b/packages/manager/src/components/EntityDetail/EntityDetail.stories.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { action } from '@storybook/addon-actions'; import * as React from 'react'; -import { linodeFactory } from 'src/factories/linodes'; import { LinodeEntityDetail } from 'src/features/Linodes/LinodeEntityDetail'; import { EntityDetail } from './EntityDetail'; diff --git a/packages/manager/src/components/StackScript/StackScript.tsx b/packages/manager/src/components/StackScript/StackScript.tsx index 6b95ef255ff..786897165a7 100644 --- a/packages/manager/src/components/StackScript/StackScript.tsx +++ b/packages/manager/src/components/StackScript/StackScript.tsx @@ -1,3 +1,4 @@ +import { listToItemsByID } from '@linode/queries'; import { Box, Button, @@ -16,7 +17,6 @@ import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; import { Link } from 'src/components/Link'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; -import { listToItemsByID } from '@linode/queries'; import { useAllImagesQuery } from 'src/queries/images'; import { CodeBlock } from '../CodeBlock/CodeBlock'; diff --git a/packages/manager/src/factories/disk.ts b/packages/manager/src/factories/disk.ts index 165b5163baa..70330a97e4b 100644 --- a/packages/manager/src/factories/disk.ts +++ b/packages/manager/src/factories/disk.ts @@ -1,6 +1,7 @@ -import { Disk } from '@linode/api-v4/lib/linodes/types'; import { Factory } from '@linode/utilities'; +import type { Disk } from '@linode/api-v4/lib/linodes/types'; + export const linodeDiskFactory = Factory.Sync.makeFactory({ created: '2018-01-01', disk_encryption: 'enabled', diff --git a/packages/manager/src/factories/index.ts b/packages/manager/src/factories/index.ts index 5698c3f11fe..1532384e897 100644 --- a/packages/manager/src/factories/index.ts +++ b/packages/manager/src/factories/index.ts @@ -20,7 +20,6 @@ export * from './images'; export * from './kernels'; export * from './kubernetesCluster'; export * from './linodeConfigs'; -export * from './linodes'; export * from './longviewClient'; export * from './longviewDisks'; export * from './longviewProcess'; @@ -30,7 +29,6 @@ export * from './longviewSubscription'; export * from './longviewTopProcesses'; export * from './managed'; export * from './networking'; -export * from './nodebalancer'; export * from './notification'; export * from './oauth'; export * from './objectStorage'; diff --git a/packages/manager/src/features/Backups/BackupDrawer.test.tsx b/packages/manager/src/features/Backups/BackupDrawer.test.tsx index be8bdcde0f7..eb58026abc9 100644 --- a/packages/manager/src/features/Backups/BackupDrawer.test.tsx +++ b/packages/manager/src/features/Backups/BackupDrawer.test.tsx @@ -1,14 +1,15 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { - accountSettingsFactory, - linodeFactory, - typeFactory, -} from 'src/factories'; + +import { accountSettingsFactory, typeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { BackupDrawer } from './BackupDrawer'; const queryMocks = vi.hoisted(() => ({ + useAccountSettings: vi.fn().mockReturnValue({ + data: undefined, + }), useAllLinodesQuery: vi.fn().mockReturnValue({ data: undefined, }), @@ -18,9 +19,6 @@ const queryMocks = vi.hoisted(() => ({ useTypeQuery: vi.fn().mockReturnValue({ data: undefined, }), - useAccountSettings: vi.fn().mockReturnValue({ - data: undefined, - }), })); vi.mock('@linode/queries', async () => { @@ -51,8 +49,6 @@ vi.mock('src/queries/accountSettings', async () => { describe('BackupDrawer', () => { beforeEach(() => { const mockType = typeFactory.build({ - id: 'mock-linode-type', - label: 'Mock Linode Type', addons: { backups: { price: { @@ -68,6 +64,8 @@ describe('BackupDrawer', () => { ], }, }, + id: 'mock-linode-type', + label: 'Mock Linode Type', }); queryMocks.useAccountSettings.mockReturnValue({ data: accountSettingsFactory.build({ @@ -87,20 +85,20 @@ describe('BackupDrawer', () => { queryMocks.useAllLinodesQuery.mockReturnValue({ data: [ linodeFactory.build({ + backups: { enabled: false }, region: 'es-mad', type: 'mock-linode-type', - backups: { enabled: false }, }), ...linodeFactory.buildList(5, { + backups: { enabled: false }, region: 'us-east', type: 'mock-linode-type', - backups: { enabled: false }, }), ], }); const { findByText } = renderWithTheme( - + ); expect(await findByText('Total for 6 Linodes:')).toBeVisible(); expect(await findByText('$12.50')).toBeVisible(); @@ -110,15 +108,15 @@ describe('BackupDrawer', () => { queryMocks.useAllLinodesQuery.mockReturnValue({ data: [ linodeFactory.build({ + backups: { enabled: false }, region: 'es-mad', type: 'mock-linode-type', - backups: { enabled: false }, }), ], }); const { findByText } = renderWithTheme( - + ); expect(await findByText('Total for 1 Linode:')).toBeVisible(); expect(await findByText('$0.00')).toBeVisible(); @@ -134,7 +132,7 @@ describe('BackupDrawer', () => { }); const { findByText } = renderWithTheme( - + ); expect(await findByText('Total for 1 Linode:')).toBeVisible(); expect(await findByText('$--.--')).toBeVisible(); @@ -156,7 +154,7 @@ describe('BackupDrawer', () => { }); const { findByText, queryByText } = renderWithTheme( - + ); // Confirm that Linodes without backups are listed in table. /* eslint-disable no-await-in-loop */ diff --git a/packages/manager/src/features/Backups/BackupLinodeRow.test.tsx b/packages/manager/src/features/Backups/BackupLinodeRow.test.tsx index 4a648a419a5..6c891619547 100644 --- a/packages/manager/src/features/Backups/BackupLinodeRow.test.tsx +++ b/packages/manager/src/features/Backups/BackupLinodeRow.test.tsx @@ -1,6 +1,6 @@ +import { linodeFactory, linodeTypeFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory, linodeTypeFactory } from 'src/factories/linodes'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Backups/BackupsCTA.test.tsx b/packages/manager/src/features/Backups/BackupsCTA.test.tsx index cddd228d6ed..a09eff4c069 100644 --- a/packages/manager/src/features/Backups/BackupsCTA.test.tsx +++ b/packages/manager/src/features/Backups/BackupsCTA.test.tsx @@ -1,10 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { - accountSettingsFactory, - linodeFactory, - profileFactory, -} from 'src/factories'; +import { accountSettingsFactory, profileFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx index 02762f651e2..77624192104 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx @@ -1,9 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import React from 'react'; import { alertFactory, - linodeFactory, notificationChannelFactory, serviceTypesFactory, } from 'src/factories/'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx index 418605f6657..1616c915642 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.test.tsx @@ -1,9 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { AlertResources } from './AlertsResources'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx index 75a0343caf6..7a1135e129b 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/DimensionFilter.tsx @@ -1,5 +1,4 @@ -import { Box } from '@linode/ui'; -import { Button, Stack, Typography } from '@linode/ui'; +import { Box, Button, Stack, Typography } from '@linode/ui'; import React from 'react'; import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/ResourceMultiSelect.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/ResourceMultiSelect.test.tsx index d89fe9d3a24..4436ae11d1a 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/ResourceMultiSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/GeneralInformation/ResourceMultiSelect.test.tsx @@ -1,8 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; import { CloudPulseMultiResourceSelect } from './ResourceMultiSelect'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx index f6f2dc56d63..dd800c40c3d 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Resources/CloudPulseModifyAlertResources.test.tsx @@ -1,8 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { alertFactory, linodeFactory } from 'src/factories'; +import { alertFactory } from 'src/factories'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; import { CloudPulseModifyAlertResources } from './CloudPulseModifyAlertResources'; diff --git a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx index 318826ca7bc..0c80632ecd0 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertResources.test.tsx @@ -1,11 +1,11 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryHistory } from 'history'; import React from 'react'; import { Router } from 'react-router-dom'; -import { alertFactory, linodeFactory } from 'src/factories'; +import { alertFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { EditAlertResources } from './EditAlertResources'; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx index d6dc9f0c9e5..ec086796f8b 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx @@ -1,8 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import { fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { CloudPulseResourcesSelect } from './CloudPulseResourcesSelect'; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.test.tsx index d002827b82b..13ab105dcdf 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseTagsFilter.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { CloudPulseTagsSelect } from './CloudPulseTagsFilter'; diff --git a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx index 8e5bb821446..1245e43d2cd 100644 --- a/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx +++ b/packages/manager/src/features/Domains/CreateDomain/CreateDomain.tsx @@ -1,4 +1,5 @@ import { useGrants, useProfile } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Autocomplete, @@ -22,7 +23,6 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; import { reportException } from 'src/exceptionReporting'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect'; import { useCreateDomainMutation } from 'src/queries/domains'; import { sendCreateDomainEvent } from 'src/utilities/analytics/customEventAnalytics'; diff --git a/packages/manager/src/features/Domains/EditDomainDrawer.test.tsx b/packages/manager/src/features/Domains/EditDomainDrawer.test.tsx index 5d5ed3a886e..53072f13f88 100644 --- a/packages/manager/src/features/Domains/EditDomainDrawer.test.tsx +++ b/packages/manager/src/features/Domains/EditDomainDrawer.test.tsx @@ -1,4 +1,4 @@ -import { linodeFactory } from 'src/factories/linodes'; +import { linodeFactory } from '@linode/utilities'; import { generateDefaultDomainRecords } from './domainUtils'; diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx index c2059a723cd..e7b9c51e249 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/AddLinodeDrawer.tsx @@ -4,6 +4,7 @@ import { useGrants, useProfile, } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Drawer, Notice } from '@linode/ui'; import { useTheme } from '@mui/material'; import { useParams } from '@tanstack/react-router'; @@ -13,7 +14,6 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; import { NotFound } from 'src/components/NotFound'; import { SupportLink } from 'src/components/SupportLink'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { getEntityIdsByPermission } from 'src/utilities/grants'; import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CustomFirewallFields.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CustomFirewallFields.tsx index 6101ab30f25..6ad938a0741 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CustomFirewallFields.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CustomFirewallFields.tsx @@ -1,4 +1,5 @@ import { useAllFirewallsQuery, useGrants } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { Box, FormControlLabel, @@ -12,7 +13,6 @@ import { Controller, useFormContext } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { FIREWALL_LIMITS_CONSIDERATIONS_LINK } from 'src/constants'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { NodeBalancerSelect } from 'src/features/NodeBalancers/NodeBalancerSelect'; import { useAccountManagement } from 'src/hooks/useAccountManagement'; import { sendLinodeCreateFormInputEvent } from 'src/utilities/analytics/formEventAnalytics'; diff --git a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx index 663411c353d..cf584fab3d9 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.tsx @@ -1,3 +1,4 @@ +import { useProfile } from '@linode/queries'; import { Box, Chip, Stack, Typography } from '@linode/ui'; import { capitalize } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; @@ -10,7 +11,6 @@ import { MaskableText } from 'src/components/MaskableText/MaskableText'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; -import { useProfile } from '@linode/queries'; import { UsersActionMenu } from './UsersActionMenu'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx index 454d63aa156..7005156946f 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.test.tsx @@ -1,9 +1,9 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import React from 'react'; -import { imageFactory, linodeDiskFactory, linodeFactory } from 'src/factories'; +import { imageFactory, linodeDiskFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx index 7c36bbf4345..1f4d975f7b8 100644 --- a/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx +++ b/packages/manager/src/features/Images/ImagesCreate/CreateImageTab.tsx @@ -1,4 +1,11 @@ import { yupResolver } from '@hookform/resolvers/yup'; +import { + useAllLinodeDisksQuery, + useLinodeQuery, + useGrants, + useRegionsQuery, +} from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { Autocomplete, Box, @@ -20,17 +27,10 @@ import { Controller, useForm } from 'react-hook-form'; import { Link } from 'src/components/Link'; import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useEventsPollingActions } from 'src/queries/events/events'; import { useCreateImageMutation } from 'src/queries/images'; -import { - useAllLinodeDisksQuery, - useLinodeQuery, - useGrants, - useRegionsQuery, -} from '@linode/queries'; import type { CreateImagePayload } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx index d809685e05c..e9a07149a7e 100644 --- a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.test.tsx @@ -1,7 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { imageFactory, linodeFactory } from 'src/factories'; +import { imageFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx index 60db38ca5b8..5bac747db58 100644 --- a/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/RebuildImageDrawer.tsx @@ -1,3 +1,4 @@ +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Divider, Drawer, Notice, Stack } from '@linode/ui'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -7,7 +8,6 @@ import { useHistory } from 'react-router-dom'; import { DescriptionList } from 'src/components/DescriptionList/DescriptionList'; import { NotFound } from 'src/components/NotFound'; import { REBUILD_LINODE_IMAGE_PARAM_NAME } from 'src/features/Linodes/LinodesDetail/LinodeRebuild/utils'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useImageAndLinodeGrantCheck } from '../utils'; diff --git a/packages/manager/src/features/Images/utils.test.tsx b/packages/manager/src/features/Images/utils.test.tsx index eba0b26ca23..875078cb129 100644 --- a/packages/manager/src/features/Images/utils.test.tsx +++ b/packages/manager/src/features/Images/utils.test.tsx @@ -1,4 +1,6 @@ -import { eventFactory, imageFactory, linodeFactory } from 'src/factories'; +import { linodeFactory } from '@linode/utilities'; + +import { eventFactory, imageFactory } from 'src/factories'; import { getEventsForImages, getImageLabelForLinode } from './utils'; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx index cae4ce58950..6a0db68e26c 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterTierPanel.tsx @@ -1,10 +1,10 @@ +import { useAccount } from '@linode/queries'; import { Stack, Typography } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; import React from 'react'; import { DocsLink } from 'src/components/DocsLink/DocsLink'; import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; -import { useAccount } from '@linode/queries'; import { CLUSTER_TIER_DOCS_LINK } from '../constants'; import { diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx index 6b581166619..e0b54231505 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeRow.tsx @@ -1,3 +1,4 @@ +import { usePreferences } from '@linode/queries'; import { Box, Typography } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; @@ -9,7 +10,6 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { transitionText } from 'src/features/Linodes/transitions'; import { useInProgressEvents } from 'src/queries/events/events'; -import { usePreferences } from '@linode/queries'; import NodeActionMenu from './NodeActionMenu'; diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx index a6610035774..6dd7533fcab 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable.test.tsx @@ -1,8 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import { DateTime } from 'luxon'; import * as React from 'react'; import { kubeLinodeFactory } from 'src/factories/kubernetesCluster'; -import { linodeFactory } from 'src/factories/linodes'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts index 9ea22c18369..7bfcbbded8d 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.test.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.test.ts @@ -1,3 +1,4 @@ +import { linodeTypeFactory } from '@linode/utilities'; import { renderHook } from '@testing-library/react'; import { @@ -5,7 +6,6 @@ import { kubeLinodeFactory, kubernetesEnterpriseTierVersionFactory, kubernetesVersionFactory, - linodeTypeFactory, nodePoolFactory, } from 'src/factories'; import { extendType } from 'src/utilities/extendType'; diff --git a/packages/manager/src/features/Linodes/AccessTable.test.tsx b/packages/manager/src/features/Linodes/AccessTable.test.tsx index 92f564fb722..be440b75923 100644 --- a/packages/manager/src/features/Linodes/AccessTable.test.tsx +++ b/packages/manager/src/features/Linodes/AccessTable.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { PUBLIC_IP_ADDRESSES_TOOLTIP_TEXT } from 'src/features/Linodes/PublicIPAddressesTooltip'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/CloneLanding/Details.tsx b/packages/manager/src/features/Linodes/CloneLanding/Details.tsx index b39c9e304b9..4c37df080cb 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/Details.tsx +++ b/packages/manager/src/features/Linodes/CloneLanding/Details.tsx @@ -1,3 +1,5 @@ +import { useRegionsQuery } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Button, @@ -13,8 +15,6 @@ import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import { Link } from 'src/components/Link'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; -import { useRegionsQuery } from '@linode/queries'; import { StyledButton, diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx index 1aaabaef4bb..7c2f3f26d45 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Region.test.tsx @@ -1,15 +1,13 @@ -import { regionFactory } from '@linode/utilities'; +import { + linodeFactory, + linodeTypeFactory, + regionFactory, +} from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import React from 'react'; -import { - grantsFactory, - imageFactory, - linodeFactory, - linodeTypeFactory, - profileFactory, -} from 'src/factories'; +import { grantsFactory, imageFactory, profileFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Summary/utilities.test.ts b/packages/manager/src/features/Linodes/LinodeCreate/Summary/utilities.test.ts index 1da6463ccd4..d985577bc93 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Summary/utilities.test.ts +++ b/packages/manager/src/features/Linodes/LinodeCreate/Summary/utilities.test.ts @@ -1,4 +1,4 @@ -import { linodeTypeFactory } from 'src/factories'; +import { linodeTypeFactory } from '@linode/utilities'; import { getLinodePrice } from './utilities'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.test.tsx index 11748fa03f1..b983da7834f 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Tabs/Backups/BackupSelect.test.tsx @@ -1,6 +1,6 @@ +import { backupFactory, linodeFactory } from '@linode/utilities'; import React from 'react'; -import { backupFactory, linodeFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndHookFormContext } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx index 7755d89ffb5..7882c83a47a 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTable.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { HttpResponse, http } from 'msw'; import React from 'react'; -import { linodeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { server } from 'src/mocks/testServer'; import { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx index 2e10389b690..159a694c3a1 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/LinodeSelectTableRow.test.tsx @@ -1,8 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { imageFactory, linodeFactory, typeFactory } from 'src/factories'; +import { imageFactory, typeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { diff --git a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.test.tsx index 787793169c0..ece6e5e3534 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/shared/SelectLinodeCard.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { SelectLinodeCard } from './SelectLinodeCard'; diff --git a/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx b/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx index f53293418a0..48b3a4d64f2 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/utilities.test.tsx @@ -1,4 +1,5 @@ import { + createLinodeRequestFactory, getIsLegacyInterfaceArray, linodeConfigInterfaceFactory, linodeInterfaceFactoryPublic, @@ -6,8 +7,6 @@ import { linodeInterfaceFactoryVlan, } from '@linode/utilities'; -import { createLinodeRequestFactory } from 'src/factories'; - import { getDefaultInterfaceGenerationFromAccountSetting, getInterfacesPayload, diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx index 0e048523048..52c27929e3a 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetail.test.tsx @@ -1,4 +1,5 @@ import { queryClientFactory } from '@linode/queries'; +import { linodeFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; @@ -6,7 +7,6 @@ import { accountFactory, firewallFactory, kubernetesClusterFactory, - linodeFactory, subnetAssignedLinodeDataFactory, subnetFactory, vpcFactory, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx index eb8f95f0d2b..3513cf40806 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/CaptureSnapshot.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories/linodes'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx index 074098faef1..c2c89ed4699 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/EnableBackupsDialog.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; import { DISK_ENCRYPTION_BACKUPS_CAVEAT_COPY } from 'src/components/Encryption/constants'; -import { linodeFactory } from 'src/factories'; import { typeFactory } from 'src/factories/types'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx index ca314b4a045..7e5f5bf84cd 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/LinodeBackups.test.tsx @@ -1,7 +1,7 @@ import { LinodeBackupsResponse } from '@linode/api-v4'; +import { backupFactory, linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { backupFactory, linodeFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx index e951d207cae..055a4f62cce 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/RestoreToLinodeDrawer.test.tsx @@ -1,6 +1,6 @@ +import { backupFactory, linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { backupFactory, linodeFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx index 59b4d5826e2..432ce683b0f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeBackup/ScheduleSettings.test.tsx @@ -1,9 +1,9 @@ +import { linodeFactory } from '@linode/utilities'; +import { userEvent } from '@testing-library/user-event'; import { Settings } from 'luxon'; import * as React from 'react'; -import { userEvent } from '@testing-library/user-event'; import { profileFactory } from 'src/factories'; -import { linodeFactory } from 'src/factories/linodes'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx index e8f29ee3dce..05f0f7af2aa 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.test.tsx @@ -1,7 +1,7 @@ -import { linodeConfigInterfaceFactory } from '@linode/utilities'; +import { linodeConfigInterfaceFactory, linodeFactory } from '@linode/utilities'; import React from 'react'; -import { linodeConfigFactory, linodeFactory } from 'src/factories'; +import { linodeConfigFactory } from 'src/factories'; import { LKE_ENTERPRISE_LINODE_VPC_CONFIG_WARNING } from 'src/features/Kubernetes/constants'; import { LINODE_UNREACHABLE_HELPER_TEXT, @@ -11,8 +11,11 @@ import { import 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { LinodeConfigDialog, padList } from './LinodeConfigDialog'; -import { unrecommendedConfigNoticeSelector } from './LinodeConfigDialog'; +import { + LinodeConfigDialog, + padList, + unrecommendedConfigNoticeSelector, +} from './LinodeConfigDialog'; import type { MemoryLimit } from './LinodeConfigDialog'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx index 0f6d8c4118a..5a507711805 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigs.test.tsx @@ -1,6 +1,6 @@ +import { linodeFactory } from '@linode/utilities'; import React from 'react'; -import { linodeFactory } from 'src/factories'; import 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx index a00e6b8a60b..2f63014b5f0 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -1,8 +1,10 @@ -import { linodeConfigInterfaceFactoryWithVPC } from '@linode/utilities'; +import { + linodeConfigInterfaceFactoryWithVPC, + linodeIPFactory, +} from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { linodeIPFactory } from 'src/factories/linodes'; import { ipResponseToDisplayRows, vpcConfigInterfaceToDisplayRows, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx index cad53f7fbe1..ebb18467c96 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/NetworkingSummaryPanel/NetworkTransfer.test.tsx @@ -1,7 +1,7 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeTransferFactory, regionFactory } from '@linode/utilities'; import React from 'react'; -import { accountTransferFactory, linodeTransferFactory } from 'src/factories'; +import { accountTransferFactory } from 'src/factories'; import { typeFactory } from 'src/factories/types'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx index acc5bd0bdc8..b6abe693b1c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeRescue/RescueDialog.test.tsx @@ -1,6 +1,6 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory } from 'src/factories/linodes'; import { typeFactory } from 'src/factories/types'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.test.tsx index 789f3ff2d28..9d5a382025c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/LinodeSettingsLabelPanel.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.test.tsx index c7f1ca7228a..a81e22e3f4b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.test.tsx @@ -1,6 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import React from 'react'; -import { linodeDiskFactory, linodeFactory } from 'src/factories'; +import { linodeDiskFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.test.tsx index a2abaeeee7f..1744c8e6eb3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDisks.test.tsx @@ -1,9 +1,9 @@ +import { linodeFactory } from '@linode/utilities'; import React from 'react'; import { linodeDiskFactory } from 'src/factories'; -import { linodeFactory } from 'src/factories/linodes'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { LinodeDisks } from './LinodeDisks'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.test.tsx index 05cf8a05dc7..60c05b64ddd 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeVolumes.test.tsx @@ -1,6 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { accountFactory, linodeFactory, volumeFactory } from 'src/factories'; +import { accountFactory, volumeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.test.tsx index c0d28d55a60..18d4b4873f0 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/ResizeDiskDrawer.test.tsx @@ -1,6 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import React from 'react'; -import { linodeDiskFactory, linodeFactory } from 'src/factories'; +import { linodeDiskFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.test.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.test.tsx index 4ef2719d2b4..818fc6d6857 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeActionMenu/LinodeActionMenu.test.tsx @@ -1,10 +1,9 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeBackupsFactory, regionFactory } from '@linode/utilities'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { extendedTypes } from 'src/__data__/ExtendedType'; -import { linodeBackupsFactory } from 'src/factories/linodes'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { LinodeActionMenu } from './LinodeActionMenu'; diff --git a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.test.tsx b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.test.tsx index 7788546de9b..84b7d8bf6b1 100644 --- a/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesLanding/LinodeRow/LinodeRow.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme, wrapWithTableBody } from 'src/utilities/testHelpers'; import { LinodeRow, RenderFlag } from './LinodeRow'; diff --git a/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx b/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx index 7884196c7ce..4ae6c09252b 100644 --- a/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx +++ b/packages/manager/src/features/Linodes/SMTPRestrictionText.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { accountFactory } from 'src/factories/account'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts index e95b738d915..aa02cc8cedb 100644 --- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.utils.test.ts @@ -1,4 +1,4 @@ -import { linodeFactory } from 'src/factories'; +import { linodeFactory } from '@linode/utilities'; import { getPrivateIPOptions } from './ConfigNodeIPSelect.utils'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx index fc5c22b3744..24491fc02f4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDeleteDialog.test.tsx @@ -1,7 +1,7 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { nodeBalancerFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancerDeleteDialog } from './NodeBalancerDeleteDialog'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx index 38f94168524..7a8835ae565 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerConfigurations.test.tsx @@ -1,11 +1,11 @@ +import { + nodeBalancerConfigFactory, + nodeBalancerConfigNodeFactory, +} from '@linode/utilities'; import { waitForElementToBeRemoved } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { - nodeBalancerConfigFactory, - nodeBalancerConfigNodeFactory, -} from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.test.tsx index 79ff1a21e05..3cbfe38e965 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSettings.test.tsx @@ -1,6 +1,7 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import * as React from 'react'; -import { firewallFactory, nodeBalancerFactory } from 'src/factories'; +import { firewallFactory } from 'src/factories'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx index c4f8ba712b5..41dc4477c66 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.test.tsx @@ -1,11 +1,11 @@ -import { waitFor } from '@testing-library/react'; -import * as React from 'react'; - import { - firewallFactory, nodeBalancerConfigFactory, nodeBalancerFactory, -} from 'src/factories'; +} from '@linode/utilities'; +import { waitFor } from '@testing-library/react'; +import * as React from 'react'; + +import { firewallFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx index 5b266838373..c4d1f2d13ec 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/TablesPanel.test.tsx @@ -1,6 +1,6 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import * as React from 'react'; -import { nodeBalancerFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { TablesPanel } from './TablesPanel'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx index 4a3d494c358..4f737531ed4 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.test.tsx @@ -1,8 +1,8 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; -import { nodeBalancerFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { NodeBalancerSelect } from './NodeBalancerSelect'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx index e976126eba0..be1beed0236 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerSelect.tsx @@ -1,11 +1,10 @@ +import { useAllNodeBalancersQuery } from '@linode/queries'; import { Autocomplete, CustomPopper } from '@linode/ui'; +import { mapIdsToDevices } from '@linode/utilities'; import CloseIcon from '@mui/icons-material/Close'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import * as React from 'react'; -import { useAllNodeBalancersQuery } from '@linode/queries'; -import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; - import type { APIError, NodeBalancer } from '@linode/api-v4'; import type { SxProps, Theme } from '@mui/material/styles'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx index 0eb8e8793af..35f1b35eba9 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancerTableRow.test.tsx @@ -1,8 +1,8 @@ import { breakpoints } from '@linode/ui'; +import { nodeBalancerFactory } from '@linode/utilities'; import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { nodeBalancerFactory } from 'src/factories'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { renderWithTheme, resizeScreenSize } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx index e3771063f44..0f393a69699 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancersLanding/NodeBalancersLanding.test.tsx @@ -1,7 +1,7 @@ +import { nodeBalancerFactory } from '@linode/utilities'; import { waitForElementToBeRemoved } from '@testing-library/react'; import * as React from 'react'; -import { nodeBalancerFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx index efa6105d883..f9c399301bb 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.test.tsx @@ -1,8 +1,8 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { linodeFactory, placementGroupFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsAssignLinodesDrawer } from './PlacementGroupsAssignLinodesDrawer'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx index 3aa90c75ad2..049c9b512b9 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsAssignLinodesDrawer.tsx @@ -7,6 +7,7 @@ import { useAllPlacementGroupsQuery, useAssignLinodesToPlacementGroup, } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Box, @@ -24,7 +25,6 @@ import { DescriptionList } from 'src/components/DescriptionList/DescriptionList' import { NotFound } from 'src/components/NotFound'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; -import { LinodeSelect } from '../Linodes/LinodeSelect/LinodeSelect'; import { getLinodesFromAllPlacementGroups, hasPlacementGroupReachedCapacity, diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx index b28a18d1284..e5b10f4cd75 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDeleteModal.test.tsx @@ -1,7 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import { userEvent } from '@testing-library/user-event'; import * as React from 'react'; -import { linodeFactory, placementGroupFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsDeleteModal } from './PlacementGroupsDeleteModal'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx index b827b445d98..734e673b6a2 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsDetail.test.tsx @@ -1,7 +1,7 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory, placementGroupFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; import { PlacementGroupsDetail } from './PlacementGroupsDetail'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTable.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTable.test.tsx index 56a41adce9e..8bdfa94325e 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTable.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTable.test.tsx @@ -1,6 +1,6 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsLinodesTable } from './PlacementGroupsLinodesTable'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.test.tsx index befbd550601..f55395c1058 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsDetail/PlacementGroupsLinodes/PlacementGroupsLinodesTableRow.test.tsx @@ -1,8 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; -import { wrapWithTableBody } from 'src/utilities/testHelpers'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { wrapWithTableBody, renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsLinodesTableRow } from './PlacementGroupsLinodesTableRow'; diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx index b5c159373c5..ae1c8bc754c 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsRow.test.tsx @@ -1,7 +1,7 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory, placementGroupFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { renderWithTheme, resizeScreenSize, diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.test.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.test.tsx index 14c5ab66a7c..ec54d37749f 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.test.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsUnassignModal.test.tsx @@ -1,6 +1,6 @@ +import { linodeFactory } from '@linode/utilities'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { PlacementGroupsUnassignModal } from './PlacementGroupsUnassignModal'; diff --git a/packages/manager/src/features/PlacementGroups/utils.test.ts b/packages/manager/src/features/PlacementGroups/utils.test.ts index 63a0d4061f2..c83ba1a4698 100644 --- a/packages/manager/src/features/PlacementGroups/utils.test.ts +++ b/packages/manager/src/features/PlacementGroups/utils.test.ts @@ -1,7 +1,7 @@ -import { regionFactory } from '@linode/utilities'; +import { linodeFactory, regionFactory } from '@linode/utilities'; import { renderHook } from '@testing-library/react'; -import { linodeFactory, placementGroupFactory } from 'src/factories'; +import { placementGroupFactory } from 'src/factories'; import { getLinodesFromAllPlacementGroups, diff --git a/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx b/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx index bedacf7a6d6..8fcb9118f2c 100644 --- a/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx +++ b/packages/manager/src/features/Profile/AuthenticationSettings/SMSMessaging.tsx @@ -1,3 +1,4 @@ +import { useProfile, useSMSOptOutMutation } from '@linode/queries'; import { ActionsPanel, Box, @@ -12,7 +13,6 @@ import * as React from 'react'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; import { Link } from 'src/components/Link'; -import { useSMSOptOutMutation, useProfile } from '@linode/queries'; import { getFormattedNumber } from './PhoneVerification/helpers'; diff --git a/packages/manager/src/features/TopMenu/CreateMenu/ProductFamilyGroup.tsx b/packages/manager/src/features/TopMenu/CreateMenu/ProductFamilyGroup.tsx index 704ac350474..e34b5130a35 100644 --- a/packages/manager/src/features/TopMenu/CreateMenu/ProductFamilyGroup.tsx +++ b/packages/manager/src/features/TopMenu/CreateMenu/ProductFamilyGroup.tsx @@ -1,5 +1,4 @@ -import { Stack } from '@linode/ui'; -import { Typography } from '@linode/ui'; +import { Stack, Typography } from '@linode/ui'; import * as React from 'react'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx index fbef0006bfa..f7ae9f77def 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx @@ -1,7 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { linodeFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 40896631c32..28bbf483700 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -5,6 +5,7 @@ import { useGrants, useProfile, } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { Autocomplete, Box, @@ -26,7 +27,6 @@ import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; import { Link } from 'src/components/Link'; import { NotFound } from 'src/components/NotFound'; import { RemovableSelectionsListTable } from 'src/components/RemovableSelectionsList/RemovableSelectionsListTable'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { VPC_AUTO_ASSIGN_IPV4_TOOLTIP, VPC_MULTIPLE_CONFIGURATIONS_LEARN_MORE_LINK, diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx index ff12dab22a9..b5d9afe83b7 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.test.tsx @@ -1,6 +1,7 @@ import { linodeConfigInterfaceFactory, linodeConfigInterfaceFactoryWithVPC, + linodeFactory, } from '@linode/utilities'; import { fireEvent, @@ -15,7 +16,6 @@ import { subnetFactory, } from 'src/factories'; import { linodeConfigFactory } from 'src/factories/linodeConfigs'; -import { linodeFactory } from 'src/factories/linodes'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { diff --git a/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx b/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx index 6103097de90..98728539279 100644 --- a/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx +++ b/packages/manager/src/features/Volumes/Drawers/AttachVolumeDrawer.tsx @@ -1,4 +1,5 @@ import { useAttachVolumeMutation, useGrants } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { ActionsPanel, Box, @@ -16,7 +17,6 @@ import { number, object } from 'yup'; import { BLOCK_STORAGE_ENCRYPTION_SETTING_IMMUTABLE_COPY } from 'src/components/Encryption/constants'; import { useIsBlockStorageEncryptionFeatureEnabled } from 'src/components/Encryption/utils'; import { NotFound } from 'src/components/NotFound'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useEventsPollingActions } from 'src/queries/events/events'; import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; diff --git a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx index b1d99ae1105..336725824fb 100644 --- a/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx +++ b/packages/manager/src/features/Volumes/Drawers/VolumeDrawer/LinodeVolumeAddDrawer.test.tsx @@ -1,7 +1,8 @@ +import { linodeFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import * as React from 'react'; -import { accountFactory, linodeFactory } from 'src/factories'; +import { accountFactory } from 'src/factories'; import { HttpResponse, http, server } from 'src/mocks/testServer'; import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; diff --git a/packages/manager/src/features/Volumes/VolumeCreate.tsx b/packages/manager/src/features/Volumes/VolumeCreate.tsx index 7384e3d5aa1..adf84b3304d 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate.tsx @@ -8,6 +8,7 @@ import { useRegionsQuery, useVolumeTypesQuery, } from '@linode/queries'; +import { LinodeSelect } from '@linode/shared'; import { Box, Button, @@ -45,7 +46,6 @@ import { TagsInput } from 'src/components/TagsInput/TagsInput'; import { MAX_VOLUME_SIZE } from 'src/constants'; import { EUAgreementCheckbox } from 'src/features/Account/Agreements/EUAgreementCheckbox'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useFlags } from 'src/hooks/useFlags'; import { sendCreateVolumeEvent } from 'src/utilities/analytics/customEventAnalytics'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; diff --git a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts index 57acef660bb..b620cfda332 100644 --- a/packages/manager/src/mocks/presets/crud/handlers/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/handlers/linodes.ts @@ -1,22 +1,19 @@ import { configFactory, + linodeBackupFactory, + linodeFactory, + linodeIPFactory, linodeInterfaceFactoryPublic, linodeInterfaceFactoryVPC, linodeInterfaceFactoryVlan, linodeInterfaceSettingsFactory, + linodeStatsFactory, + linodeTransferFactory, } from '@linode/utilities'; import { DateTime } from 'luxon'; import { http } from 'msw'; -import { - firewallDeviceFactory, - linodeBackupFactory, - linodeDiskFactory, - linodeFactory, - linodeIPFactory, - linodeStatsFactory, - linodeTransferFactory, -} from 'src/factories'; +import { firewallDeviceFactory, linodeDiskFactory } from 'src/factories'; import { queueEvents } from 'src/mocks/utilities/events'; import { makeNotFoundResponse, diff --git a/packages/manager/src/mocks/presets/crud/seeds/linodes.ts b/packages/manager/src/mocks/presets/crud/seeds/linodes.ts index 72cf4a8268e..05b3df90357 100644 --- a/packages/manager/src/mocks/presets/crud/seeds/linodes.ts +++ b/packages/manager/src/mocks/presets/crud/seeds/linodes.ts @@ -1,7 +1,6 @@ -import { configFactory } from '@linode/utilities'; +import { linodeFactory, configFactory } from '@linode/utilities'; import { getSeedsCountMap } from 'src/dev-tools/utils'; -import { linodeFactory } from 'src/factories'; import { mswDB } from 'src/mocks/indexedDB'; import { seedWithUniqueIds } from 'src/mocks/presets/crud/seeds/utils'; diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 43589df80c3..00f3377d718 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -8,7 +8,17 @@ */ import { accountAvailabilityFactory, + dedicatedTypeFactory, + linodeFactory, + linodeIPFactory, + linodeStatsFactory, + linodeTransferFactory, + linodeTypeFactory, + nodeBalancerConfigFactory, + nodeBalancerConfigNodeFactory, + nodeBalancerFactory, pickRandom, + proDedicatedTypeFactory, regionAvailabilityFactory, regions, } from '@linode/utilities'; @@ -38,7 +48,6 @@ import { databaseFactory, databaseInstanceFactory, databaseTypeFactory, - dedicatedTypeFactory, domainFactory, domainRecordFactory, entityTransferFactory, @@ -55,11 +64,6 @@ import { kubernetesVersionFactory, linodeConfigFactory, linodeDiskFactory, - linodeFactory, - linodeIPFactory, - linodeStatsFactory, - linodeTransferFactory, - linodeTypeFactory, lkeEnterpriseTypeFactory, lkeHighAvailabilityTypeFactory, lkeStandardAvailabilityTypeFactory, @@ -73,9 +77,6 @@ import { managedSSHPubKeyFactory, managedStatsFactory, monitorFactory, - nodeBalancerConfigFactory, - nodeBalancerConfigNodeFactory, - nodeBalancerFactory, nodeBalancerTypeFactory, nodePoolFactory, notificationChannelFactory, @@ -91,7 +92,6 @@ import { placementGroupFactory, possibleMySQLReplicationTypes, possiblePostgresReplicationTypes, - proDedicatedTypeFactory, profileFactory, promoFactory, securityQuestionsFactory, diff --git a/packages/manager/src/utilities/codesnippets/generate-cURL.test.ts b/packages/manager/src/utilities/codesnippets/generate-cURL.test.ts index 010b3836cc7..c0f6ef8c882 100644 --- a/packages/manager/src/utilities/codesnippets/generate-cURL.test.ts +++ b/packages/manager/src/utilities/codesnippets/generate-cURL.test.ts @@ -1,4 +1,4 @@ -import { createLinodeRequestFactory } from 'src/factories/linodes'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { generateCurlCommand } from './generate-cURL'; diff --git a/packages/manager/src/utilities/codesnippets/generate-cli.test.ts b/packages/manager/src/utilities/codesnippets/generate-cli.test.ts index b6df3e3760e..5b73e92717a 100644 --- a/packages/manager/src/utilities/codesnippets/generate-cli.test.ts +++ b/packages/manager/src/utilities/codesnippets/generate-cli.test.ts @@ -1,4 +1,4 @@ -import { createLinodeRequestFactory } from 'src/factories/linodes'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { generateCLICommand } from './generate-cli'; diff --git a/packages/manager/src/utilities/linodes.test.ts b/packages/manager/src/utilities/linodes.test.ts index 76620116f05..b90ee0ed7ca 100644 --- a/packages/manager/src/utilities/linodes.test.ts +++ b/packages/manager/src/utilities/linodes.test.ts @@ -1,6 +1,7 @@ +import { linodeFactory } from '@linode/utilities'; import { renderHook } from '@testing-library/react'; -import { accountMaintenanceFactory, linodeFactory } from 'src/factories'; +import { accountMaintenanceFactory } from 'src/factories'; import { addMaintenanceToLinodes, diff --git a/packages/manager/src/utilities/pricing/backups.test.tsx b/packages/manager/src/utilities/pricing/backups.test.tsx index a3ef5db57af..14c5fde1fca 100644 --- a/packages/manager/src/utilities/pricing/backups.test.tsx +++ b/packages/manager/src/utilities/pricing/backups.test.tsx @@ -1,4 +1,4 @@ -import { linodeFactory, linodeTypeFactory } from 'src/factories'; +import { linodeFactory, linodeTypeFactory } from '@linode/utilities'; import { getLinodeBackupPrice, getTotalBackupsPrice } from './backups'; diff --git a/packages/manager/src/utilities/pricing/kubernetes.test.tsx b/packages/manager/src/utilities/pricing/kubernetes.test.tsx index 6a76f0329bb..bb7799cdcd1 100644 --- a/packages/manager/src/utilities/pricing/kubernetes.test.tsx +++ b/packages/manager/src/utilities/pricing/kubernetes.test.tsx @@ -1,4 +1,6 @@ -import { linodeTypeFactory, nodePoolFactory } from 'src/factories'; +import { linodeTypeFactory } from '@linode/utilities'; + +import { nodePoolFactory } from 'src/factories'; import { extendType } from 'src/utilities/extendType'; import { getKubernetesMonthlyPrice, getTotalClusterPrice } from './kubernetes'; diff --git a/packages/manager/src/utilities/pricing/linodes.test.ts b/packages/manager/src/utilities/pricing/linodes.test.ts index 52f324cf89e..3f82c5ce58d 100644 --- a/packages/manager/src/utilities/pricing/linodes.test.ts +++ b/packages/manager/src/utilities/pricing/linodes.test.ts @@ -1,4 +1,4 @@ -import { linodeTypeFactory } from 'src/factories'; +import { linodeTypeFactory } from '@linode/utilities'; import { getLinodeBackupPrice } from './backups'; import { diff --git a/packages/shared/.changeset/README.md b/packages/shared/.changeset/README.md new file mode 100644 index 00000000000..b9a412689fb --- /dev/null +++ b/packages/shared/.changeset/README.md @@ -0,0 +1,18 @@ +# Changesets + +This directory gets auto-populated when running `pnpm changeset`. +You can however add your changesets manually as well, knowing that the [TYPE] is limited to the following options `Added`, `Fixed`, `Changed`, `Removed`, `Tech Stories`, `Tests`, `Upcoming Features` and follow this format: + +```md +--- +'@linode/[PACKAGE]': [TYPE] +--- + +My PR Description ([#`PR number`](`PR link`)) +``` + +You must commit them to the repo so they can be picked up for the changelog generation. + +This directory get wiped out when running `pnpm generate-changelog`. + +See `changeset.mjs` for implementation details. diff --git a/packages/shared/.changeset/pr-11844-added-1742228260491.md b/packages/shared/.changeset/pr-11844-added-1742228260491.md new file mode 100644 index 00000000000..33ea95d0bba --- /dev/null +++ b/packages/shared/.changeset/pr-11844-added-1742228260491.md @@ -0,0 +1,5 @@ +--- +"@linode/shared": Added +--- + +New `shared` package with `LinodeSelect` as the first component ([#11844](https://github.com/linode/manager/pull/11844)) diff --git a/packages/shared/.eslintrc.json b/packages/shared/.eslintrc.json new file mode 100644 index 00000000000..b8f8c62e03a --- /dev/null +++ b/packages/shared/.eslintrc.json @@ -0,0 +1,5 @@ +{ + // Temporarily extending ESLint config from linode/manager as the base config. + // @todo: modularization - Replace the path with the base shared ESLint config once available. + "extends": ["../manager/.eslintrc.cjs"] +} diff --git a/packages/shared/.prettierrc b/packages/shared/.prettierrc new file mode 100644 index 00000000000..c563e850dad --- /dev/null +++ b/packages/shared/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 80, + "singleQuote": true +} diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/shared/README.md b/packages/shared/README.md new file mode 100644 index 00000000000..e591a9c1ea7 --- /dev/null +++ b/packages/shared/README.md @@ -0,0 +1,11 @@ +# Shared Feature Component Library + +`@linode/shared` contains definitions for React-based feature components that are used frequently across Akamai Connected Cloud Manager. + +In contrast to the other UI component library in this repository, [`@linode/ui`](../ui/), components in this package make use of [`@linode/api-v4`](../api-v4/) and other dependencies to implement common, opinionated and complex components to enable a seamless experience for users as they navigate between features of the app. + +## Components + +All components defined in this library must conform to the [CDS 2.0 design system](https://github.com/linode/design-language-system) and be built using base components from [`@linode/ui`](../ui/). + +Interfaces must be documented using the [TSDoc](https://tsdoc.org/) comment standard. This repository also includes support for Storybook stories and Vitest unit tests, which may be included as necessary to improve component quality and reliability. \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 00000000000..8b827308c5d --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,72 @@ +{ + "name": "@linode/shared", + "version": "0.0.1", + "description": "Linode shared feature component library", + "main": "src/index.ts", + "module": "src/index.ts", + "types": "src/index.ts", + "author": "Linode", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/linode/manager/tree/develop/packages/shared" + }, + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "start": "tsc -w --preserveWatchOutput", + "lint": "eslint . --quiet --ext .js,.ts,.tsx", + "typecheck": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "test:debug": "node --inspect-brk scripts/test.js --runInBand", + "precommit": "lint-staged" + }, + "lint-staged": { + "*.{ts,tsx,js}": [ + "prettier --write", + "eslint --ext .js,.ts,.tsx --quiet" + ] + }, + "dependencies": { + "@linode/api-v4": "workspace:*", + "@linode/queries": "workspace:*", + "@linode/ui": "workspace:*", + "@linode/utilities": "workspace:*", + "@mui/material": "^6.4.5", + "@tanstack/react-query": "5.51.24", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@linode/eslint-plugin-cloud-manager": "^0.0.10", + "@storybook/addon-actions": "^8.6.7", + "@storybook/react": "^8.6.7", + "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "~6.4.2", + "@testing-library/react": "~16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.18", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^7.1.0", + "eslint-config-prettier": "~8.1.0", + "eslint-plugin-cypress": "^2.11.3", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-perfectionist": "^1.4.0", + "eslint-plugin-prettier": "~3.3.1", + "eslint-plugin-ramda": "^2.5.1", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^3.0.0", + "eslint-plugin-react-refresh": "^0.4.13", + "eslint-plugin-scanjs-rules": "^0.2.1", + "eslint-plugin-sonarjs": "^0.5.0", + "eslint-plugin-testing-library": "^3.1.2", + "eslint-plugin-xss": "^0.1.10", + "lint-staged": "^15.2.9", + "prettier": "~2.2.1", + "vite-plugin-svgr": "^3.2.0" + } +} diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.stories.tsx b/packages/shared/src/LinodeSelect/LinodeSelect.stories.tsx similarity index 96% rename from packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.stories.tsx rename to packages/shared/src/LinodeSelect/LinodeSelect.stories.tsx index d7cbd9de212..5b0aa4c78fb 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.stories.tsx +++ b/packages/shared/src/LinodeSelect/LinodeSelect.stories.tsx @@ -1,4 +1,3 @@ -import { Linode } from '@linode/api-v4/lib/linodes'; import { action } from '@storybook/addon-actions'; import React from 'react'; @@ -8,6 +7,7 @@ import type { LinodeMultiSelectProps, LinodeSingleSelectProps, } from './LinodeSelect'; +import type { Linode } from '@linode/api-v4/lib/linodes'; import type { Meta, StoryObj } from '@storybook/react'; const linodes = [ diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx b/packages/shared/src/LinodeSelect/LinodeSelect.test.tsx similarity index 86% rename from packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx rename to packages/shared/src/LinodeSelect/LinodeSelect.test.tsx index e0af79aab29..47dc1d7fd54 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.test.tsx +++ b/packages/shared/src/LinodeSelect/LinodeSelect.test.tsx @@ -1,10 +1,10 @@ -import { screen, waitFor } from '@testing-library/react'; +import { linodeFactory } from '@linode/utilities'; +import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { describe, expect, test, vi } from 'vitest'; -import { linodeFactory } from 'src/factories'; -import { renderWithTheme } from 'src/utilities/testHelpers'; - +import { QueryClientWrapper, renderWithWrappers } from '../utilities/wrap'; import { LinodeSelect } from './LinodeSelect'; import type { Linode } from '@linode/api-v4'; @@ -17,14 +17,15 @@ describe('LinodeSelect', () => { const options: Linode[] = []; // Assuming no options are available const onSelectionChange = vi.fn(); - renderWithTheme( + const screen = renderWithWrappers( + />, + [QueryClientWrapper()] ); const input = screen.getByTestId(TEXTFIELD_ID); @@ -43,13 +44,14 @@ describe('LinodeSelect', () => { const option: Linode[] = []; // Assuming no options are available const onSelectionChange = vi.fn(); - renderWithTheme( + const screen = renderWithWrappers( + />, + [QueryClientWrapper()] ); // Open the dropdown @@ -68,14 +70,15 @@ describe('LinodeSelect', () => { const option: Linode[] = []; // Assuming no options are available const onSelectionChange = vi.fn(); - renderWithTheme( + const screen = renderWithWrappers( + />, + [QueryClientWrapper()] ); const input = screen.getByTestId(TEXTFIELD_ID); @@ -93,14 +96,15 @@ describe('LinodeSelect', () => { const option = linodeFactory.build({ id: 1, label: 'Linode 1' }); const onSelectionChange = vi.fn(); - renderWithTheme( + const screen = renderWithWrappers( + />, + [QueryClientWrapper()] ); const input = screen.getByTestId(TEXTFIELD_ID); @@ -117,14 +121,15 @@ describe('LinodeSelect', () => { const option = linodeFactory.build({ id: 1, label: 'Linode 1' }); const onSelectionChange = vi.fn(); - renderWithTheme( + const screen = renderWithWrappers( + />, + [QueryClientWrapper()] ); const input = screen.getByTestId(TEXTFIELD_ID); diff --git a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx b/packages/shared/src/LinodeSelect/LinodeSelect.tsx similarity index 96% rename from packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx rename to packages/shared/src/LinodeSelect/LinodeSelect.tsx index 6903748ed41..f497d095217 100644 --- a/packages/manager/src/features/Linodes/LinodeSelect/LinodeSelect.tsx +++ b/packages/shared/src/LinodeSelect/LinodeSelect.tsx @@ -1,9 +1,7 @@ -import { Autocomplete, CustomPopper } from '@linode/ui'; -import React from 'react'; - -import Close from 'src/assets/icons/close.svg'; import { useAllLinodesQuery } from '@linode/queries'; -import { mapIdsToDevices } from 'src/utilities/mapIdsToDevices'; +import { Autocomplete, CloseIcon, CustomPopper } from '@linode/ui'; +import { mapIdsToDevices } from '@linode/utilities'; +import React from 'react'; import type { APIError, Filter, Linode } from '@linode/api-v4'; import type { SxProps, Theme } from '@mui/material/styles'; @@ -142,7 +140,6 @@ export const LinodeSelect = ( : linodes?.find(value) ?? null : mapIdsToDevices(value, linodes) } - ChipProps={{ deleteIcon: }} PopperComponent={CustomPopper} clearOnBlur={false} data-testid="add-linode-autocomplete" @@ -162,6 +159,7 @@ export const LinodeSelect = ( onBlur={onBlur} onInputChange={(_, value) => setInputValue(value)} options={options || (linodes ?? [])} + slotProps={{ chip: { deleteIcon: } }} sx={sx} /> ); diff --git a/packages/shared/src/LinodeSelect/index.ts b/packages/shared/src/LinodeSelect/index.ts new file mode 100644 index 00000000000..c9e407ede3d --- /dev/null +++ b/packages/shared/src/LinodeSelect/index.ts @@ -0,0 +1 @@ +export * from './LinodeSelect'; diff --git a/packages/shared/src/env.d.ts b/packages/shared/src/env.d.ts new file mode 100644 index 00000000000..f36c722baed --- /dev/null +++ b/packages/shared/src/env.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const src: ComponentClass; + export default src; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 00000000000..c9e407ede3d --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from './LinodeSelect'; diff --git a/packages/shared/src/utilities/wrap.tsx b/packages/shared/src/utilities/wrap.tsx new file mode 100644 index 00000000000..4893cdafa9f --- /dev/null +++ b/packages/shared/src/utilities/wrap.tsx @@ -0,0 +1,29 @@ +/* eslint-disable react-refresh/only-export-components */ +import { queryClientFactory } from '@linode/queries'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import type { RenderResult } from '@testing-library/react'; + +type Wrapper = (ui: React.ReactNode) => React.ReactNode; + +export const wrap = ( + ui: React.ReactNode, + wrappers: Wrapper[] +): React.ReactNode => wrappers.reduce((prev, wrapper) => wrapper(prev), ui); + +export const renderWithWrappers = ( + ui: React.ReactNode, + wrappers: Wrapper[] +): RenderResult => { + const renderResult = render(wrap(ui, wrappers)); + return { + ...renderResult, + rerender: (ui) => renderResult.rerender(wrap(ui, wrappers)), + }; +}; + +export const QueryClientWrapper = (queryClient = queryClientFactory()) => ( + ui: React.ReactNode +) => {ui}; diff --git a/packages/shared/testSetup.ts b/packages/shared/testSetup.ts new file mode 100644 index 00000000000..141cd45f9a4 --- /dev/null +++ b/packages/shared/testSetup.ts @@ -0,0 +1,9 @@ +import * as matchers from '@testing-library/jest-dom/matchers'; +import { cleanup } from '@testing-library/react'; +import { afterEach, expect } from 'vitest'; + +expect.extend(matchers); + +afterEach(() => { + cleanup(); +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 00000000000..160560a852f --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "jsx": "react", + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "noEmit": true, + "allowUnreachableCode": false, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "incremental": true, + "types": ["@testing-library/jest-dom"] + }, + "include": ["src"] +} diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts new file mode 100644 index 00000000000..ba5b20959d2 --- /dev/null +++ b/packages/shared/vitest.config.ts @@ -0,0 +1,11 @@ +import svgr from 'vite-plugin-svgr'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [svgr({ exportAsDefault: true })], + + test: { + environment: 'jsdom', + setupFiles: './testSetup.ts', + }, +}); diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index 7554d0d7358..8cd516084a3 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -4,6 +4,7 @@ export { default as CheckboxIcon } from './checkbox.svg'; export { default as CheckboxCheckedIcon } from './checkboxChecked.svg'; export { default as CheckboxIndeterminateIcon } from './checkboxIndeterminate.svg'; export { default as ChevronDownIcon } from './chevron-down.svg'; +export { default as CloseIcon } from './close.svg'; export { default as InfoIcon } from './info.svg'; export { default as PendingIcon } from './pending.svg'; export { default as PlusSignIcon } from './plusSign.svg'; diff --git a/packages/utilities/package.json b/packages/utilities/package.json index b6c08885330..406d77685e7 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -2,7 +2,7 @@ "name": "@linode/utilities", "version": "0.1.0", "description": "Linode Utility functions library", - "main": "src/index.js", + "main": "src/index.ts", "module": "src/index.ts", "types": "src/index.ts", "author": "Linode", diff --git a/packages/utilities/src/factories/index.ts b/packages/utilities/src/factories/index.ts index 2aecf0b5021..ea88b488e7c 100644 --- a/packages/utilities/src/factories/index.ts +++ b/packages/utilities/src/factories/index.ts @@ -1,6 +1,8 @@ export * from './accountAvailability'; export * from './config'; export * from './factoryProxy'; -export * from './linodeConfigInterfaceFactory'; +export * from './linodes'; +export * from './linodeConfigInterface'; export * from './linodeInterface'; +export * from './nodebalancer'; export * from './regions'; diff --git a/packages/utilities/src/factories/linodeConfigInterfaceFactory.ts b/packages/utilities/src/factories/linodeConfigInterface.ts similarity index 100% rename from packages/utilities/src/factories/linodeConfigInterfaceFactory.ts rename to packages/utilities/src/factories/linodeConfigInterface.ts diff --git a/packages/manager/src/factories/linodes.ts b/packages/utilities/src/factories/linodes.ts similarity index 99% rename from packages/manager/src/factories/linodes.ts rename to packages/utilities/src/factories/linodes.ts index 859472752b8..c6fca8aa6eb 100644 --- a/packages/manager/src/factories/linodes.ts +++ b/packages/utilities/src/factories/linodes.ts @@ -1,4 +1,4 @@ -import { Factory } from '@linode/utilities'; +import { Factory } from './factoryProxy'; import type { CreateLinodeRequest, diff --git a/packages/manager/src/factories/nodebalancer.ts b/packages/utilities/src/factories/nodebalancer.ts similarity index 97% rename from packages/manager/src/factories/nodebalancer.ts rename to packages/utilities/src/factories/nodebalancer.ts index b3c41b6b264..6a9ebe18621 100644 --- a/packages/manager/src/factories/nodebalancer.ts +++ b/packages/utilities/src/factories/nodebalancer.ts @@ -1,4 +1,4 @@ -import { Factory } from '@linode/utilities'; +import { Factory } from './factoryProxy'; import type { NodeBalancer, diff --git a/packages/utilities/src/helpers/index.ts b/packages/utilities/src/helpers/index.ts index 056390a60df..976db2d3ac6 100644 --- a/packages/utilities/src/helpers/index.ts +++ b/packages/utilities/src/helpers/index.ts @@ -27,6 +27,7 @@ export * from './isNumber'; export * from './isToday'; export * from './link'; export * from './manuallySetVPCConfigInterfacesToActive'; +export * from './mapIdsToDevices'; export * from './maybeCastToNumber'; export * from './metadata'; export * from './minute-conversion'; diff --git a/packages/manager/src/utilities/mapIdsToDevices.test.ts b/packages/utilities/src/helpers/mapIdsToDevices.test.ts similarity index 86% rename from packages/manager/src/utilities/mapIdsToDevices.test.ts rename to packages/utilities/src/helpers/mapIdsToDevices.test.ts index af30a9537d1..b379a125582 100644 --- a/packages/manager/src/utilities/mapIdsToDevices.test.ts +++ b/packages/utilities/src/helpers/mapIdsToDevices.test.ts @@ -1,10 +1,9 @@ -import { nodeBalancerFactory } from 'src/factories'; -import { linodeFactory } from 'src/factories'; +import { linodeFactory, nodeBalancerFactory } from '../factories'; +import { describe, it, expect } from 'vitest'; import { mapIdsToDevices } from './mapIdsToDevices'; -import type { NodeBalancer } from '@linode/api-v4'; -import type { Linode } from '@linode/api-v4'; +import type { NodeBalancer, Linode } from '@linode/api-v4'; describe('mapIdsToDevices', () => { const linodes = linodeFactory.buildList(5); diff --git a/packages/manager/src/utilities/mapIdsToDevices.ts b/packages/utilities/src/helpers/mapIdsToDevices.ts similarity index 91% rename from packages/manager/src/utilities/mapIdsToDevices.ts rename to packages/utilities/src/helpers/mapIdsToDevices.ts index f8d6e856d92..065b86ff360 100644 --- a/packages/manager/src/utilities/mapIdsToDevices.ts +++ b/packages/utilities/src/helpers/mapIdsToDevices.ts @@ -1,4 +1,4 @@ -import { isNotNullOrUndefined } from '@linode/utilities'; +import { isNotNullOrUndefined } from '../helpers'; import type { Linode, NodeBalancer } from '@linode/api-v4'; diff --git a/packages/utilities/src/helpers/sort-by.test.ts b/packages/utilities/src/helpers/sort-by.test.ts index aaa1017ff38..6109eb26bc6 100644 --- a/packages/utilities/src/helpers/sort-by.test.ts +++ b/packages/utilities/src/helpers/sort-by.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { sortByVersion } from '@linode/utilities'; +import { sortByVersion } from './sort-by'; describe('sortByVersion', () => { it('should identify the later major version as greater', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5133ce857f..63a9746e989 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,6 +109,9 @@ importers: '@linode/search': specifier: workspace:* version: link:../search + '@linode/shared': + specifier: workspace:* + version: link:../shared '@linode/ui': specifier: workspace:* version: link:../ui @@ -313,46 +316,46 @@ importers: version: 0.0.10(eslint@7.32.0) '@storybook/addon-a11y': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-actions': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-controls': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-docs': specifier: ^8.6.7 - version: 8.6.7(@types/react@18.3.12)(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(@types/react@18.3.12)(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-mdx-gfm': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-measure': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-storysource': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/addon-viewport': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/blocks': specifier: ^8.6.7 - version: 8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)) '@storybook/manager-api': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/preview-api': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/react': specifier: ^8.6.7 - version: 8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3) + version: 8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3) '@storybook/react-vite': specifier: ^8.6.7 - version: 8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.34.8)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + version: 8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.34.8)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) '@storybook/theming': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@swc/core': specifier: ^1.10.9 version: 1.10.11 @@ -454,7 +457,7 @@ importers: version: 6.21.0(eslint@7.32.0)(typescript@5.7.3) '@vitejs/plugin-react-swc': specifier: ^3.7.2 - version: 3.7.2(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + version: 3.7.2(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) '@vitest/coverage-v8': specifier: ^3.0.7 version: 3.0.7(vitest@3.0.7) @@ -484,7 +487,7 @@ importers: version: 1.14.0(cypress@14.0.1) cypress-vite: specifier: ^1.6.0 - version: 1.6.0(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + version: 1.6.0(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) dotenv: specifier: ^16.0.3 version: 16.4.5 @@ -559,16 +562,16 @@ importers: version: 1.5.5(redux@4.2.1) storybook: specifier: ^8.6.7 - version: 8.6.7(prettier@2.2.1) + version: 8.6.9(prettier@2.2.1) storybook-dark-mode: specifier: 4.0.1 - version: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)) + version: 4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)) vite: specifier: ^6.2.2 - version: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + version: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) vite-plugin-svgr: specifier: ^3.2.0 - version: 3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + version: 3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) packages/queries: dependencies: @@ -676,6 +679,118 @@ importers: specifier: ^5 || ^6 version: 6.1.1(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + packages/shared: + dependencies: + '@linode/api-v4': + specifier: workspace:* + version: link:../api-v4 + '@linode/queries': + specifier: workspace:* + version: link:../queries + '@linode/ui': + specifier: workspace:* + version: link:../ui + '@linode/utilities': + specifier: workspace:* + version: link:../utilities + '@mui/material': + specifier: ^6.4.5 + version: 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: 5.51.24 + version: 5.51.24(react@18.3.1) + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + devDependencies: + '@linode/eslint-plugin-cloud-manager': + specifier: ^0.0.10 + version: 0.0.10(eslint@7.32.0) + '@storybook/addon-actions': + specifier: ^8.6.7 + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) + '@storybook/react': + specifier: ^8.6.7 + version: 8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3) + '@testing-library/dom': + specifier: ^10.1.0 + version: 10.4.0 + '@testing-library/jest-dom': + specifier: ~6.4.2 + version: 6.4.8 + '@testing-library/react': + specifier: ~16.0.0 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.5.2 + version: 14.5.2(@testing-library/dom@10.4.0) + '@types/react': + specifier: ^18.2.55 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.1 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@7.32.0)(typescript@5.7.3))(eslint@7.32.0)(typescript@5.7.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@7.32.0)(typescript@5.7.3) + eslint: + specifier: ^7.1.0 + version: 7.32.0 + eslint-config-prettier: + specifier: ~8.1.0 + version: 8.1.0(eslint@7.32.0) + eslint-plugin-cypress: + specifier: ^2.11.3 + version: 2.15.2(eslint@7.32.0) + eslint-plugin-jsx-a11y: + specifier: ^6.7.1 + version: 6.10.2(eslint@7.32.0) + eslint-plugin-perfectionist: + specifier: ^1.4.0 + version: 1.5.1(eslint@7.32.0)(typescript@5.7.3) + eslint-plugin-prettier: + specifier: ~3.3.1 + version: 3.3.1(eslint-config-prettier@8.1.0(eslint@7.32.0))(eslint@7.32.0)(prettier@2.2.1) + eslint-plugin-ramda: + specifier: ^2.5.1 + version: 2.5.1 + eslint-plugin-react: + specifier: ^7.19.0 + version: 7.37.2(eslint@7.32.0) + eslint-plugin-react-hooks: + specifier: ^3.0.0 + version: 3.0.0(eslint@7.32.0) + eslint-plugin-react-refresh: + specifier: ^0.4.13 + version: 0.4.13(eslint@7.32.0) + eslint-plugin-scanjs-rules: + specifier: ^0.2.1 + version: 0.2.1 + eslint-plugin-sonarjs: + specifier: ^0.5.0 + version: 0.5.0(eslint@7.32.0) + eslint-plugin-testing-library: + specifier: ^3.1.2 + version: 3.10.2(eslint@7.32.0)(typescript@5.7.3) + eslint-plugin-xss: + specifier: ^0.1.10 + version: 0.1.12 + lint-staged: + specifier: ^15.2.9 + version: 15.4.3 + prettier: + specifier: ~2.2.1 + version: 2.2.1 + vite-plugin-svgr: + specifier: ^3.2.0 + version: 3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + packages/ui: dependencies: '@emotion/react': @@ -717,13 +832,13 @@ importers: version: 0.0.10(eslint@7.32.0) '@storybook/addon-actions': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/preview-api': specifier: ^8.6.7 - version: 8.6.7(storybook@8.6.7(prettier@2.2.1)) + version: 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/react': specifier: ^8.6.7 - version: 8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3) + version: 8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3) '@testing-library/dom': specifier: ^10.1.0 version: 10.4.0 @@ -768,7 +883,7 @@ importers: version: 2.2.1 vite-plugin-svgr: specifier: ^3.2.0 - version: 3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + version: 3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) packages/utilities: dependencies: @@ -999,34 +1114,29 @@ packages: resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.26.10': - resolution: {integrity: sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==} + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} engines: {node: '>=6.9.0'} '@babel/highlight@7.25.9': resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.26.10': - resolution: {integrity: sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==} - engines: {node: '>=6.0.0'} - hasBin: true - '@babel/parser@7.26.2': resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.26.10': - resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/template@7.25.9': - resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@babel/template@7.26.9': - resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} '@babel/traverse@7.25.9': @@ -1037,10 +1147,6 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} - '@babel/types@7.26.10': - resolution: {integrity: sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==} - engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -2057,67 +2163,67 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@storybook/addon-a11y@8.6.7': - resolution: {integrity: sha512-/pGRa27AVpoFG0J2+PTKSQCk6ytbRkcR+5fi75iLlqgp7YZN9rVJ8SYyEXALf/B8Gw9hSk2uxCyT3dA7ZTy52Q==} + '@storybook/addon-a11y@8.6.9': + resolution: {integrity: sha512-X5s5RFLORwFjDXcEJitFKar0MMIUgp9JUfcT9VhQfJjnvZf7urf+9M2UGD9TwWAta5EAUBpGDkt9cqDi2UvTxA==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-actions@8.6.7': - resolution: {integrity: sha512-XgZCwIcZGThEyD7e2q7rN/jzg7ZHUxn/ln403eex04jWAGBBbtC2IVuowwCWV8HwDihnhpCZEP6HlgjakOYZbQ==} + '@storybook/addon-actions@8.6.9': + resolution: {integrity: sha512-H2v17sMbSl8jhSulPxcOyChsFbzik9E7mgCWIf4P114KcIUokWLVuALnSOeqHME6lY0pPBZs3DgvVVMVMm7zNw==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-controls@8.6.7': - resolution: {integrity: sha512-6ReB1Sc1qlqvAM7NUmtw2K1cKCgGBs8zYRgL44Q2ti+r55a2ownhm6WUm/kZs2ixSkV9ehm1osiqbGBfAn0Isw==} + '@storybook/addon-controls@8.6.9': + resolution: {integrity: sha512-YXBYsbHqdYhmrbGI+wv9LAr/LlKnPt9f9GL+9rw82lnYadWObYxzUxs+PPLNO5tc14fd2g+FMVHOfovaRdFvrQ==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-docs@8.6.7': - resolution: {integrity: sha512-kgNPEVuLGNJE8EdVQi5Tg2DYgR66/gut07jvhqnJfNqUkj6UpBHad0JR1uwrd7xS3kJs29Fs7UyU87RJnSlwcg==} + '@storybook/addon-docs@8.6.9': + resolution: {integrity: sha512-yAP59G5Vd+E6O9KLfBR5ALdOFA5yEZ0n1f8Ne9jwF+NGu1U8KNIfWnZmBYaBGe+bpYn0CWV5AfdFvw83bzHYpw==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-highlight@8.6.7': - resolution: {integrity: sha512-4KE1RF4XfqII7XrJPgf/1W0t0EWRKmik5Rrpb6WofXfgZ2QYzLFnyESjf67/g2TMgDnle2drfa/pt5tGV4+I2Q==} + '@storybook/addon-highlight@8.6.9': + resolution: {integrity: sha512-I0gBHgaH74wX6yf5S7zUmdfr25hwPONpSAqPPGBSNYu0Jj9Je+ANr1y4T1I3cOaEvf73QntDhCgHC6/iqY90Fw==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-mdx-gfm@8.6.7': - resolution: {integrity: sha512-IfGgPnOMq51yBpnaY2w5hlm4pBgIMig61vsigqySU7KKFY6qxD/LcIJAxOPh2s9dLhYGYDrO0hFGR9fQ7Niu5A==} + '@storybook/addon-mdx-gfm@8.6.9': + resolution: {integrity: sha512-NG8wDB27WM3f24r5A69G1lcA58jisnPQIdT91tNEj089tlRoN0m0eXEmv5X4Gd13M0JgiuEhLU8ywAgpmeEHuQ==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-measure@8.6.7': - resolution: {integrity: sha512-4dkkCltjKRcJH+ZMv5nbNT0LBQfcXIydVfN9mAvhDsiPFD5eZcHbN4XVfUslECWgrkaa/a6FE1W9PNEUBjCJaA==} + '@storybook/addon-measure@8.6.9': + resolution: {integrity: sha512-2GrHtaYZgM7qeil5/XfNJrdnan7hoLLUyU7w7fph0EVl7tiwmhtp4He0PX9hrT/Abk2HxeCP4WU2fAGwIuTkYg==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-storysource@8.6.7': - resolution: {integrity: sha512-tIoTQp3MMyF3S4XarMOBVO40DofILO3Mz8upT4wGEfQULLjgCkS2K5c4BbT4de1hF49JsqvPByVlavntWQFTdg==} + '@storybook/addon-storysource@8.6.9': + resolution: {integrity: sha512-BPEUhEuo6JijM71ZNAPziXQur1HzN12iekYJnpfsJZF/cUwZWjTF2HeXBn0Z2DjOjqtBzoSPaJgT7CLVAKucsQ==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/addon-viewport@8.6.7': - resolution: {integrity: sha512-kTrt6ByCbBIbqoRqQO9watDl5nSIKCC+R0/EmpEl6ZtzBV3l8trZHdvCHhIqOyv7nfaa7pIeTTG1GD6Gdrxk3w==} + '@storybook/addon-viewport@8.6.9': + resolution: {integrity: sha512-1xkozyB1zs3eSNTc8ePAMcajUfbKvNMTjs5LYdts2N1Ss0xeZ+K/gphfRg0GaYsNvRYi5piufag/niHCGkT3hA==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/blocks@8.6.7': - resolution: {integrity: sha512-IFhIKO7R1UPpnoG/5tZH0FgC79oYgXNf+7aGUwq29M/CQWy6p/Pvp0y4P962btY1UZRol+SsU//33nH8o6yNRw==} + '@storybook/blocks@8.6.9': + resolution: {integrity: sha512-+vSRkHLD7ho3Wd1WVA1KrYAnv7BnGHOhHWHAgTR5IdeMdgzQxm6+HHeqGB5sncilA0AjVC6udBIgHbCSuD61dA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^8.6.7 + storybook: ^8.6.9 peerDependenciesMeta: react: optional: true react-dom: optional: true - '@storybook/builder-vite@8.6.7': - resolution: {integrity: sha512-hgYnVu2cy8clrmDwidu4XjvFMTEi9WiblLH5cPI3LWQjVajIQmDpcWVp6kbD063sIOphh9zYP7cVKGO7ktMB/g==} + '@storybook/builder-vite@8.6.9': + resolution: {integrity: sha512-8U11A7sLPvvcnJQ3pXyoX1LdJDpa4+JOYcASL9A+DL591jkfYKxhim7R4BOHO55aetmqQAoA/LEAD5runu7zoQ==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 vite: ^4.0.0 || ^5.0.0 || ^6.0.0 '@storybook/components@8.4.5': @@ -2125,8 +2231,8 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/components@8.6.7': - resolution: {integrity: sha512-8pnjH1w7PZ/Iiuve1/BJY7EO/kmu0qdE34X1ZM8DyHzuy33EL/PfUuhxNkrL4ayMXrEDp/EJMHx2bqO1RdRV6A==} + '@storybook/components@8.6.9': + resolution: {integrity: sha512-CqWUAYK/RgV++sXfiDG63DM2JF2FeidvnMO5/bki2hFbEqgs0/yy7BKUjhsGmuri5y+r9B2FJhW0WnE6PI8NWw==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 @@ -2135,18 +2241,18 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/core@8.6.7': - resolution: {integrity: sha512-FcvLFA+Qn3+D6LgQkk0MOXA5FBz8DGc0UZmZuVbIwIUV4MV4ywCMwtKdG0cyhtzQg0YNyfiIYWJr7lZ4jLLhYg==} + '@storybook/core@8.6.9': + resolution: {integrity: sha512-psYxJAlj34ZaDAk+OvT/He6ZuUh0eGiHVtZNe0xWbNp5pQvOBjf+dg48swdI6KEbVs3aeU+Wnyra/ViU2RtA+Q==} peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: prettier: optional: true - '@storybook/csf-plugin@8.6.7': - resolution: {integrity: sha512-HK7yQD4kFu04JOKnUwoFeR58r5WY6ucF0D8zfW4Gx+r8hBJ5K4t3z6k2dlIlRQF1X5+2vNkQOwD8liHjckuZ8Q==} + '@storybook/csf-plugin@8.6.9': + resolution: {integrity: sha512-IQnhyaVUkcRR9e4xiHN83xMQtTMH+lJp472iMifUIqxx/Yw137BTef2DEEp6EnRct4yKrch24+Nl65LWg0mRpQ==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 '@storybook/global@5.0.0': resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} @@ -2158,49 +2264,49 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - '@storybook/instrumenter@8.6.7': - resolution: {integrity: sha512-FeQiV0g5crCWs0P1wKY4xZzb4PxAYNcrm2+9LLGVqwnC7qzrSCPf0p10MlveVfwsen1m6Wbqfe+wl21c31Hfmg==} + '@storybook/instrumenter@8.6.9': + resolution: {integrity: sha512-Gp6OSiu9KA/p1HWd7VW9TtpWX32ZBfqRVrOm4wW1AM6B4XACbQWFE/aQ25HwU834yfdJkr2BW+uUH8DBAQ6kTw==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/manager-api@8.6.7': - resolution: {integrity: sha512-BA8RxaLP07WGF660LWo7qB3Jomr/+MPuCZmuKPqXxPhfIovqYjr0hnugxJBjEah0ic31aNX4NucNfDRuV7F5sA==} + '@storybook/manager-api@8.6.9': + resolution: {integrity: sha512-mxq9B9rxAraOCBapGKsUDfI+8yNtFhTgKMZCxmHoUCxvAHaIt4S9JcdX0qQQKUsBTr/b2hHm0O7A8DYrbgBRfw==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/preview-api@8.6.7': - resolution: {integrity: sha512-Rz83Nx43v3Dn9/SjhIsorkcx1gPmlclueuzf6YywJTqE1E/L4dgoe2mOA9MfF0jr0bh3TwEA2J3ii0Jstg1Orw==} + '@storybook/preview-api@8.6.9': + resolution: {integrity: sha512-hW3Z8NBrGs2bNunaHgrLjpfrOcWsxH0ejAqaba8MolPXjzNs0lTFF/Ela7pUsh2m1R4/kiD+WfddQzyipUo4Mg==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - '@storybook/react-dom-shim@8.6.7': - resolution: {integrity: sha512-+JH7gbRI6NRbt9o0l1rY4wFdeVt8wGRddm0b55OBlwBGlFo2nvGVOH73J4AGphXVhfY7z33I3TXIjXQ561UdEQ==} + '@storybook/react-dom-shim@8.6.9': + resolution: {integrity: sha512-SjqP6r5yy87OJRAiq1JzFazn6VWfptOA2HaxOiP8zRhJgG41K0Vseh8tbZdycj1AzJYSCcnKaIcfd/GEo/41+g==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/react-vite@8.6.7': - resolution: {integrity: sha512-KiTeYaZ+AUQ1AFHSItP8dhUbd2v7Qy8+BB7w64VxQMw/dw5n0Z38lo4Tzdlkn22q2smW2ce4QwAzh2pfTz3b8g==} + '@storybook/react-vite@8.6.9': + resolution: {integrity: sha512-V81hRb2zv+LsKJnyjXQMYzL7ojdp92C3ThQ3r+SFGxKxY9t1JdoxloRmeyrN6XHyIAYhiJRwni0E1RagtPWG1g==} engines: {node: '>=18.0.0'} peerDependencies: - '@storybook/test': 8.6.7 + '@storybook/test': 8.6.9 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.7 + storybook: ^8.6.9 vite: ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: '@storybook/test': optional: true - '@storybook/react@8.6.7': - resolution: {integrity: sha512-6R8znSm7kzsoAJyRbEiDWE+5xjeAIzwEcfT60fqx+uMdd0vDFM7f2uT4fYy+CijWas1oFWcNV/LMd3EqSkBGsQ==} + '@storybook/react@8.6.9': + resolution: {integrity: sha512-xu4eJyYNz3mHeqnHn80KZZ2s22ZfqqCTzCNCVAyM6MWTxUwIpLX6FXC/vmcT1gPwwTl2KcRHZXaE7snB3aOLuw==} engines: {node: '>=18.0.0'} peerDependencies: - '@storybook/test': 8.6.7 + '@storybook/test': 8.6.9 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.7 + storybook: ^8.6.9 typescript: '>= 4.2.x' peerDependenciesMeta: '@storybook/test': @@ -2208,18 +2314,18 @@ packages: typescript: optional: true - '@storybook/source-loader@8.6.7': - resolution: {integrity: sha512-ycfrPHCs5OUrJTLCXDxvxLVB1zjL7IEepPs53o4RGRWO8xV1z0QfXXiX1drk48rep6dDu+a3mRWfNJ8m0RV/GA==} + '@storybook/source-loader@8.6.9': + resolution: {integrity: sha512-Ogh3HjJoAiKD7svqqyA+bM8xOZm2kuMs0jkYsrfuZG6BlO9xVuTSviGRWHowUEEznQZOBz8a++SmTaqNEoen6g==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/test@8.6.7': - resolution: {integrity: sha512-uF1JbBtdT7tuiXfEtHsUShBHIhm2vc0C39nKVJaTWyK9CybajXaj2Ny3IRa3oY9NKnklwGgN+kZ/Z9YiIOc4MQ==} + '@storybook/test@8.6.9': + resolution: {integrity: sha512-lIJA6jup3ZZNkKFyUiy1q2tHWZv5q5bTaLxTnI85XIWr+sFCZG5oo3pOQESBkX4V95rv8sq9gEmEWySZvW7MBw==} peerDependencies: - storybook: ^8.6.7 + storybook: ^8.6.9 - '@storybook/theming@8.6.7': - resolution: {integrity: sha512-F/i4XS5bew9dvtNiHvDJF0mko1IUbPM9PUjTYPaw6cK8ytS0kdec703MsJ/GUA7seeEWBeGdZjV3ua0pys650A==} + '@storybook/theming@8.6.9': + resolution: {integrity: sha512-FQafe66itGnIh0V42R65tgFKyz0RshpIs0pTrxrdByuB2yKsep+f8ZgKLJE3fCKw/Egw4bUuICo2m8d7uOOumA==} peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 @@ -3357,8 +3463,8 @@ packages: engines: {node: '>=18'} hasBin: true - consola@3.4.1: - resolution: {integrity: sha512-zaUUWockhqxFf4bSXS+kTJwxWvAyMuKtShx0BWcGrMEUqbETcBCT91iQs9pECNx7yz8VH4VeWW/1KAbhE8kiww==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} consolidated-events@2.0.2: @@ -5992,8 +6098,8 @@ packages: storybook-dark-mode@4.0.1: resolution: {integrity: sha512-9l3qY8NdgwZnY+NlO1XHB3eUb6FmZo9GazJeUSeFkjRqwA5FmnMSeq0YVqEOqfwniM/TvQwOiTYd5g/hC2wugA==} - storybook@8.6.7: - resolution: {integrity: sha512-9gktoFMQDSCINNGQH869d/sar9rVtAhr0HchcvDA6bssAqgQJvTphY4qC9lH54SxfTJm/7Sy+BKEngMK+dziJg==} + storybook@8.6.9: + resolution: {integrity: sha512-Iw4+R4V3yX7MhXJaLBAT4oLtZ+SaTzX8KvUNZiQzvdD+TrFKVA3QKV8gvWjstGyU2dd+afE1Ph6EG5Xa2Az2CA==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -6536,8 +6642,8 @@ packages: yaml: optional: true - vite@6.2.2: - resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} + vite@6.2.3: + resolution: {integrity: sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -6891,7 +6997,7 @@ snapshots: '@babel/generator': 7.26.2 '@babel/helper-compilation-targets': 7.25.9 '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) - '@babel/helpers': 7.26.10 + '@babel/helpers': 7.26.0 '@babel/parser': 7.26.2 '@babel/template': 7.25.9 '@babel/traverse': 7.25.9 @@ -6942,10 +7048,10 @@ snapshots: '@babel/helper-validator-option@7.25.9': {} - '@babel/helpers@7.26.10': + '@babel/helpers@7.26.0': dependencies: - '@babel/template': 7.26.9 - '@babel/types': 7.26.10 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 '@babel/highlight@7.25.9': dependencies: @@ -6954,15 +7060,15 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/parser@7.26.10': - dependencies: - '@babel/types': 7.26.10 - '@babel/parser@7.26.2': dependencies: '@babel/types': 7.26.0 - '@babel/runtime@7.26.10': + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 @@ -6972,12 +7078,6 @@ snapshots: '@babel/parser': 7.26.2 '@babel/types': 7.26.0 - '@babel/template@7.26.9': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 - '@babel/traverse@7.25.9': dependencies: '@babel/code-frame': 7.26.2 @@ -6995,11 +7095,6 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - '@babel/types@7.26.10': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@bcoe/v8-coverage@1.0.2': {} '@braintree/asset-loader@2.0.0': {} @@ -7098,7 +7193,7 @@ snapshots: '@emotion/babel-plugin@11.13.5': dependencies: '@babel/helper-module-imports': 7.25.9 - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@emotion/hash': 0.9.2 '@emotion/memoize': 0.9.0 '@emotion/serialize': 1.3.3 @@ -7129,7 +7224,7 @@ snapshots: '@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.13.5 '@emotion/serialize': 1.3.3 @@ -7155,7 +7250,7 @@ snapshots: '@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.3.1 '@emotion/react': 11.13.5(@types/react@18.3.12)(react@18.3.1) @@ -7504,12 +7599,12 @@ snapshots: '@istanbuljs/schema@0.1.3': {} - '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': + '@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': dependencies: glob: 10.4.5 magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.7.3) - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) optionalDependencies: typescript: 5.7.3 @@ -7580,7 +7675,7 @@ snapshots: '@mui/icons-material@6.4.5(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/material': 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 optionalDependencies: @@ -7588,7 +7683,7 @@ snapshots: '@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/core-downloads-tracker': 6.4.5 '@mui/system': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/types': 7.2.21(@types/react@18.3.12) @@ -7609,7 +7704,7 @@ snapshots: '@mui/private-theming@6.4.3(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) prop-types: 15.8.1 react: 18.3.1 @@ -7618,7 +7713,7 @@ snapshots: '@mui/styled-engine@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@emotion/cache': 11.13.5 '@emotion/serialize': 1.3.3 '@emotion/sheet': 1.4.0 @@ -7631,7 +7726,7 @@ snapshots: '@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/private-theming': 6.4.3(@types/react@18.3.12)(react@18.3.1) '@mui/styled-engine': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) '@mui/types': 7.2.21(@types/react@18.3.12) @@ -7651,7 +7746,7 @@ snapshots: '@mui/utils@6.4.3(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/types': 7.2.21(@types/react@18.3.12) '@types/prop-types': 15.7.14 clsx: 2.1.1 @@ -7663,7 +7758,7 @@ snapshots: '@mui/x-date-pickers@7.27.0(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(dayjs@1.11.13)(luxon@3.4.4)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/material': 6.4.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mui/system': 6.4.3(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.5(@emotion/react@11.13.5(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1) '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) @@ -7685,7 +7780,7 @@ snapshots: '@mui/x-internals@7.26.0(@types/react@18.3.12)(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@mui/utils': 6.4.3(@types/react@18.3.12)(react@18.3.1) react: 18.3.1 transitivePeerDependencies: @@ -7938,110 +8033,110 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@storybook/addon-a11y@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-a11y@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - '@storybook/addon-highlight': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/addon-highlight': 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/global': 5.0.0 - '@storybook/test': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/test': 8.6.9(storybook@8.6.9(prettier@2.2.1)) axe-core: 4.10.2 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/addon-actions@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-actions@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 '@types/uuid': 9.0.8 dequal: 2.0.3 polished: 4.3.1 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) uuid: 9.0.1 - '@storybook/addon-controls@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-controls@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 dequal: 2.0.3 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) ts-dedent: 2.2.0 - '@storybook/addon-docs@8.6.7(@types/react@18.3.12)(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-docs@8.6.9(@types/react@18.3.12)(storybook@8.6.9(prettier@2.2.1))': dependencies: '@mdx-js/react': 3.1.0(@types/react@18.3.12)(react@18.3.1) - '@storybook/blocks': 8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)) - '@storybook/csf-plugin': 8.6.7(storybook@8.6.7(prettier@2.2.1)) - '@storybook/react-dom-shim': 8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)) + '@storybook/blocks': 8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)) + '@storybook/csf-plugin': 8.6.9(storybook@8.6.9(prettier@2.2.1)) + '@storybook/react-dom-shim': 8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/addon-highlight@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-highlight@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/addon-mdx-gfm@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-mdx-gfm@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: remark-gfm: 4.0.0 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) ts-dedent: 2.2.0 transitivePeerDependencies: - supports-color - '@storybook/addon-measure@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-measure@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) tiny-invariant: 1.3.3 - '@storybook/addon-storysource@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-storysource@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - '@storybook/source-loader': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/source-loader': 8.6.9(storybook@8.6.9(prettier@2.2.1)) estraverse: 5.3.0 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) tiny-invariant: 1.3.3 - '@storybook/addon-viewport@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/addon-viewport@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: memoizerific: 1.11.3 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/blocks@8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))': + '@storybook/blocks@8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) ts-dedent: 2.2.0 optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-vite@8.6.7(storybook@8.6.7(prettier@2.2.1))(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': + '@storybook/builder-vite@8.6.9(storybook@8.6.9(prettier@2.2.1))(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': dependencies: - '@storybook/csf-plugin': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/csf-plugin': 8.6.9(storybook@8.6.9(prettier@2.2.1)) browser-assert: 1.2.1 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) ts-dedent: 2.2.0 - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) - '@storybook/components@8.4.5(storybook@8.6.7(prettier@2.2.1))': + '@storybook/components@8.4.5(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/components@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/components@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/core-events@8.4.5(storybook@8.6.7(prettier@2.2.1))': + '@storybook/core-events@8.4.5(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/core@8.6.7(prettier@2.2.1)(storybook@8.6.7(prettier@2.2.1))': + '@storybook/core@8.6.9(prettier@2.2.1)(storybook@8.6.9(prettier@2.2.1))': dependencies: - '@storybook/theming': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/theming': 8.6.9(storybook@8.6.9(prettier@2.2.1)) better-opn: 3.0.2 browser-assert: 1.2.1 - esbuild: 0.25.1 - esbuild-register: 3.6.0(esbuild@0.25.1) + esbuild: 0.24.2 + esbuild-register: 3.6.0(esbuild@0.24.2) jsdoc-type-pratt-parser: 4.1.0 process: 0.11.10 recast: 0.23.9 @@ -8056,9 +8151,9 @@ snapshots: - supports-color - utf-8-validate - '@storybook/csf-plugin@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/csf-plugin@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) unplugin: 1.16.0 '@storybook/global@5.0.0': {} @@ -8068,84 +8163,84 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/instrumenter@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/instrumenter@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 '@vitest/utils': 2.1.5 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/manager-api@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/manager-api@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/preview-api@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/preview-api@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/react-dom-shim@8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))': + '@storybook/react-dom-shim@8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/react-vite@8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.34.8)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': + '@storybook/react-vite@8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.34.8)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) '@rollup/pluginutils': 5.1.3(rollup@4.34.8) - '@storybook/builder-vite': 8.6.7(storybook@8.6.7(prettier@2.2.1))(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) - '@storybook/react': 8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3) + '@storybook/builder-vite': 8.6.9(storybook@8.6.9(prettier@2.2.1))(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)) + '@storybook/react': 8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3) find-up: 5.0.0 magic-string: 0.30.17 react: 18.3.1 react-docgen: 7.1.0 react-dom: 18.3.1(react@18.3.1) resolve: 1.22.8 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) tsconfig-paths: 4.2.0 - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) optionalDependencies: - '@storybook/test': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/test': 8.6.9(storybook@8.6.9(prettier@2.2.1)) transitivePeerDependencies: - rollup - supports-color - typescript - '@storybook/react@8.6.7(@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1))(typescript@5.7.3)': + '@storybook/react@8.6.9(@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1))(typescript@5.7.3)': dependencies: - '@storybook/components': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/components': 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@storybook/global': 5.0.0 - '@storybook/manager-api': 8.6.7(storybook@8.6.7(prettier@2.2.1)) - '@storybook/preview-api': 8.6.7(storybook@8.6.7(prettier@2.2.1)) - '@storybook/react-dom-shim': 8.6.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)) - '@storybook/theming': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/manager-api': 8.6.9(storybook@8.6.9(prettier@2.2.1)) + '@storybook/preview-api': 8.6.9(storybook@8.6.9(prettier@2.2.1)) + '@storybook/react-dom-shim': 8.6.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)) + '@storybook/theming': 8.6.9(storybook@8.6.9(prettier@2.2.1)) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) optionalDependencies: - '@storybook/test': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/test': 8.6.9(storybook@8.6.9(prettier@2.2.1)) typescript: 5.7.3 - '@storybook/source-loader@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/source-loader@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: es-toolkit: 1.27.0 estraverse: 5.3.0 prettier: 3.3.3 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/test@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/test@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: '@storybook/global': 5.0.0 - '@storybook/instrumenter': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/instrumenter': 8.6.9(storybook@8.6.9(prettier@2.2.1)) '@testing-library/dom': 10.4.0 '@testing-library/jest-dom': 6.5.0 '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) '@vitest/expect': 2.0.5 '@vitest/spy': 2.0.5 - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) - '@storybook/theming@8.6.7(storybook@8.6.7(prettier@2.2.1))': + '@storybook/theming@8.6.9(storybook@8.6.9(prettier@2.2.1))': dependencies: - storybook: 8.6.7(prettier@2.2.1) + storybook: 8.6.9(prettier@2.2.1) '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.26.0)': dependencies: @@ -8313,14 +8408,14 @@ snapshots: '@testing-library/cypress@10.0.3(cypress@14.0.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@testing-library/dom': 10.4.0 cypress: 14.0.1 '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@types/aria-query': 5.0.4 aria-query: 5.3.0 chalk: 4.1.2 @@ -8331,7 +8426,7 @@ snapshots: '@testing-library/jest-dom@6.4.8': dependencies: '@adobe/css-tools': 4.4.1 - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 aria-query: 5.3.2 chalk: 3.0.0 css.escape: 1.5.1 @@ -8351,7 +8446,7 @@ snapshots: '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 '@testing-library/dom': 10.4.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -8367,24 +8462,24 @@ snapshots: '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.26.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.26.10 - '@babel/types': 7.26.10 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 '@types/babel__traverse@7.20.6': dependencies: - '@babel/types': 7.26.10 + '@babel/types': 7.26.0 '@types/braintree-web@3.96.15': dependencies: @@ -8755,10 +8850,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.7.2(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': + '@vitejs/plugin-react-swc@3.7.2(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1))': dependencies: '@swc/core': 1.10.11 - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' @@ -9094,7 +9189,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 cosmiconfig: 7.1.0 resolve: 1.22.8 @@ -9204,7 +9299,7 @@ snapshots: canvg@3.0.11: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.27.0 '@types/raf': 3.4.3 core-js: 3.39.0 raf: 3.4.1 @@ -9399,7 +9494,7 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 - consola@3.4.1: {} + consola@3.4.2: {} consolidated-events@2.0.2: {} @@ -9491,11 +9586,11 @@ snapshots: dependencies: cypress: 14.0.1 - cypress-vite@1.6.0(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)): + cypress-vite@1.6.0(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)): dependencies: chokidar: 3.6.0 debug: 4.4.0(supports-color@8.1.1) - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) transitivePeerDependencies: - supports-color @@ -9688,7 +9783,7 @@ snapshots: dom-helpers@5.2.1: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 csstype: 3.1.3 dompurify@3.2.4: @@ -9859,10 +9954,10 @@ snapshots: es-toolkit@1.27.0: {} - esbuild-register@3.6.0(esbuild@0.25.1): + esbuild-register@3.6.0(esbuild@0.24.2): dependencies: debug: 4.4.0(supports-color@8.1.1) - esbuild: 0.25.1 + esbuild: 0.24.2 transitivePeerDependencies: - supports-color @@ -10589,7 +10684,7 @@ snapshots: history@4.10.1: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 loose-envify: 1.4.0 resolve-pathname: 3.0.0 tiny-invariant: 1.3.3 @@ -11005,7 +11100,7 @@ snapshots: jspdf@3.0.1: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.27.0 atob: 2.1.2 btoa: 1.2.1 fflate: 0.8.2 @@ -11889,7 +11984,7 @@ snapshots: polished@4.3.1: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 possible-typed-array-names@1.0.0: {} @@ -12004,7 +12099,7 @@ snapshots: dependencies: '@babel/core': 7.26.0 '@babel/traverse': 7.25.9 - '@babel/types': 7.26.10 + '@babel/types': 7.26.0 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 '@types/doctrine': 0.0.9 @@ -12053,7 +12148,7 @@ snapshots: react-redux@7.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(redux@4.2.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 loose-envify: 1.4.0 @@ -12066,7 +12161,7 @@ snapshots: react-router-dom@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 history: 4.10.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -12083,7 +12178,7 @@ snapshots: react-router@5.3.4(react@18.3.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 history: 4.10.1 hoist-non-react-statics: 3.3.2 loose-envify: 1.4.0 @@ -12104,7 +12199,7 @@ snapshots: react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -12120,7 +12215,7 @@ snapshots: react-waypoint@10.3.0(react@18.3.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 consolidated-events: 2.0.2 prop-types: 15.8.1 react: 18.3.1 @@ -12163,7 +12258,7 @@ snapshots: recompose@0.30.0(react@18.3.1): dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 change-emitter: 0.1.6 fbjs: 0.8.18 hoist-non-react-statics: 2.5.5 @@ -12189,7 +12284,7 @@ snapshots: redux@4.2.1: dependencies: - '@babel/runtime': 7.26.10 + '@babel/runtime': 7.26.0 reflect.getprototypeof@1.0.6: dependencies: @@ -12550,14 +12645,14 @@ snapshots: std-env@3.8.0: {} - storybook-dark-mode@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.7(prettier@2.2.1)): + storybook-dark-mode@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.9(prettier@2.2.1)): dependencies: - '@storybook/components': 8.4.5(storybook@8.6.7(prettier@2.2.1)) - '@storybook/core-events': 8.4.5(storybook@8.6.7(prettier@2.2.1)) + '@storybook/components': 8.4.5(storybook@8.6.9(prettier@2.2.1)) + '@storybook/core-events': 8.4.5(storybook@8.6.9(prettier@2.2.1)) '@storybook/global': 5.0.0 '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/manager-api': 8.6.7(storybook@8.6.7(prettier@2.2.1)) - '@storybook/theming': 8.6.7(storybook@8.6.7(prettier@2.2.1)) + '@storybook/manager-api': 8.6.9(storybook@8.6.9(prettier@2.2.1)) + '@storybook/theming': 8.6.9(storybook@8.6.9(prettier@2.2.1)) fast-deep-equal: 3.1.3 memoizerific: 1.11.3 transitivePeerDependencies: @@ -12565,9 +12660,9 @@ snapshots: - react-dom - storybook - storybook@8.6.7(prettier@2.2.1): + storybook@8.6.9(prettier@2.2.1): dependencies: - '@storybook/core': 8.6.7(prettier@2.2.1)(storybook@8.6.7(prettier@2.2.1)) + '@storybook/core': 8.6.9(prettier@2.2.1)(storybook@8.6.9(prettier@2.2.1)) optionalDependencies: prettier: 2.2.1 transitivePeerDependencies: @@ -12871,7 +12966,7 @@ snapshots: bundle-require: 5.1.0(esbuild@0.25.1) cac: 6.7.14 chokidar: 4.0.3 - consola: 3.4.1 + consola: 3.4.2 debug: 4.4.0(supports-color@8.1.1) esbuild: 0.25.1 joycon: 3.1.1 @@ -13108,7 +13203,7 @@ snapshots: debug: 4.4.0(supports-color@8.1.1) es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) transitivePeerDependencies: - '@types/node' - jiti @@ -13123,12 +13218,12 @@ snapshots: - tsx - yaml - vite-plugin-svgr@3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)): + vite-plugin-svgr@3.3.0(rollup@4.34.8)(typescript@5.7.3)(vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1)): dependencies: '@rollup/pluginutils': 5.1.3(rollup@4.34.8) '@svgr/core': 8.1.0(typescript@5.7.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.7.3)) - vite: 6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) + vite: 6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) transitivePeerDependencies: - rollup - supports-color @@ -13147,7 +13242,7 @@ snapshots: tsx: 4.19.3 yaml: 2.6.1 - vite@6.2.2(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1): + vite@6.2.3(@types/node@20.17.6)(jiti@1.21.6)(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1): dependencies: esbuild: 0.25.1 postcss: 8.5.3 diff --git a/scripts/changelog/utils/constants.mjs b/scripts/changelog/utils/constants.mjs index 9b61ea0035e..d9aab768d70 100644 --- a/scripts/changelog/utils/constants.mjs +++ b/scripts/changelog/utils/constants.mjs @@ -10,6 +10,7 @@ export const PACKAGES = [ "api-v4", "manager", "queries", + "shared", "ui", "utilities", "validation", diff --git a/scripts/package-versions/index.js b/scripts/package-versions/index.js index 831ef3823f8..955aaa22b05 100644 --- a/scripts/package-versions/index.js +++ b/scripts/package-versions/index.js @@ -11,7 +11,8 @@ * - `` (Optional) Desired Validation package version. * - `` (Optional) Desired UI package version. * - `` (Optional) Desired Utilities package version. - * - `` (Optional) Desired Queries package version. + * - `` (Optional) Desired Queries package version. + * - `` (Optional) Desired Shared package version. * * Optional Flags: * - `-f | --force` Forces the script to update package versions without @@ -115,6 +116,7 @@ const jobs = [ { name: 'ui', path: getPackagePath('ui'), desiredVersion: desiredUiVersion }, { name: 'utilities', path: getPackagePath('utilities'), desiredVersion: desiredUtilitiesVersion }, { name: 'queries', path: getPackagePath('queries'), desiredVersion: desiredQueriesVersion }, + { name: 'shared', path: getPackagePath('shared'), desiredVersion: desiredSharedVersion }, ]; // Describes the files that will be written to, and the changes that will be made. diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 224072aade5..d54bb838a3a 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -2,6 +2,7 @@ export default [ "packages/api-v4", "packages/manager", "packages/search", + "packages/shared", "packages/ui", "packages/utilities", ]; From f3ddf2b7254f837cb30ad9f25ef85b48a7fbd417 Mon Sep 17 00:00:00 2001 From: Ankita Date: Thu, 27 Mar 2025 09:49:17 +0530 Subject: [PATCH 33/84] upcoming:[DI-24187] - Update success message for edit/create/enable/disable alert (#11903) * upcoming:[DI-24187] - Update success message for edit alert * upcoming:[DI-24187] - Add changeset * upcoming:[DI-24187] - Update const name * upcoming:[DI-24187] - Update success message for create/edit/enable/disable alert * upcoming:[DI-24187] - Update changeset for last commit --------- Co-authored-by: venkatmano-akamai --- .../pr-11903-upcoming-features-1742542218295.md | 5 +++++ .../e2e/core/cloudpulse/alerts-listing-page.spec.ts | 9 ++++++--- .../e2e/core/cloudpulse/create-user-alert.spec.ts | 3 ++- .../cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts | 3 ++- .../Alerts/AlertsListing/AlertListTable.test.tsx | 5 +++-- .../CloudPulse/Alerts/AlertsListing/AlertListTable.tsx | 3 ++- .../Alerts/CreateAlert/CreateAlertDefinition.tsx | 3 ++- .../Alerts/EditAlert/EditAlertDefinition.test.tsx | 3 ++- .../CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx | 3 ++- .../manager/src/features/CloudPulse/Alerts/constants.ts | 6 ++++++ 10 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 packages/manager/.changeset/pr-11903-upcoming-features-1742542218295.md diff --git a/packages/manager/.changeset/pr-11903-upcoming-features-1742542218295.md b/packages/manager/.changeset/pr-11903-upcoming-features-1742542218295.md new file mode 100644 index 00000000000..16718046ed6 --- /dev/null +++ b/packages/manager/.changeset/pr-11903-upcoming-features-1742542218295.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update success message for create/edit/enable/disable alert at `CreateAlertDefinition.tsx`, `EditAlertDefinition.tsx`, and `AlertListTable.tsx` ([#11903](https://github.com/linode/manager/pull/11903)) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts index 02392ba5670..90d9498dd7b 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/alerts-listing-page.spec.ts @@ -13,7 +13,10 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { accountFactory, alertFactory } from 'src/factories'; -import { alertStatuses } from 'src/features/CloudPulse/Alerts/constants'; +import { + UPDATE_ALERT_SUCCESS_MESSAGE, + alertStatuses, +} from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Alert } from '@linode/api-v4'; @@ -330,7 +333,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { 'Alert-1', 'Disable', '@getFirstAlertDefinitions', - 'Alert disabled' + UPDATE_ALERT_SUCCESS_MESSAGE ); // Enable "Alert-2" @@ -339,7 +342,7 @@ describe('Integration Tests for CloudPulse Alerts Listing Page', () => { 'Alert-2', 'Enable', '@getSecondAlertDefinitions', - 'Alert enabled' + UPDATE_ALERT_SUCCESS_MESSAGE ); }); }); diff --git a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts index 7a12abb99b1..6545e59d9c5 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/create-user-alert.spec.ts @@ -29,6 +29,7 @@ import { notificationChannelFactory, triggerConditionFactory, } from 'src/factories'; +import { CREATE_ALERT_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Flags } from 'src/featureFlags'; @@ -405,7 +406,7 @@ describe('Create Alert', () => { // Verify URL redirection and toast notification cy.url().should('endWith', '/alerts/definitions'); - ui.toast.assertMessage('Alert successfully created'); + ui.toast.assertMessage(CREATE_ALERT_SUCCESS_MESSAGE); // Confirm that Alert is listed on landing page with expected configuration. cy.findByText(label) diff --git a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts index e9aae01f0bc..bb695e4f2df 100644 --- a/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts +++ b/packages/manager/cypress/e2e/core/cloudpulse/edit-user-alert.spec.ts @@ -39,6 +39,7 @@ import { notificationChannelFactory, triggerConditionFactory, } from 'src/factories'; +import { UPDATE_ALERT_SUCCESS_MESSAGE } from 'src/features/CloudPulse/Alerts/constants'; import { formatDate } from 'src/utilities/formatDate'; import type { Database } from '@linode/api-v4'; @@ -362,7 +363,7 @@ describe('Integration Tests for Edit Alert', () => { // Verify URL redirection and toast notification cy.url().should('endWith', 'alerts/definitions'); - ui.toast.assertMessage('Alert successfully updated.'); + ui.toast.assertMessage(UPDATE_ALERT_SUCCESS_MESSAGE); // Confirm that Alert is listed on landing page with expected configuration. cy.findByText('Alert-2') diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx index 29338d9c14f..8ebd5c96310 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.test.tsx @@ -5,6 +5,7 @@ import { alertFactory } from 'src/factories'; import { formatDate } from 'src/utilities/formatDate'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { AlertsListTable } from './AlertListTable'; const queryMocks = vi.hoisted(() => ({ @@ -97,7 +98,7 @@ describe('Alert List Table test', () => { const actionMenu = getByLabelText(`Action menu for Alert ${alert.label}`); await userEvent.click(actionMenu); await userEvent.click(getByText('Enable')); // click the enable button to enable alert - expect(getByText('Alert enabled')).toBeInTheDocument(); // validate whether snackbar is displayed properly if alert is enabled successfully + expect(getByText(UPDATE_ALERT_SUCCESS_MESSAGE)).toBeInTheDocument(); // validate whether snackbar is displayed properly }); it('should show success snackbar when disabling alert succeeds', async () => { @@ -114,7 +115,7 @@ describe('Alert List Table test', () => { const actionMenu = getByLabelText(`Action menu for Alert ${alert.label}`); await userEvent.click(actionMenu); await userEvent.click(getByText('Disable')); // click the enable button to enable alert - expect(getByText('Alert disabled')).toBeInTheDocument(); // validate whether snackbar is displayed properly if alert is disabled successfully + expect(getByText(UPDATE_ALERT_SUCCESS_MESSAGE)).toBeInTheDocument(); // validate whether snackbar is displayed properly }); it('should show error snackbar when enabling alert fails', async () => { diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx index ccd5d2a516c..4523734aa4f 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsListing/AlertListTable.tsx @@ -14,6 +14,7 @@ import { TableSortCell } from 'src/components/TableSortCell'; import { useEditAlertDefinition } from 'src/queries/cloudpulse/alerts'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { AlertTableRow } from './AlertTableRow'; import { AlertListingTableLabelMap } from './constants'; @@ -71,7 +72,7 @@ export const AlertsListTable = React.memo((props: AlertsListTableProps) => { }) .then(() => { // Handle success - enqueueSnackbar(`Alert ${toggleStatus}`, { + enqueueSnackbar(UPDATE_ALERT_SUCCESS_MESSAGE, { variant: 'success', }); }) diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx index a36dee43e80..7ef28a337ed 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx @@ -12,6 +12,7 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { useFlags } from 'src/hooks/useFlags'; import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts'; +import { CREATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { enhanceValidationSchemaWithEntityIdValidation } from '../Utils/utils'; import { MetricCriteriaField } from './Criteria/MetricCriteria'; import { TriggerConditions } from './Criteria/TriggerConditions'; @@ -110,7 +111,7 @@ export const CreateAlertDefinition = () => { const onSubmit = handleSubmit(async (values) => { try { await createAlert(filterFormValues(values)); - enqueueSnackbar('Alert successfully created', { + enqueueSnackbar(CREATE_ALERT_SUCCESS_MESSAGE, { variant: 'success', }); alertCreateExit(); diff --git a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx index 52cf44506e9..79f3a696c7b 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.test.tsx @@ -7,6 +7,7 @@ import { Router } from 'react-router-dom'; import { alertFactory, notificationChannelFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; +import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { EditAlertDefinition } from './EditAlertDefinition'; const queryMocks = vi.hoisted(() => ({ @@ -104,7 +105,7 @@ describe('EditAlertDefinition component', () => { expect(push).toHaveBeenLastCalledWith('/alerts/definitions'); await waitFor(() => { expect( - getByText('Alert successfully updated.') // validate whether snackbar is displayed properly + getByText(UPDATE_ALERT_SUCCESS_MESSAGE) // validate whether snackbar is displayed properly ).toBeInTheDocument(); }); }, diff --git a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx index 36a83a36d26..8f1d991549f 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/EditAlert/EditAlertDefinition.tsx @@ -11,6 +11,7 @@ import { Breadcrumb } from 'src/components/Breadcrumb/Breadcrumb'; import { useFlags } from 'src/hooks/useFlags'; import { useEditAlertDefinition } from 'src/queries/cloudpulse/alerts'; +import { UPDATE_ALERT_SUCCESS_MESSAGE } from '../constants'; import { MetricCriteriaField } from '../CreateAlert/Criteria/MetricCriteria'; import { TriggerConditions } from '../CreateAlert/Criteria/TriggerConditions'; import { CloudPulseAlertSeveritySelect } from '../CreateAlert/GeneralInformation/AlertSeveritySelect'; @@ -73,7 +74,7 @@ export const EditAlertDefinition = (props: EditAlertProps) => { const onSubmit = handleSubmit(async (values) => { try { await editAlert({ alertId, serviceType, ...values }); - enqueueSnackbar('Alert successfully updated.', { + enqueueSnackbar(UPDATE_ALERT_SUCCESS_MESSAGE, { variant: 'success', }); history.push(definitionLanding); diff --git a/packages/manager/src/features/CloudPulse/Alerts/constants.ts b/packages/manager/src/features/CloudPulse/Alerts/constants.ts index 0bddacdb14f..44a45d965cd 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/constants.ts +++ b/packages/manager/src/features/CloudPulse/Alerts/constants.ts @@ -181,3 +181,9 @@ export const engineTypeMap: Record = { mysql: 'MySQL', postgresql: 'PostgreSQL', }; + +export const CREATE_ALERT_SUCCESS_MESSAGE = + 'Alert successfully created. It may take a few minutes for your changes to take effect.'; + +export const UPDATE_ALERT_SUCCESS_MESSAGE = + 'Alert successfully updated. It may take a few minutes for your changes to take effect.'; From ddc972e824947f0893789340c042031498602d67 Mon Sep 17 00:00:00 2001 From: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:02:35 +0530 Subject: [PATCH 34/84] refactor: [DI-24193] - Update query cache on api success (#11917) * refactor: [DI-24193] - Update query cache on api success * refactor: [DI-24193] - Remove redundant invalidate query * added changeset --- .../pr-11917-added-1743002198888.md | 5 ++++ .../manager/src/queries/cloudpulse/alerts.ts | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-11917-added-1743002198888.md diff --git a/packages/manager/.changeset/pr-11917-added-1743002198888.md b/packages/manager/.changeset/pr-11917-added-1743002198888.md new file mode 100644 index 00000000000..6a611bde7b2 --- /dev/null +++ b/packages/manager/.changeset/pr-11917-added-1743002198888.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Added +--- + +Add cache update logic on edit alert query ([#11917](https://github.com/linode/manager/pull/11917)) diff --git a/packages/manager/src/queries/cloudpulse/alerts.ts b/packages/manager/src/queries/cloudpulse/alerts.ts index 4a1d5dd38fc..9300f5409af 100644 --- a/packages/manager/src/queries/cloudpulse/alerts.ts +++ b/packages/manager/src/queries/cloudpulse/alerts.ts @@ -4,6 +4,7 @@ import { deleteEntityFromAlert, editAlertDefinition, } from '@linode/api-v4/lib/cloudpulse'; +import { queryPresets } from '@linode/queries'; import { keepPreviousData, useMutation, @@ -11,7 +12,6 @@ import { useQueryClient, } from '@tanstack/react-query'; -import { queryPresets } from '@linode/queries'; import { queryFactory } from './queries'; import type { @@ -78,8 +78,30 @@ export const useEditAlertDefinition = () => { return useMutation({ mutationFn: ({ alertId, serviceType, ...data }) => editAlertDefinition(data, serviceType, alertId), - onSuccess() { - queryClient.invalidateQueries(queryFactory.alerts); + + onSuccess(data) { + const allAlertsQueryKey = queryFactory.alerts._ctx.all().queryKey; + queryClient.cancelQueries({ queryKey: allAlertsQueryKey }); + queryClient.setQueryData(allAlertsQueryKey, (oldData) => { + return ( + oldData?.map((alert) => { + return alert.id === data.id ? data : alert; + }) ?? [data] + ); + }); + + queryClient.invalidateQueries({ + queryKey: queryFactory.alerts._ctx.alertByServiceTypeAndId( + data.service_type, + String(data.id) + ).queryKey, + }); + + queryClient.invalidateQueries({ + queryKey: queryFactory.alerts._ctx.alertsByServiceType( + data.service_type + ).queryKey, + }); }, }); }; From 422da1f8da3669864202702490e0792fc5aa662f Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:02:53 -0400 Subject: [PATCH 35/84] upcoming: [M3-9530] - Display interface type first in Linode Network IP Addresses table (#11865) * update ordering for IP table * update todo comment * update aria label/id * update logic to make sortable * Added changeset: Display interface type first in Linode Network IP Addresses table * fix cypress * address feedback pt1 * add preference key for IP table * maybe this? * or maybe this... * remove function map * add back in comment * argh * lots of cleanup :)) * change slaac for now * fix tests * update changeset --- .../pr-11865-changed-1742248570726.md | 5 ++ .../e2e/core/linodes/linode-network.spec.ts | 8 ++-- .../Linodes/LinodeEntityDetailBody.tsx | 4 +- .../LinodeNetworking/DeleteIPDialog.tsx | 2 +- .../LinodeNetworking/DeleteRangeDialog.tsx | 2 +- .../LinodeNetworking/ExplainerCopy.tsx | 2 +- .../LinodeNetworking/IPTransfer.tsx | 14 +++--- .../LinodeIPAddressRow.test.tsx | 4 +- .../LinodeNetworking/LinodeIPAddressRow.tsx | 6 +-- .../LinodeIPAddresses.test.ts | 22 ++++----- .../LinodeNetworking/LinodeIPAddresses.tsx | 46 ++++++++++--------- .../LinodeNetworkingActionMenu.test.tsx | 4 +- .../LinodeNetworkingActionMenu.tsx | 15 +++--- .../LinodesDetail/LinodeNetworking/types.ts | 22 ++++----- 14 files changed, 80 insertions(+), 76 deletions(-) create mode 100644 packages/manager/.changeset/pr-11865-changed-1742248570726.md diff --git a/packages/manager/.changeset/pr-11865-changed-1742248570726.md b/packages/manager/.changeset/pr-11865-changed-1742248570726.md new file mode 100644 index 00000000000..c2f599dbf7d --- /dev/null +++ b/packages/manager/.changeset/pr-11865-changed-1742248570726.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Display interface type first in Linode Network IP Addresses table ([#11865](https://github.com/linode/manager/pull/11865)) diff --git a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts index 3bd3cd70996..74a9894178b 100644 --- a/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/linode-network.spec.ts @@ -83,7 +83,7 @@ describe('IP Addresses', () => { * - Confirms the success toast message after editing RDNS */ it('checks for the toast message upon editing an RDNS', () => { - cy.findByLabelText('IPv4 Addresses') + cy.findByLabelText('Linode IP Addresses') .should('be.visible') .within(() => { // confirm table headers @@ -99,7 +99,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('IPv4 – Public').should('be.visible'); + cy.findByText('Public – IPv4').should('be.visible'); cy.findByText(mockRDNS).should('be.visible'); // open up the edit RDNS drawer @@ -136,7 +136,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('IPv4 – Public').should('be.visible'); + cy.findByText('Public – IPv4').should('be.visible'); ui.actionMenu .findByTitle(`Action menu for IP Address ${linodeIPv4}`) .should('be.visible'); @@ -147,7 +147,7 @@ describe('IP Addresses', () => { .should('be.visible') .closest('tr') .within(() => { - cy.findByText('IPv6 – Range').should('be.visible'); + cy.findByText('Range – IPv6').should('be.visible'); ui.actionMenu .findByTitle(`Action menu for IP Address ${_ipv6Range.range}`) .should('be.visible'); diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx index 224d232e4d6..afe23fb086d 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx @@ -34,7 +34,7 @@ import { StyledVPCBox, sxLastListItem, } from './LinodeEntityDetail.styles'; -import { ipv4TableID } from './LinodesDetail/LinodeNetworking/LinodeIPAddresses'; +import { ipTableId } from './LinodesDetail/LinodeNetworking/LinodeIPAddresses'; import { lishLink, sshLink } from './LinodesDetail/utilities'; import type { LinodeHandlers } from './LinodesLanding/LinodesLanding'; @@ -291,7 +291,7 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => { variant="body1" > View all IP Addresses diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteIPDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteIPDialog.tsx index df8a8edd576..fd35afa240f 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteIPDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteIPDialog.tsx @@ -1,9 +1,9 @@ +import { useLinodeIPDeleteMutation } from '@linode/queries'; import { ActionsPanel, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { useLinodeIPDeleteMutation } from '@linode/queries'; interface Props { address: string; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteRangeDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteRangeDialog.tsx index 51a7a0b0324..c90e50b1686 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteRangeDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/DeleteRangeDialog.tsx @@ -1,9 +1,9 @@ +import { useLinodeRemoveRangeMutation } from '@linode/queries'; import { ActionsPanel, Typography } from '@linode/ui'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { useLinodeRemoveRangeMutation } from '@linode/queries'; import type { IPRange } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ExplainerCopy.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ExplainerCopy.tsx index 4cb88752185..cea8c567faf 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ExplainerCopy.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/ExplainerCopy.tsx @@ -1,7 +1,7 @@ +import { useLinodeQuery } from '@linode/queries'; import * as React from 'react'; import { SupportLink } from 'src/components/SupportLink'; -import { useLinodeQuery } from '@linode/queries'; import type { IPType } from './AddIPDrawer'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx index 55c663d438c..3c2af86a574 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx @@ -1,3 +1,10 @@ +import { + useAllIPv6RangesQuery, + useAllLinodesQuery, + useAssignAdressesMutation, + useLinodeIPsQuery, + useLinodeQuery, +} from '@linode/queries'; import { ActionsPanel, Autocomplete, @@ -12,13 +19,6 @@ import Grid from '@mui/material/Grid2'; import { styled, useTheme } from '@mui/material/styles'; import * as React from 'react'; -import { - useAllLinodesQuery, - useLinodeQuery, - useAssignAdressesMutation, - useLinodeIPsQuery, - useAllIPv6RangesQuery, -} from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import type { APIError, IPRange } from '@linode/api-v4'; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx index 2f63014b5f0..5268da3b209 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.test.tsx @@ -135,10 +135,10 @@ describe('ipResponseToDisplayRows', () => { ); expect( - ipDisplays.find((ipDisplay) => ipDisplay.type === 'IPv4 – Public') + ipDisplays.find((ipDisplay) => ipDisplay.type === 'Public – IPv4') ).toBeUndefined(); expect( - ipDisplays.find((ipDisplay) => ipDisplay.type === 'VPC IPv4 – NAT') + ipDisplays.find((ipDisplay) => ipDisplay.type === 'VPC NAT – IPv4') ).toBeDefined(); }); }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx index 94181fd3e97..500656f0eba 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddressRow.tsx @@ -1,8 +1,8 @@ import { - usePreferences, + useAllIPsQuery, useLinodeIPsQuery, useLinodeQuery, - useAllIPsQuery, + usePreferences, } from '@linode/queries'; import { CircleProgress, Typography } from '@linode/ui'; import { styled } from '@mui/material/styles'; @@ -59,7 +59,7 @@ export const LinodeIPAddressRow = (props: LinodeIPAddressRowProps) => { ); const isOnlyPublicIP = - ips?.ipv4.public.length === 1 && type === 'IPv4 – Public'; + ips?.ipv4.public.length === 1 && type === 'Public – IPv4'; return ( { const ipv4List = ipAddressFactory.buildList(4); @@ -86,21 +86,19 @@ describe('createType utility function', () => { const publicIPv4 = ipAddressFactory.build({ public: true, type: 'ipv4' }); const privateIPv4 = ipAddressFactory.build({ public: false, type: 'ipv4' }); - expect(createType(publicIPv4, 'Public')).toBe('IPv4 – Public'); - expect(createType(privateIPv4, 'Private')).toBe('IPv4 – Private'); + expect(createType(publicIPv4, 'Public')).toBe('Public – IPv4'); + expect(createType(privateIPv4, 'Private')).toBe('Private – IPv4'); - expect(createType(publicIPv4, 'Reserved')).toBe('IPv4 – Reserved (public)'); - expect(createType(privateIPv4, 'Reserved')).toBe( - 'IPv4 – Reserved (private)' - ); + expect(createType(publicIPv4, 'Reserved')).toBe('Reserved IPv4 (public)'); + expect(createType(privateIPv4, 'Reserved')).toBe('Reserved IPv4 (private)'); - expect(createType(publicIPv4, 'Shared')).toBe('IPv4 – Shared'); + expect(createType(publicIPv4, 'Shared')).toBe('Shared – IPv4'); }); it('creates the correct type for ipv6', () => { const ipv6 = ipAddressFactory.build({ type: 'ipv6' }); - expect(createType(ipv6, 'SLAAC')).toBe('IPv6 – SLAAC'); - expect(createType(ipv6, 'Link Local')).toBe('IPv6 – Link Local'); + expect(createType(ipv6, 'SLAAC')).toBe('Public – IPv6 – SLAAC'); + expect(createType(ipv6, 'Link Local')).toBe('Link Local – IPv6'); }); }); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx index 54c64b84a41..b84b034c024 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeIPAddresses.tsx @@ -1,3 +1,8 @@ +import { + useLinodeIPsQuery, + useLinodeQuery, + useRegionsQuery, +} from '@linode/queries'; import { Box, Button, @@ -21,11 +26,6 @@ import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useVPCConfigInterface } from 'src/hooks/useVPCConfigInterface'; -import { - useLinodeQuery, - useLinodeIPsQuery, - useRegionsQuery, -} from '@linode/queries'; import { AddIPDrawer } from './AddIPDrawer'; import { DeleteIPDialog } from './DeleteIPDialog'; @@ -48,7 +48,7 @@ import type { LinodeIPsResponse, } from '@linode/api-v4'; -export const ipv4TableID = 'ips'; +export const ipTableId = 'ips'; interface LinodeIPAddressesProps { linodeID: number; @@ -207,10 +207,15 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { )} {/* @todo: It'd be nice if we could always sort by public -> private. */} - + {({ data: orderedData, handleOrderChange, order, orderBy }) => { return ( - +
    Address @@ -235,7 +240,7 @@ export const LinodeIPAddresses = (props: LinodeIPAddressesProps) => { {...ipDisplay} {...handlers} isVPCOnlyLinode={ - isVPCOnlyLinode && ipDisplay.type === 'IPv4 – Public' + isVPCOnlyLinode && ipDisplay.type === 'Public – IPv4' } key={`${ipDisplay.address}-${ipDisplay.type}`} linodeId={linodeID} @@ -339,7 +344,7 @@ export const vpcConfigInterfaceToDisplayRows = ( if (ipv4?.vpc) { ipDisplay.push({ address: ipv4.vpc, - type: 'IPv4 – VPC', + type: 'VPC – IPv4', ...emptyProps, }); } @@ -347,7 +352,7 @@ export const vpcConfigInterfaceToDisplayRows = ( if (ipv4?.nat_1_1) { ipDisplay.push({ address: ipv4.nat_1_1, - type: 'VPC IPv4 – NAT', + type: 'VPC NAT – IPv4', ...emptyProps, }); } @@ -356,7 +361,7 @@ export const vpcConfigInterfaceToDisplayRows = ( ip_ranges.forEach((ip_range) => { ipDisplay.push({ address: ip_range, - type: 'IPv4 – VPC – Range', + type: 'VPC – Range – IPv4', ...emptyProps, }); }); @@ -423,7 +428,7 @@ export const ipResponseToDisplayRows = ( gateway: '', rdns: '', subnetMask: '', - type: 'IPv6 – Range' as IPDisplay['type'], + type: 'Range – IPv6' as IPDisplay['type'], }; }) ); @@ -455,16 +460,13 @@ const ipToDisplay = (ip: IPAddress, key: ipKey): IPDisplay => { }; export const createType = (ip: IPAddress, key: ipKey) => { - let type = ''; - type += ip.type === 'ipv4' ? 'IPv4' : 'IPv6'; - - type += ' – '; + if (key === 'Reserved' && ip.type === 'ipv4') { + return ip.public ? 'Reserved IPv4 (public)' : 'Reserved IPv4 (private)'; + } - if (key === 'Reserved') { - type += ip.public ? 'Reserved (public)' : 'Reserved (private)'; - } else { - type += key; + if (key === 'SLAAC') { + return 'Public – IPv6 – SLAAC'; } - return type; + return `${key} – ${ip.type === 'ipv4' ? 'IPv4' : 'IPv6'}`; }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.test.tsx index bcb1d6373dc..08acd5b4331 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.test.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.test.tsx @@ -38,7 +38,7 @@ describe('LinodeNetworkingActionMenu', () => { ); @@ -54,7 +54,7 @@ describe('LinodeNetworkingActionMenu', () => { ); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx index efcf57476af..e2b2748ad26 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeNetworkingActionMenu.tsx @@ -25,7 +25,6 @@ interface Props { export const LinodeNetworkingActionMenu = (props: Props) => { const theme = useTheme(); const matchesMdDown = useMediaQuery(theme.breakpoints.down('lg')); - const { ipAddress, ipType, @@ -37,15 +36,15 @@ export const LinodeNetworkingActionMenu = (props: Props) => { } = props; const showEdit = ![ - 'IPv4 – Private', - 'IPv4 – Reserved (private)', - 'IPv4 – Reserved (public)', - 'IPv4 – VPC', - 'IPv6 – Link Local', - 'VPC IPv4 – NAT', + 'Link Local – IPv6', + 'Private – IPv4', + 'Reserved IPv4 (private)', + 'Reserved IPv4 (public)', + 'VPC NAT – IPv4', + 'VPC – IPv4', ].includes(ipType); - const deletableIPTypes = ['IPv4 – Public', 'IPv4 – Private', 'IPv6 – Range']; + const deletableIPTypes = ['Private – IPv4', 'Public – IPv4', 'Range – IPv6']; // if we have a 116 we don't want to give the option to remove it const is116Range = ipAddress?.prefix === 116; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/types.ts b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/types.ts index f12c35c7c2a..b51c09fe137 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/types.ts +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/types.ts @@ -1,12 +1,12 @@ export type IPTypes = - | 'IPv4 – Private' - | 'IPv4 – Public' - | 'IPv4 – Reserved (private)' - | 'IPv4 – Reserved (public)' - | 'IPv4 – Shared' - | 'IPv4 – VPC – Range' - | 'IPv4 – VPC' - | 'IPv6 – Link Local' - | 'IPv6 – Range' - | 'IPv6 – SLAAC' - | 'VPC IPv4 – NAT'; + | 'Link Local – IPv6' + | 'Private – IPv4' + | 'Public – IPv4' + | 'Public – IPv6 – SLAAC' + | 'Range – IPv6' + | 'Reserved IPv4 (private)' + | 'Reserved IPv4 (public)' + | 'Shared – IPv4' + | 'VPC NAT – IPv4' + | 'VPC – IPv4' + | 'VPC – Range – IPv4'; From 6672d85d4fc071f0f55bc4d13f2aca0b0923c785 Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:07:58 -0400 Subject: [PATCH 36/84] upcoming: [M3-9118] - Add the Interface Details drawer (#11888) * begin showing details * add default chips * omg * spacing eslint * show IPs * janky navigation * mask sensitive text * add unit test * address feedback pt 1 * feedback pt 2 * feedback pt 2.5 * feedback pt 2.5.1 * update firewall link * fix tests --- ...r-11888-upcoming-features-1742589295521.md | 5 + .../Devices/FirewallDeviceRow.tsx | 12 +-- .../FirewallLanding/PublicTemplateRules.tsx | 44 ++++----- .../FirewallLanding/VPCTemplateRules.tsx | 73 +++++++------- .../DialogContents/SuccessDialogContent.tsx | 97 ++++++++----------- .../InterfaceDetailsContent.test.tsx | 61 ++++++++++++ .../InterfaceDetailsContent.tsx | 69 +++++++++++++ .../InterfaceDetailsDrawer.tsx | 47 +++++++++ .../PublicInterfaceDetailsContent.tsx | 72 ++++++++++++++ .../VPCInterfaceDetailsContent.tsx | 80 +++++++++++++++ .../VlanInterfaceDetailsContent.tsx | 29 ++++++ .../LinodeInterfaceActionMenu.tsx | 3 +- .../LinodeInterfaces/LinodeInterfaces.tsx | 31 +++++- .../LinodesDetail/LinodesDetailNavigation.tsx | 7 +- 14 files changed, 498 insertions(+), 132 deletions(-) create mode 100644 packages/manager/.changeset/pr-11888-upcoming-features-1742589295521.md create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.test.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsDrawer.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/PublicInterfaceDetailsContent.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx create mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VlanInterfaceDetailsContent.tsx diff --git a/packages/manager/.changeset/pr-11888-upcoming-features-1742589295521.md b/packages/manager/.changeset/pr-11888-upcoming-features-1742589295521.md new file mode 100644 index 00000000000..b059ec02b08 --- /dev/null +++ b/packages/manager/.changeset/pr-11888-upcoming-features-1742589295521.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Interface Details drawer for Linode Interfaces ([#11888](https://github.com/linode/manager/pull/11888)) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx index 08250082cac..b0b4014a76d 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceRow.tsx @@ -25,6 +25,10 @@ export const FirewallDeviceRow = React.memo((props: FirewallDeviceRowProps) => { const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + const link = isInterfaceDevice + ? `/linodes/${entityId}/networking/interfaces/${id}` + : `/${type}s/${id}/${type === 'linode' ? 'networking' : 'summary'}`; + return ( @@ -33,13 +37,7 @@ export const FirewallDeviceRow = React.memo((props: FirewallDeviceRowProps) => { {isInterfaceDevice && !label ? ( ) : ( - // @TODO Linode Interfaces - perhaps link to the interface's details later - + {label} )} diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/PublicTemplateRules.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/PublicTemplateRules.tsx index e6972d2207e..dc65cf6a7a3 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/PublicTemplateRules.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/PublicTemplateRules.tsx @@ -1,4 +1,4 @@ -import { Box, List, ListItem, Typography } from '@linode/ui'; +import { Box, List, ListItem, Stack, Typography } from '@linode/ui'; import * as React from 'react'; import { STRENGTHEN_TEMPLATE_RULES } from './constants'; @@ -8,22 +8,21 @@ import type { Theme } from '@mui/material'; export const PublicTemplateRules = () => { return ( <> - ({ marginTop: theme.spacing(3) })}> - Allows for login with SSH, and regular networking control data. - - ({ marginTop: theme.spacing(2) })}> - {STRENGTHEN_TEMPLATE_RULES} - - ({ - backgroundColor: theme.tokens.alias.Background.Neutral, - marginTop: theme.spacing(2), - padding: theme.spacing(2), - })} - data-testid="public-template-info" - > - {sharedTemplateRules} - + + + Allows for login with SSH, and regular networking control data. + + {STRENGTHEN_TEMPLATE_RULES} + ({ + backgroundColor: theme.tokens.alias.Background.Neutral, + padding: theme.spacingFunction(16), + })} + data-testid="public-template-info" + > + {sharedTemplateRules} + + {sharedTemplatePolicies} ); @@ -31,14 +30,13 @@ export const PublicTemplateRules = () => { const templateRuleStyling = (theme: Theme) => ({ backgroundColor: theme.tokens.alias.Background.Neutral, - marginTop: theme.spacing(1), - padding: `${theme.spacing(1)} ${theme.spacing(2)}`, + padding: `${theme.spacingFunction(8)} ${theme.spacingFunction(16)}`, }); export const sharedTemplateRules = ( <> Rules - ({ marginTop: theme.spacing(1) })}> + ({ marginTop: theme.spacingFunction(8) })}> Allow Inbound SSH @@ -52,7 +50,7 @@ export const sharedTemplateRules = ( Sources: All IPv4, IPv6 - ({ marginTop: theme.spacing(2) })}> + ({ marginTop: theme.spacingFunction(16) })}> Allow Inbound ICMP @@ -67,7 +65,7 @@ export const sharedTemplateRules = ( ); export const sharedTemplatePolicies = ( - <> + ({ ...templateRuleStyling(theme), @@ -84,5 +82,5 @@ export const sharedTemplatePolicies = ( Default Outbound Policy: ACCEPT - + ); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/VPCTemplateRules.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/VPCTemplateRules.tsx index f2d7bead5cf..e9ebc709890 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/VPCTemplateRules.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/VPCTemplateRules.tsx @@ -1,4 +1,4 @@ -import { Box, List, ListItem, Typography } from '@linode/ui'; +import { Box, List, ListItem, Stack, Typography } from '@linode/ui'; import * as React from 'react'; import { TextTooltip } from 'src/components/TextTooltip'; @@ -12,42 +12,43 @@ import { export const VPCTemplateRules = () => { return ( <> - ({ marginTop: theme.spacing(3) })}> - Allows for login with SSH, regular networking control data, and inbound - traffic from the VPC address space. - - ({ marginTop: theme.spacing(2) })}> - {STRENGTHEN_TEMPLATE_RULES} - - ({ - backgroundColor: theme.tokens.alias.Background.Neutral, - marginTop: theme.spacing(2), - padding: theme.spacing(2), - })} - data-testid="vpc-template-info" - > - {sharedTemplateRules} - ({ marginTop: theme.spacing(2) })}> - Allow traffic for{' '} - {' '} - ranges + + + Allows for login with SSH, regular networking control data, and + inbound traffic from the VPC address space. - - - Protocol: TCP, UDP - - - Ports: All Ports - - - Sources: 10.0.0.0/8, 192.168.0.0/17, 172.16.0.0/12 - - - + {STRENGTHEN_TEMPLATE_RULES} + ({ + backgroundColor: theme.tokens.alias.Background.Neutral, + padding: theme.spacingFunction(16), + })} + data-testid="vpc-template-info" + > + {sharedTemplateRules} + ({ marginTop: theme.spacingFunction(16) })} + > + Allow traffic for{' '} + {' '} + ranges + + + + Protocol: TCP, UDP + + + Ports: All Ports + + + Sources: 10.0.0.0/8, 192.168.0.0/17, 172.16.0.0/12 + + + + {sharedTemplatePolicies} ); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/SuccessDialogContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/SuccessDialogContent.tsx index 2e1360047ff..cfd683d782a 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/SuccessDialogContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/UpgradeInterfaces/DialogContents/SuccessDialogContent.tsx @@ -35,18 +35,11 @@ export const SuccessDialogContent = ( ({ backgroundColor: theme.tokens.alias.Background.Neutral, - marginTop: theme.spacing(1), - padding: theme.spacing(2), + marginTop: theme.spacingFunction(8), + padding: theme.spacingFunction(16), })} > - ({ - marginTop: theme.spacing(1), - })} - variant="h3" - > - Upgrade Summary - + Upgrade Summary {linodeInterfaces.map((linodeInterface) => ( { } = props; return ( - <> - ({ - marginBottom: theme.spacing(2), - marginTop: theme.spacing(2), - })} - > - Interface Meta Info: Interface #{id} - - ID: {id} - MAC Address: {mac_address} - Created: {created} - Updated: {updated} - Version: {version} - {publicInterface && ( + + ({ - marginBottom: theme.spacing(2), - marginTop: theme.spacing(2), + marginBottom: theme.spacingFunction(16), })} > - Public Interface successfully upgraded + Interface Meta Info: Interface #{id} + ID: {id} + MAC Address: {mac_address} + Created: {created} + Updated: {updated} + Version: {version} + + {publicInterface && ( + Public Interface successfully upgraded )} {vpc && } {vlan && } - + ); }; @@ -127,31 +114,28 @@ const VPCInterfaceInfo = (props: VPCInterfaceInfo) => { return ( <> - ({ - marginBottom: theme.spacing(2), - marginTop: theme.spacing(2), - })} - > + VPC Interface Details - {default_route && ( + + {default_route && ( + + Default Route:{' '} + {default_route.ipv4 ? 'IPv4' : default_route.ipv6 ? 'IPv6' : 'None'} + + )} + VPC ID: {vpc_id} + Subnet ID: {subnet_id} - Default Route:{' '} - {default_route.ipv4 ? 'IPv4' : default_route.ipv6 ? 'IPv6' : 'None'} + Addresses: {addresses.map((address) => address.address).join(', ')} - )} - VPC ID: {vpc_id} - Subnet ID: {subnet_id} - - Addresses: {addresses.map((address) => address.address).join(', ')} - - {primaryAddress && ( - Primary Address: {primaryAddress.address} - )} - {ranges.length > 0 && ( - Routed Ranges: {ranges.join(', ')} - )} + {primaryAddress && ( + Primary Address: {primaryAddress.address} + )} + {ranges.length > 0 && ( + Routed Ranges: {ranges.join(', ')} + )} + ); }; @@ -163,16 +147,13 @@ const VlanInterfaceInfo = (props: Pick) => { return ( <> - ({ - marginBottom: theme.spacing(2), - marginTop: theme.spacing(2), - })} - > + VLAN Interface Details - Label: {vlan_label} - IPAM Address: {ipam_address} + + Label: {vlan_label} + IPAM Address: {ipam_address} + ); }; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.test.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.test.tsx new file mode 100644 index 00000000000..2e9f9faefde --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.test.tsx @@ -0,0 +1,61 @@ +import { + linodeInterfaceFactoryPublic, + linodeInterfaceFactoryVPC, + linodeInterfaceFactoryVlan, +} from '@linode/utilities'; +import React from 'react'; + +import { renderWithTheme } from 'src/utilities/testHelpers'; + +import { InterfaceDetailsContent } from './InterfaceDetailsContent'; + +describe('InterfaceDetailsContent', () => { + it('shows the information for a Public Interface', () => { + const publicInterface = linodeInterfaceFactoryPublic.build(); + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Type')).toBeVisible(); + expect(getByText('Public')).toBeVisible(); + expect(getByText('ID')).toBeVisible(); + expect(getByText('MAC Address')).toBeVisible(); + expect(getByText('IPv4 Addresses')).toBeVisible(); + expect(getByText('IPv6 Addresses')).toBeVisible(); + expect(getByText('Created')).toBeVisible(); + expect(getByText('Modified')).toBeVisible(); + }); + + it('shows the information for a VPC Interface', () => { + const vpcInterface = linodeInterfaceFactoryVPC.build(); + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Type')).toBeVisible(); + expect(getByText('VPC')).toBeVisible(); + expect(getByText('ID')).toBeVisible(); + expect(getByText('MAC Address')).toBeVisible(); + expect(getByText('VPC Label')).toBeVisible(); + expect(getByText('Subnet Label')).toBeVisible(); + expect(getByText('IPv4 Addresses')).toBeVisible(); + expect(getByText('Created')).toBeVisible(); + expect(getByText('Modified')).toBeVisible(); + }); + + it('shows the information for a VLAN Interface', () => { + const vlanInterface = linodeInterfaceFactoryVlan.build(); + const { getByText } = renderWithTheme( + + ); + + expect(getByText('Type')).toBeVisible(); + expect(getByText('VLAN')).toBeVisible(); + expect(getByText('ID')).toBeVisible(); + expect(getByText('MAC Address')).toBeVisible(); + expect(getByText('VLAN Label')).toBeVisible(); + expect(getByText('IPAM Address')).toBeVisible(); + expect(getByText('Created')).toBeVisible(); + expect(getByText('Modified')).toBeVisible(); + }); +}); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.tsx new file mode 100644 index 00000000000..3e6453f1989 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsContent.tsx @@ -0,0 +1,69 @@ +import { Box, Chip, Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { DateTimeDisplay } from 'src/components/DateTimeDisplay'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; + +import { getLinodeInterfaceType } from '../utilities'; +import { PublicInterfaceDetailsContent } from './PublicInterfaceDetailsContent'; +import { VlanInterfaceDetailsContent } from './VlanInterfaceDetailsContent'; +import { VPCInterfaceDetailsContent } from './VPCInterfaceDetailsContent'; + +import type { LinodeInterface } from '@linode/api-v4'; + +export const InterfaceDetailsContent = (props: LinodeInterface) => { + const { created, default_route, id, mac_address, updated } = props; + const type = getLinodeInterfaceType(props); + + return ( + + {(default_route.ipv4 || default_route.ipv6) && ( + + {default_route.ipv4 && ( + + )} + {default_route.ipv6 && ( + + )} + + )} + + + Type + + {type} + + + + ID + + {id} + + + + MAC Address + + + + {props.public && } + {props.vpc && } + {props.vlan && } + + + Created + + + + + + + + Modified + + + + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsDrawer.tsx new file mode 100644 index 00000000000..bb8d0d9c0f5 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/InterfaceDetailsDrawer.tsx @@ -0,0 +1,47 @@ +import { useLinodeInterfaceQuery } from '@linode/queries'; +import { Box, Button, Drawer } from '@linode/ui'; +import React from 'react'; +import { useLocation } from 'react-router-dom'; + +import { NotFound } from 'src/components/NotFound'; + +import { InterfaceDetailsContent } from './InterfaceDetailsContent'; + +interface Props { + interfaceId: number | undefined; + linodeId: number; + onClose: () => void; + open: boolean; +} + +export const InterfaceDetailsDrawer = (props: Props) => { + const location = useLocation(); + const interfaceIdFromLocation = +location.pathname.split('/').slice(-1); + + const { interfaceId: id, linodeId, onClose, open } = props; + const interfaceId = id ?? interfaceIdFromLocation; + + const { data: linodeInterface, error, isLoading } = useLinodeInterfaceQuery( + linodeId, + interfaceId, + open + ); + + return ( + + {linodeInterface && } + + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/PublicInterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/PublicInterfaceDetailsContent.tsx new file mode 100644 index 00000000000..e5a0a9c9f61 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/PublicInterfaceDetailsContent.tsx @@ -0,0 +1,72 @@ +import { Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { MaskableText } from 'src/components/MaskableText/MaskableText'; + +import type { PublicInterfaceData } from '@linode/api-v4'; + +export const PublicInterfaceDetailsContent = (props: PublicInterfaceData) => { + const { ipv4, ipv6 } = props; + + const ipv4ToTypography = ( + <> + {ipv4.addresses.map((address) => ( + + ))} + {ipv4.shared.map((shared) => ( + + ))} + + ); + + const ipv6ToTypography = ( + <> + {ipv6.slaac.map((slaac) => ( + + ))} + {ipv6.shared.map((shared) => ( + + ))} + {ipv6.ranges.map((range) => ( + + ))} + + ); + + return ( + <> + + + IPv4 Addresses + + {ipv4ToTypography} + + + + IPv6 Addresses + + {ipv6ToTypography} + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx new file mode 100644 index 00000000000..e92fb1b5099 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx @@ -0,0 +1,80 @@ +import { useVPCQuery } from '@linode/queries'; +import { CircleProgress, Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { Link } from 'src/components/Link'; +import { MaskableText } from 'src/components/MaskableText/MaskableText'; + +import type { VPCInterfaceData } from '@linode/api-v4'; + +export const VPCInterfaceDetailsContent = (props: VPCInterfaceData) => { + const { ipv4, subnet_id, vpc_id } = props; + const { data: vpc } = useVPCQuery(vpc_id, Boolean(vpc_id)); + + const subnet = vpc?.subnets.find((subnet) => subnet.id === subnet_id); + + const ipv4ToTypography = ( + <> + {ipv4.addresses.map((address) => + address.nat_1_1_address ? ( + <> + + + + ) : ( + + ) + )} + {ipv4.ranges.map((range) => ( + + ))} + + ); + + return ( + <> + + + VPC Label + + {vpc ? ( + {vpc.label} + ) : ( + + )} + + + + Subnet Label + + {subnet ? ( + {subnet.label} + ) : ( + + )} + + + + IPv4 Addresses + + {ipv4ToTypography} + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VlanInterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VlanInterfaceDetailsContent.tsx new file mode 100644 index 00000000000..e7561e27138 --- /dev/null +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VlanInterfaceDetailsContent.tsx @@ -0,0 +1,29 @@ +import { Stack, Typography } from '@linode/ui'; +import React from 'react'; + +import { MaskableText } from 'src/components/MaskableText/MaskableText'; + +export const VlanInterfaceDetailsContent = (props: { + ipam_address: string; + vlan_label: string; +}) => { + const { ipam_address, vlan_label } = props; + return ( + <> + + + VLAN Label + + {vlan_label} + + + + IPAM Address + + + + + + + ); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceActionMenu.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceActionMenu.tsx index e8a51b8897b..a4fcbb705d6 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceActionMenu.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaceActionMenu.tsx @@ -12,13 +12,14 @@ interface Props { export interface InterfaceActionHandlers { onDelete: (interfaceId: number) => void; + onShowDetails: (interfaceId: number) => void; } export const LinodeInterfaceActionMenu = (props: Props) => { const { handlers, id, type } = props; const actions = [ - { onClick: () => alert(`Details ${id}`), title: 'Details' }, + { onClick: () => handlers.onShowDetails(id), title: 'Details' }, { onClick: () => alert(`Edit ${id}`), title: 'Edit' }, { onClick: () => handlers.onDelete(id), title: 'Delete' }, ]; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx index c940b9c0304..3fcff6f703b 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/LinodeInterfaces.tsx @@ -1,8 +1,10 @@ import { Box, Button, Paper, Typography } from '@linode/ui'; import React, { useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { AddInterfaceDrawer } from './AddInterfaceDrawer/AddInterfaceDrawer'; import { DeleteInterfaceDialog } from './DeleteInterfaceDialog'; +import { InterfaceDetailsDrawer } from './InterfaceDetailsDrawer/InterfaceDetailsDrawer'; import { LinodeInterfacesTable } from './LinodeInterfacesTable'; interface Props { @@ -11,13 +13,21 @@ interface Props { } export const LinodeInterfaces = ({ linodeId, regionId }: Props) => { + const location = useLocation(); + const history = useHistory(); + const [isAddDrawerOpen, setIsAddDrawerOpen] = useState(false); - const [isDeleteDrawerOpen, setIsDeleteDrawerOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedInterfaceId, setSelectedInterfaceId] = useState(); const onDelete = (interfaceId: number) => { setSelectedInterfaceId(interfaceId); - setIsDeleteDrawerOpen(true); + setIsDeleteDialogOpen(true); + }; + + const onShowDetails = (interfaceId: number) => { + setSelectedInterfaceId(interfaceId); + history.replace(`${location.pathname}/interfaces/${interfaceId}`); }; return ( @@ -37,7 +47,10 @@ export const LinodeInterfaces = ({ linodeId, regionId }: Props) => { Add Network Interface - + setIsAddDrawerOpen(false)} @@ -47,8 +60,16 @@ export const LinodeInterfaces = ({ linodeId, regionId }: Props) => { setIsDeleteDrawerOpen(false)} - open={isDeleteDrawerOpen} + onClose={() => setIsDeleteDialogOpen(false)} + open={isDeleteDialogOpen} + /> + { + history.replace(`/linodes/${linodeId}/networking`); + }} + interfaceId={selectedInterfaceId} + linodeId={linodeId} + open={location.pathname.includes('networking/interfaces')} /> ); diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx index 7a9f7f2990f..7f96ee9c2a5 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodesDetailNavigation.tsx @@ -1,3 +1,4 @@ +import { useLinodeQuery } from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; @@ -16,7 +17,6 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { SMTPRestrictionText } from 'src/features/Linodes/SMTPRestrictionText'; -import { useLinodeQuery } from '@linode/queries'; import { useTypeQuery } from 'src/queries/types'; const LinodeSummary = React.lazy(() => import('./LinodeSummary/LinodeSummary')); @@ -87,7 +87,10 @@ const LinodesDetailNavigation = () => { ].filter((thisTab) => !thisTab.hidden); const matches = (p: string) => { - return Boolean(matchPath(p, { path: location.pathname })); + return ( + Boolean(matchPath(p, { path: location.pathname })) || + location.pathname.includes(p) + ); }; const getIndex = () => { From f9b567b54781785ec03d4dbede8c2d2d8a0a75f7 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:50:41 -0700 Subject: [PATCH 37/84] upcoming: [M3-9588] - Add VPC & Firewall section to LKE-E create flow (#11901) * Add VPC/Firewall section for LKE-E * Update test coverage * Added changeset: Add VPC & Firewall section to LKE-E create flow * Make margins of dividers between sections more consistent * Address feedback: rename component, capitalization --- .../pr-11901-upcoming-features-1742509729379.md | 5 +++++ .../e2e/core/kubernetes/lke-create.spec.ts | 7 +++++++ .../CreateCluster/ClusterNetworkingPanel.tsx | 14 ++++++++++++++ .../Kubernetes/CreateCluster/CreateCluster.tsx | 15 ++++++++++++--- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-11901-upcoming-features-1742509729379.md create mode 100644 packages/manager/src/features/Kubernetes/CreateCluster/ClusterNetworkingPanel.tsx diff --git a/packages/manager/.changeset/pr-11901-upcoming-features-1742509729379.md b/packages/manager/.changeset/pr-11901-upcoming-features-1742509729379.md new file mode 100644 index 00000000000..e07bbc8bb16 --- /dev/null +++ b/packages/manager/.changeset/pr-11901-upcoming-features-1742509729379.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add VPC & Firewall section to LKE-E create flow ([#11901](https://github.com/linode/manager/pull/11901)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index d9efd24c5df..ebe1e5a933a 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -1308,6 +1308,7 @@ describe('LKE Cluster Creation with LKE-E', () => { * - Confirms an LKE-E supported region can be selected * - Confirms an LKE-E supported k8 version can be selected * - Confirms the APL section is disabled while it remains unsupported + * - Confirms the VPC & Firewall placeholder section displays with correct copy * - Confirms ACL is enabled by default * - Confirms the checkout bar displays the correct LKE-E info * - Confirms an enterprise cluster can be created with the correct chip, version, and price @@ -1467,6 +1468,12 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.findByRole('radio').should('be.disabled').should('be.checked'); }); + // Confirm the VPC/Firewall section displays. + cy.findByText('VPC & Firewall').should('be.visible'); + cy.findByText( + 'A VPC and Firewall are automatically generated for LKE Enterprise customers.' + ).should('be.visible'); + // Confirm the expected available plans display. validEnterprisePlanTabs.forEach((tab) => { ui.tabList.findTabByTitle(tab).should('be.visible'); diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/ClusterNetworkingPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterNetworkingPanel.tsx new file mode 100644 index 00000000000..232b5e30cba --- /dev/null +++ b/packages/manager/src/features/Kubernetes/CreateCluster/ClusterNetworkingPanel.tsx @@ -0,0 +1,14 @@ +import { Stack, Typography } from '@linode/ui'; +import React from 'react'; + +export const ClusterNetworkingPanel = () => { + return ( + + VPC & Firewall + + A VPC and Firewall are automatically generated for LKE Enterprise + customers. + + + ); +}; diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx index 5a977a5e4d3..ecdd9f8589b 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/CreateCluster.tsx @@ -58,6 +58,7 @@ import { reportAgreementSigningError } from 'src/utilities/reportAgreementSignin import { CLUSTER_VERSIONS_DOCS_LINK } from '../constants'; import KubeCheckoutBar from '../KubeCheckoutBar'; import { ApplicationPlatform } from './ApplicationPlatform'; +import { ClusterNetworkingPanel } from './ClusterNetworkingPanel'; import { ClusterTierPanel } from './ClusterTierPanel'; import { ControlPlaneACLPane } from './ControlPlaneACLPane'; import { @@ -401,7 +402,7 @@ export const CreateCluster = () => { /> {isLkeEnterpriseLAFlagEnabled && ( <> - + { )} - + {showHighAvailability && selectedTier !== 'enterprise' && ( { /> )} + {selectedTier === 'enterprise' && } {showControlPlaneACL && ( <> - {selectedTier !== 'enterprise' && } + { setIPv4Addr(newIpV4Addr); From a4f2bc36fd13247ce6835aca0dffee8f2fdd366f Mon Sep 17 00:00:00 2001 From: Dajahi Wiley <114682940+dwiley-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:23:15 -0400 Subject: [PATCH 38/84] =?UTF-8?q?fix:=20[M3-9669]=20=E2=80=93=20Adjust=20c?= =?UTF-8?q?onditions=20for=20displaying=20encryption=20status=20on=20Linod?= =?UTF-8?q?e=20Details=20page=20&=20encryption=20copy=20on=20LKE=20Create?= =?UTF-8?q?=20page=20(#11930)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...r-11930-upcoming-features-1743025907787.md | 5 ++ .../CreateCluster/NodePoolPanel.tsx | 6 +- .../Linodes/LinodeEntityDetailBody.tsx | 57 +++++++++---------- 3 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 packages/manager/.changeset/pr-11930-upcoming-features-1743025907787.md diff --git a/packages/manager/.changeset/pr-11930-upcoming-features-1743025907787.md b/packages/manager/.changeset/pr-11930-upcoming-features-1743025907787.md new file mode 100644 index 00000000000..ae300f7e232 --- /dev/null +++ b/packages/manager/.changeset/pr-11930-upcoming-features-1743025907787.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Adjust logic for displaying encryption status on Linode Details page and encryption copy on LKE Create page ([#11930](https://github.com/linode/manager/pull/11930)) diff --git a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx index c82a85fea5a..8d96b91b3e9 100644 --- a/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx +++ b/packages/manager/src/features/Kubernetes/CreateCluster/NodePoolPanel.tsx @@ -3,6 +3,7 @@ import { CircleProgress, ErrorState } from '@linode/ui'; import { doesRegionSupportFeature } from '@linode/utilities'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; +import { useFlags } from 'src/hooks/useFlags'; import { useIsAcceleratedPlansEnabled } from 'src/features/components/PlansPanel/utils'; import { extendType } from 'src/utilities/extendType'; @@ -72,6 +73,8 @@ const Panel = (props: NodePoolPanelProps) => { types, } = props; + const flags = useFlags(); + const { isAcceleratedLKEPlansEnabled } = useIsAcceleratedPlansEnabled(); const regions = useRegionsQuery().data ?? []; @@ -114,7 +117,8 @@ const Panel = (props: NodePoolPanelProps) => { if (selectedTier === 'enterprise') { return `${ADD_NODE_POOLS_ENTERPRISE_DESCRIPTION} ${ADD_NODE_POOLS_NO_ENCRYPTION_DESCRIPTION}`; } - return regionSupportsDiskEncryption + // @TODO LDE: once LDE has been fully rolled out and is in GA in all regions, remove the feature flag condition + return regionSupportsDiskEncryption && flags.linodeDiskEncryption ? `${ADD_NODE_POOLS_DESCRIPTION} ${ADD_NODE_POOLS_ENCRYPTION_DESCRIPTION}` : ADD_NODE_POOLS_DESCRIPTION; }; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx index afe23fb086d..553af2c1492 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx @@ -242,36 +242,33 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => { )} - {(isDiskEncryptionFeatureEnabled || regionSupportsDiskEncryption) && - encryptionStatus && ( - - - - - - )} + {isDiskEncryptionFeatureEnabled && encryptionStatus && ( + + + + + + )} From 0781c95d4392712c09521a3619bcab8504080de7 Mon Sep 17 00:00:00 2001 From: Hana Xu <115299789+hana-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 13:38:03 -0400 Subject: [PATCH 39/84] refactor: [M3-9416] - Move feature flag code out of Kubernetes queries file (#11922) * move feature flag code out of useKubernetesClusterQuery * move feature flag code out of useKubernetesClustersQuery * move feature flag code out of useAllKubernetesClustersQuery --- .../pr-11922-tech-stories-1742933251448.md | 5 ++ .../KubernetesClusterDetail.tsx | 10 ++- .../KubernetesLanding/KubernetesLanding.tsx | 15 ++-- .../src/features/Kubernetes/kubeUtils.ts | 11 +++ .../Linodes/LinodeEntityDetailBody.tsx | 17 ++-- .../LinodeConfigs/LinodeConfigDialog.tsx | 23 +++-- .../NodeBalancerSummary/SummaryPanel.tsx | 16 ++-- .../features/Search/useClientSideSearch.ts | 8 +- .../SupportTicketProductSelectionFields.tsx | 7 +- packages/manager/src/queries/kubernetes.ts | 87 ++++++++++--------- 10 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 packages/manager/.changeset/pr-11922-tech-stories-1742933251448.md diff --git a/packages/manager/.changeset/pr-11922-tech-stories-1742933251448.md b/packages/manager/.changeset/pr-11922-tech-stories-1742933251448.md new file mode 100644 index 00000000000..26cb87446f2 --- /dev/null +++ b/packages/manager/.changeset/pr-11922-tech-stories-1742933251448.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Move feature flag code out of Kubernetes queries file ([#11922](https://github.com/linode/manager/pull/11922)) diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx index b99c8ddcd7c..e4992552591 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubernetesClusterDetail.tsx @@ -1,3 +1,4 @@ +import { useAccount, useRegionsQuery } from '@linode/queries'; import { Box, CircleProgress, ErrorState, Stack } from '@linode/ui'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; @@ -5,11 +6,11 @@ import { useLocation, useParams } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; +import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { getKubeHighAvailability, useAPLAvailability, } from 'src/features/Kubernetes/kubeUtils'; -import { useAccount, useRegionsQuery } from '@linode/queries'; import { useKubernetesClusterMutation, useKubernetesClusterQuery, @@ -29,7 +30,12 @@ export const KubernetesClusterDetail = () => { const location = useLocation(); const { showAPL } = useAPLAvailability(); - const { data: cluster, error, isLoading } = useKubernetesClusterQuery(id); + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); + + const { data: cluster, error, isLoading } = useKubernetesClusterQuery({ + id, + isUsingBetaEndpoint, + }); const { data: regionsData } = useRegionsQuery(); const { mutateAsync: updateKubernetesCluster } = useKubernetesClusterMutation( diff --git a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx index 158d5b0fecd..0678faa9436 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesLanding/KubernetesLanding.tsx @@ -1,3 +1,4 @@ +import { useProfile } from '@linode/queries'; import { CircleProgress, ErrorState, Typography } from '@linode/ui'; import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; @@ -23,11 +24,11 @@ import { TransferDisplay } from 'src/components/TransferDisplay/TransferDisplay' import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useKubernetesClustersQuery } from 'src/queries/kubernetes'; -import { useProfile } from '@linode/queries'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { KubernetesClusterRow } from '../ClusterList/KubernetesClusterRow'; import { DeleteKubernetesClusterDialog } from '../KubernetesClusterDetail/DeleteKubernetesClusterDialog'; +import { useKubernetesBetaEndpoint } from '../kubeUtils'; import UpgradeVersionModal from '../UpgradeVersionModal'; import { KubernetesEmptyState } from './KubernetesLandingEmptyState'; @@ -98,14 +99,16 @@ export const KubernetesLanding = () => { const isRestricted = profile?.restricted ?? false; - const { data, error, isLoading } = useKubernetesClustersQuery( - { + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); + const { data, error, isLoading } = useKubernetesClustersQuery({ + enabled: !isRestricted, + filter, + params: { page: pagination.page, page_size: pagination.pageSize, }, - filter, - !isRestricted - ); + isUsingBetaEndpoint, + }); const { isDiskEncryptionFeatureEnabled, diff --git a/packages/manager/src/features/Kubernetes/kubeUtils.ts b/packages/manager/src/features/Kubernetes/kubeUtils.ts index 446ce46b54b..bdec58da941 100644 --- a/packages/manager/src/features/Kubernetes/kubeUtils.ts +++ b/packages/manager/src/features/Kubernetes/kubeUtils.ts @@ -266,3 +266,14 @@ export const useLkeStandardOrEnterpriseVersions = ( versionsError: enterpriseTierVersionsError || versionsError, }; }; + +export const useKubernetesBetaEndpoint = () => { + const { isLoading: isAPLAvailabilityLoading, showAPL } = useAPLAvailability(); + const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); + const isUsingBetaEndpoint = showAPL || isLkeEnterpriseLAFeatureEnabled; + + return { + isAPLAvailabilityLoading, + isUsingBetaEndpoint, + }; +}; diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx index 553af2c1492..6b3ffc3a294 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailBody.tsx @@ -15,12 +15,13 @@ import { } from 'src/components/Encryption/constants'; import { useIsDiskEncryptionFeatureEnabled } from 'src/components/Encryption/utils'; import { Link } from 'src/components/Link'; +import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { AccessTable } from 'src/features/Linodes/AccessTable'; import { useKubernetesClusterQuery } from 'src/queries/kubernetes'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; -import { EncryptedStatus } from '../Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable'; import { encryptionStatusTestId } from '../Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable'; +import { EncryptedStatus } from '../Kubernetes/KubernetesClusterDetail/NodePoolsDisplay/NodeTable'; import { HighPerformanceVolumeIcon } from './HighPerformanceVolumeIcon'; import { StyledBodyGrid, @@ -154,10 +155,16 @@ export const LinodeEntityDetailBody = React.memo((props: BodyProps) => { const secondAddress = ipv6 ? ipv6 : ipv4.length > 1 ? ipv4[1] : null; const matchesLgUp = useMediaQuery(theme.breakpoints.up('lg')); - const { data: cluster } = useKubernetesClusterQuery( - linodeLkeClusterId ?? -1, - Boolean(linodeLkeClusterId) - ); + const { + isAPLAvailabilityLoading, + isUsingBetaEndpoint, + } = useKubernetesBetaEndpoint(); + + const { data: cluster } = useKubernetesClusterQuery({ + enabled: Boolean(linodeLkeClusterId) && !isAPLAvailabilityLoading, + id: linodeLkeClusterId ?? -1, + isUsingBetaEndpoint, + }); return ( <> diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index d780d22cc5e..1dd9e29efb4 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -44,7 +44,10 @@ import * as React from 'react'; import { FormLabel } from 'src/components/FormLabel'; import { Link } from 'src/components/Link'; import { LKE_ENTERPRISE_LINODE_VPC_CONFIG_WARNING } from 'src/features/Kubernetes/constants'; -import { useIsLkeEnterpriseEnabled } from 'src/features/Kubernetes/kubeUtils'; +import { + useIsLkeEnterpriseEnabled, + useKubernetesBetaEndpoint, +} from 'src/features/Kubernetes/kubeUtils'; import { DeviceSelection } from 'src/features/Linodes/LinodesDetail/LinodeRescue/DeviceSelection'; import { titlecase } from 'src/features/Linodes/presentation'; import { @@ -256,10 +259,20 @@ export const LinodeConfigDialog = (props: Props) => { const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); - const { data: cluster } = useKubernetesClusterQuery( - linode?.lke_cluster_id ?? -1, - isLkeEnterpriseLAFeatureEnabled && Boolean(linode?.lke_cluster_id) - ); + + const { + isAPLAvailabilityLoading, + isUsingBetaEndpoint, + } = useKubernetesBetaEndpoint(); + + const { data: cluster } = useKubernetesClusterQuery({ + enabled: + isLkeEnterpriseLAFeatureEnabled && + Boolean(linode?.lke_cluster_id) && + !isAPLAvailabilityLoading, + id: linode?.lke_cluster_id ?? -1, + isUsingBetaEndpoint, + }); const { enqueueSnackbar } = useSnackbar(); diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx index f6f8b158285..069311161ce 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerDetail/NodeBalancerSummary/SummaryPanel.tsx @@ -13,6 +13,7 @@ import * as React from 'react'; import { Link } from 'src/components/Link'; import { TagCell } from 'src/components/TagCell/TagCell'; +import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { IPAddress } from 'src/features/Linodes/LinodesLanding/IPAddress'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; import { useKubernetesClusterQuery } from 'src/queries/kubernetes'; @@ -41,17 +42,20 @@ export const SummaryPanel = () => { id: nodebalancer?.id, }); + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); + // If we can't get the cluster (status === 'error'), we can assume it's been deleted - const { status: clusterStatus } = useKubernetesClusterQuery( - nodebalancer?.lke_cluster?.id ?? -1, - nodebalancer && Boolean(nodebalancer.lke_cluster), - { + const { status: clusterStatus } = useKubernetesClusterQuery({ + enabled: nodebalancer && Boolean(nodebalancer.lke_cluster), + id: nodebalancer?.lke_cluster?.id ?? -1, + options: { refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, retry: false, - } - ); + }, + isUsingBetaEndpoint, + }); const configPorts = configs?.reduce((acc, config) => { return [...acc, { configId: config.id, port: config.port }]; diff --git a/packages/manager/src/features/Search/useClientSideSearch.ts b/packages/manager/src/features/Search/useClientSideSearch.ts index 974ec34e878..0e1284ff0c6 100644 --- a/packages/manager/src/features/Search/useClientSideSearch.ts +++ b/packages/manager/src/features/Search/useClientSideSearch.ts @@ -1,8 +1,9 @@ -import { useAllVolumesQuery } from '@linode/queries'; +import { useAllLinodesQuery } from '@linode/queries'; import { useAllFirewallsQuery } from '@linode/queries'; +import { useAllVolumesQuery } from '@linode/queries'; import { useAllNodeBalancersQuery } from '@linode/queries'; -import { useAllLinodesQuery } from '@linode/queries'; +import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { useAllDatabasesQuery } from 'src/queries/databases/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllImagesQuery } from 'src/queries/images'; @@ -41,11 +42,12 @@ export const useClientSideSearch = ({ enabled, query }: Props) => { error: domainsError, isLoading: domainsLoading, } = useAllDomainsQuery(enabled); + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); const { data: clusters, error: lkeClustersError, isLoading: lkeClustersLoading, - } = useAllKubernetesClustersQuery(enabled); + } = useAllKubernetesClustersQuery({ enabled, isUsingBetaEndpoint }); const { data: volumes, error: volumesError, diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx index d126f94a19e..6b2ed629a27 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx @@ -9,6 +9,7 @@ import { Autocomplete, FormHelperText, TextField } from '@linode/ui'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; +import { useKubernetesBetaEndpoint } from 'src/features/Kubernetes/kubeUtils'; import { useAllDatabasesQuery } from 'src/queries/databases/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; @@ -70,11 +71,15 @@ export const SupportTicketProductSelectionFields = (props: Props) => { isLoading: nodebalancersLoading, } = useAllNodeBalancersQuery(entityType === 'nodebalancer_id'); + const { isUsingBetaEndpoint } = useKubernetesBetaEndpoint(); const { data: clusters, error: clustersError, isLoading: clustersLoading, - } = useAllKubernetesClustersQuery(entityType === 'lkecluster_id'); + } = useAllKubernetesClustersQuery({ + enabled: entityType === 'lkecluster_id', + isUsingBetaEndpoint, + }); const { data: linodes, diff --git a/packages/manager/src/queries/kubernetes.ts b/packages/manager/src/queries/kubernetes.ts index 07f36e4442b..5293bdf09ae 100644 --- a/packages/manager/src/queries/kubernetes.ts +++ b/packages/manager/src/queries/kubernetes.ts @@ -36,11 +36,6 @@ import { useQueryClient, } from '@tanstack/react-query'; -import { - useAPLAvailability, - useIsLkeEnterpriseEnabled, -} from 'src/features/Kubernetes/kubeUtils'; - import type { CreateKubeClusterPayload, CreateNodePoolData, @@ -68,11 +63,11 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { queryFn: () => getKubernetesClusterControlPlaneACL(id), queryKey: [id], }, - cluster: (useBetaEndpoint: boolean = false) => ({ - queryFn: useBetaEndpoint + cluster: (isUsingBetaEndpoint: boolean = false) => ({ + queryFn: isUsingBetaEndpoint ? () => getKubernetesClusterBeta(id) : () => getKubernetesCluster(id), - queryKey: [useBetaEndpoint ? 'v4beta' : 'v4'], + queryKey: [isUsingBetaEndpoint ? 'v4beta' : 'v4'], }), dashboard: { queryFn: () => getKubernetesClusterDashboard(id), @@ -99,8 +94,8 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { return decodedKubeConfig; } catch (error) { const err = error as { - response?: { status?: number }; reason?: string; + response?: { status?: number }; }; const serviceUnavailableStatus = 503; if ( @@ -144,12 +139,12 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { }), lists: { contextQueries: { - all: (useBetaEndpoint: boolean = false) => ({ + all: (isUsingBetaEndpoint: boolean = false) => ({ queryFn: () => - useBetaEndpoint + isUsingBetaEndpoint ? getAllKubernetesClustersBeta() : getAllKubernetesClusters(), - queryKey: [useBetaEndpoint ? 'v4beta' : 'v4'], + queryKey: [isUsingBetaEndpoint ? 'v4beta' : 'v4'], }), infinite: (filter: Filter = {}) => ({ queryFn: ({ pageParam }) => @@ -159,13 +154,13 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { paginated: ( params: Params, filter: Filter, - useBetaEndpoint: boolean = false + isUsingBetaEndpoint: boolean = false ) => ({ queryFn: () => - useBetaEndpoint + isUsingBetaEndpoint ? getKubernetesClustersBeta(params, filter) : getKubernetesClusters(params, filter), - queryKey: [params, filter, useBetaEndpoint ? 'v4beta' : 'v4'], + queryKey: [params, filter, isUsingBetaEndpoint ? 'v4beta' : 'v4'], }), }, queryKey: null, @@ -174,11 +169,11 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { queryFn: () => getAllKubernetesTieredVersionsBeta(tier), queryKey: [tier], }), - types: (useBetaEndpoint: boolean = false) => ({ - queryFn: useBetaEndpoint + types: (isUsingBetaEndpoint: boolean = false) => ({ + queryFn: isUsingBetaEndpoint ? getAllKubernetesTypesBeta : () => getAllKubernetesTypes(), - queryKey: [useBetaEndpoint ? 'v4beta' : 'v4'], + queryKey: [isUsingBetaEndpoint ? 'v4beta' : 'v4'], }), versions: { queryFn: () => getAllKubernetesVersions(), @@ -186,18 +181,15 @@ export const kubernetesQueries = createQueryKeys('kubernetes', { }, }); -export const useKubernetesClusterQuery = ( - id: number, +export const useKubernetesClusterQuery = ({ enabled = true, - options = {} -) => { - const { isLoading: isAPLAvailabilityLoading, showAPL } = useAPLAvailability(); - const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); - const useBetaEndpoint = showAPL || isLkeEnterpriseLAFeatureEnabled; - + id = -1, + options = {}, + isUsingBetaEndpoint = false, +}) => { return useQuery({ - ...kubernetesQueries.cluster(id)._ctx.cluster(useBetaEndpoint), - enabled: enabled && !isAPLAvailabilityLoading, + ...kubernetesQueries.cluster(id)._ctx.cluster(isUsingBetaEndpoint), + enabled, ...options, }); }; @@ -220,16 +212,25 @@ export const useKubernetesClustersInfiniteQuery = ( }); }; -export const useKubernetesClustersQuery = ( - params: Params, - filter: Filter, - enabled = true -) => { - const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); - const useBetaEndpoint = isLkeEnterpriseLAFeatureEnabled; +interface KubernetesClustersQueryOptions { + enabled: boolean; + filter: Filter; + params: Params; + isUsingBetaEndpoint: boolean; +} +export const useKubernetesClustersQuery = ({ + enabled = true, + filter, + params, + isUsingBetaEndpoint = false, +}: KubernetesClustersQueryOptions) => { return useQuery, APIError[]>({ - ...kubernetesQueries.lists._ctx.paginated(params, filter, useBetaEndpoint), + ...kubernetesQueries.lists._ctx.paginated( + params, + filter, + isUsingBetaEndpoint + ), enabled, placeholderData: keepPreviousData, }); @@ -477,12 +478,12 @@ export const useKubernetesTieredVersionsQuery = ( * Avoiding fetching all Kubernetes Clusters if possible. * Before you use this, consider implementing infinite scroll instead. */ -export const useAllKubernetesClustersQuery = (enabled = false) => { - const { isLkeEnterpriseLAFeatureEnabled } = useIsLkeEnterpriseEnabled(); - const useBetaEndpoint = isLkeEnterpriseLAFeatureEnabled; - +export const useAllKubernetesClustersQuery = ({ + enabled = false, + isUsingBetaEndpoint = false, +}) => { return useQuery({ - ...kubernetesQueries.lists._ctx.all(useBetaEndpoint), + ...kubernetesQueries.lists._ctx.all(isUsingBetaEndpoint), enabled, }); }; @@ -555,8 +556,8 @@ const getAllKubernetesTypesBeta = () => (results) => results.data ); -export const useKubernetesTypesQuery = (useBetaEndpoint?: boolean) => +export const useKubernetesTypesQuery = (isUsingBetaEndpoint?: boolean) => useQuery({ ...queryPresets.oneTimeFetch, - ...kubernetesQueries.types(useBetaEndpoint), + ...kubernetesQueries.types(isUsingBetaEndpoint), }); From e9b4d68c6598eeea11449f301a2753c87eaa7a81 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:15:40 -0400 Subject: [PATCH 40/84] change: [M3-9670] - Remove region selector from Edit VPC drawer (#11929) * Update text and comments * Add changeset * Update * Update packages/manager/.changeset/pr-11929-fixed-1743022527992.md Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> --------- Co-authored-by: Jaalah Ramos Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> --- .../pr-11929-fixed-1743022527992.md | 5 ++++ .../VPCs/VPCLanding/VPCEditDrawer.tsx | 26 +------------------ 2 files changed, 6 insertions(+), 25 deletions(-) create mode 100644 packages/manager/.changeset/pr-11929-fixed-1743022527992.md diff --git a/packages/manager/.changeset/pr-11929-fixed-1743022527992.md b/packages/manager/.changeset/pr-11929-fixed-1743022527992.md new file mode 100644 index 00000000000..e5af2887f49 --- /dev/null +++ b/packages/manager/.changeset/pr-11929-fixed-1743022527992.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Remove region selector from Edit VPC drawer since data center assignment cannot be changed. ([#11929](https://github.com/linode/manager/pull/11929)) diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 4b42d252d68..497c31d7846 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -1,18 +1,11 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { - useGrants, - useProfile, - useRegionsQuery, - useUpdateVPCMutation, -} from '@linode/queries'; +import { useGrants, useProfile, useUpdateVPCMutation } from '@linode/queries'; import { ActionsPanel, Drawer, Notice, TextField } from '@linode/ui'; import { updateVPCSchema } from '@linode/validation'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; import { NotFound } from 'src/components/NotFound'; -import { RegionSelect } from 'src/components/RegionSelect/RegionSelect'; -import { useFlags } from 'src/hooks/useFlags'; import type { UpdateVPCPayload, VPC } from '@linode/api-v4'; @@ -22,12 +15,9 @@ interface Props { vpc?: VPC; } -const REGION_HELPER_TEXT = 'Region cannot be changed during beta.'; - export const VPCEditDrawer = (props: Props) => { const { onClose, open, vpc } = props; - const flags = useFlags(); const { data: profile } = useProfile(); const { data: grants } = useGrants(); @@ -77,8 +67,6 @@ export const VPCEditDrawer = (props: Props) => { } }; - const { data: regionsData, error: regionsError } = useRegionsQuery(); - return ( { control={control} name="description" /> - {regionsData && ( - null} - regions={regionsData} - value={vpc?.region} - /> - )} Date: Thu, 27 Mar 2025 14:19:40 -0500 Subject: [PATCH 41/84] refactor: [M3-7277] - SAST Scan Findings: Path Traversal Vulnerability v2 (#11914) * Address vulnerabilities * Added changeset: Resolve Path Traversal Vulnerabilities detected from semgrep --- .../pr-11914-tech-stories-1742929299022.md | 5 ++ scripts/changelog/generate-changelogs.mjs | 4 +- scripts/changelog/utils/constants.mjs | 52 ++++++++++++++++--- scripts/changelog/utils/deleteChangesets.mjs | 22 ++++---- scripts/package-versions/index.js | 15 +++++- 5 files changed, 80 insertions(+), 18 deletions(-) create mode 100644 packages/manager/.changeset/pr-11914-tech-stories-1742929299022.md diff --git a/packages/manager/.changeset/pr-11914-tech-stories-1742929299022.md b/packages/manager/.changeset/pr-11914-tech-stories-1742929299022.md new file mode 100644 index 00000000000..6deb56fe4ec --- /dev/null +++ b/packages/manager/.changeset/pr-11914-tech-stories-1742929299022.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Resolve Path Traversal Vulnerabilities detected from semgrep ([#11914](https://github.com/linode/manager/pull/11914)) diff --git a/scripts/changelog/generate-changelogs.mjs b/scripts/changelog/generate-changelogs.mjs index cded57d65c4..ab176479379 100644 --- a/scripts/changelog/generate-changelogs.mjs +++ b/scripts/changelog/generate-changelogs.mjs @@ -76,9 +76,9 @@ try { if (file === "README.md") { return; } - + // Logic to parse the changeset file and generate the changelog content - const filePath = path.join(changesetDirectory(linodePackage), file); + const filePath = changesetDirectory(linodePackage) + path.sep + file; const content = fs.readFileSync(filePath, "utf-8"); const matches = content.match( new RegExp(`"@linode/${linodePackage}": ([^\n]+)`) diff --git a/scripts/changelog/utils/constants.mjs b/scripts/changelog/utils/constants.mjs index d9aab768d70..698d2fbf340 100644 --- a/scripts/changelog/utils/constants.mjs +++ b/scripts/changelog/utils/constants.mjs @@ -28,11 +28,51 @@ export const CHANGESET_TYPES = [ export const OWNER = "linode"; export const REPO = "manager"; -export const changelogPath = (linodePackage) => - path.join(__dirname, `../../../packages/${linodePackage}/CHANGELOG.md`); -export const changesetDirectory = (linodePackage) => - path.join(__dirname, `../../../packages/${linodePackage}/.changeset`); -export const packageJsonPath = (linodePackage) => - path.join(__dirname, `../../../packages/${linodePackage}/package.json`); +const CHANGELOG_PATHS = { + "api-v4": path.join(__dirname, "../../../packages/api-v4/CHANGELOG.md"), + "manager": path.join(__dirname, "../../../packages/manager/CHANGELOG.md"), + "queries": path.join(__dirname, "../../../packages/queries/CHANGELOG.md"), + "ui": path.join(__dirname, "../../../packages/ui/CHANGELOG.md"), + "utilities": path.join(__dirname, "../../../packages/utilities/CHANGELOG.md"), + "validation": path.join(__dirname, "../../../packages/validation/CHANGELOG.md"), +}; +const CHANGESET_DIRECTORIES = { + "api-v4": path.join(__dirname, "../../../packages/api-v4/.changeset"), + "manager": path.join(__dirname, "../../../packages/manager/.changeset"), + "queries": path.join(__dirname, "../../../packages/queries/.changeset"), + "ui": path.join(__dirname, "../../../packages/ui/.changeset"), + "utilities": path.join(__dirname, "../../../packages/utilities/.changeset"), + "validation": path.join(__dirname, "../../../packages/validation/.changeset"), +}; + +const PACKAGE_JSON_PATHS = { + "api-v4": path.join(__dirname, "../../../packages/api-v4/package.json"), + "manager": path.join(__dirname, "../../../packages/manager/package.json"), + "queries": path.join(__dirname, "../../../packages/queries/package.json"), + "ui": path.join(__dirname, "../../../packages/ui/package.json"), + "utilities": path.join(__dirname, "../../../packages/utilities/package.json"), + "validation": path.join(__dirname, "../../../packages/validation/package.json"), +}; + +export const changelogPath = (linodePackage) => { + if (!CHANGELOG_PATHS[linodePackage]) { + throw new Error(`Invalid package: ${linodePackage}`); + } + return CHANGELOG_PATHS[linodePackage]; +}; + +export const changesetDirectory = (linodePackage) => { + if (!CHANGESET_DIRECTORIES[linodePackage]) { + throw new Error(`Invalid package: ${linodePackage}`); + } + return CHANGESET_DIRECTORIES[linodePackage]; +}; + +export const packageJsonPath = (linodePackage) => { + if (!PACKAGE_JSON_PATHS[linodePackage]) { + throw new Error(`Invalid package: ${linodePackage}`); + } + return PACKAGE_JSON_PATHS[linodePackage]; +}; \ No newline at end of file diff --git a/scripts/changelog/utils/deleteChangesets.mjs b/scripts/changelog/utils/deleteChangesets.mjs index d43fe9d61e2..65cd43e9ae6 100644 --- a/scripts/changelog/utils/deleteChangesets.mjs +++ b/scripts/changelog/utils/deleteChangesets.mjs @@ -20,15 +20,19 @@ export const deleteChangesets = async (linodePackage) => { const files = await readdir(changesetDir); for (const file of files) { - if (file !== "README.md") { - const filePath = path.join(changesetDir, file); - try { - await unlink(filePath); - console.warn(`Deleted: ${filePath}`); - await git.rm(filePath); - } catch (error) { - console.error(`Error occurred while deleting ${filePath}:`, error); - } + + if (file === "README.md") { + continue; + } + + const filePath = changesetDir + path.sep + file; + + try { + await unlink(filePath); + console.warn("Deleted:", filePath); + await git.rm(filePath); + } catch (error) { + console.error("Error occurred while deleting:", filePath, error); } } diff --git a/scripts/package-versions/index.js b/scripts/package-versions/index.js index 955aaa22b05..59ab03b7e8f 100644 --- a/scripts/package-versions/index.js +++ b/scripts/package-versions/index.js @@ -54,6 +54,15 @@ const flags = args.filter((arg) => { */ const root = path.resolve(import.meta.dirname, '..', '..'); +const PACKAGE_PATHS = { + 'manager': path.resolve(root, 'packages', 'manager', 'package.json'), + 'api-v4': path.resolve(root, 'packages', 'api-v4', 'package.json'), + 'validation': path.resolve(root, 'packages', 'validation', 'package.json'), + 'ui': path.resolve(root, 'packages', 'ui', 'package.json'), + 'utilities': path.resolve(root, 'packages', 'utilities', 'package.json'), + 'queries': path.resolve(root, 'packages', 'queries', 'package.json') +}; + /** * Gets the path to the package.json file for the package with the given name. * @@ -62,7 +71,11 @@ const root = path.resolve(import.meta.dirname, '..', '..'); * @returns {string} Package path for `packageName`. */ const getPackagePath = (packageName) => { - return path.join(root, 'packages', packageName, 'package.json'); + if (!PACKAGE_PATHS[packageName]) { + throw new Error(`Invalid package name: ${packageName}`); + } + + return PACKAGE_PATHS[packageName]; }; /** From 3b09800a12737fc72d2c7bb13654a21da472ae0a Mon Sep 17 00:00:00 2001 From: carrillo-erik <119514965+carrillo-erik@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:11:28 -0700 Subject: [PATCH 42/84] refactor: [M3-9389] - Move Support queries (#11904) * refactor: [M3-9389] - Move Support queries * Add changesets * PR feedback and merge conflicts --- .../pr-11904-removed-1742539061783.md | 5 + .../Account/Quotas/QuotasIncreaseForm.tsx | 3 +- .../SupportTicketDetail/CloseTicketLink.tsx | 2 +- .../SupportTicketDetail.tsx | 12 +- .../TabbedReply/ReplyContainer.tsx | 2 +- .../SupportTickets/SupportTicketDialog.tsx | 2 +- .../SupportTickets/SupportTicketsLanding.tsx | 2 +- .../SupportTickets/TicketList.test.tsx | 6 +- .../Support/SupportTickets/TicketList.tsx | 4 +- packages/manager/src/queries/support.ts | 113 +----------------- .../pr-11904-added-1742539115195.md | 5 + packages/queries/src/index.ts | 1 + packages/queries/src/support/index.ts | 1 + packages/queries/src/support/support.ts | 113 ++++++++++++++++++ 14 files changed, 142 insertions(+), 129 deletions(-) create mode 100644 packages/manager/.changeset/pr-11904-removed-1742539061783.md create mode 100644 packages/queries/.changeset/pr-11904-added-1742539115195.md create mode 100644 packages/queries/src/support/index.ts create mode 100644 packages/queries/src/support/support.ts diff --git a/packages/manager/.changeset/pr-11904-removed-1742539061783.md b/packages/manager/.changeset/pr-11904-removed-1742539061783.md new file mode 100644 index 00000000000..183bf1d4797 --- /dev/null +++ b/packages/manager/.changeset/pr-11904-removed-1742539061783.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Moved Support-related queries and dependencies to shared `queries` package ([#11904](https://github.com/linode/manager/pull/11904)) diff --git a/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx index 2d30fe2400c..c7c8f901d5e 100644 --- a/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx +++ b/packages/manager/src/features/Account/Quotas/QuotasIncreaseForm.tsx @@ -1,5 +1,5 @@ import { yupResolver } from '@hookform/resolvers/yup'; -import { useProfile } from '@linode/queries'; +import { useCreateSupportTicketMutation, useProfile } from '@linode/queries'; import { Accordion, ActionsPanel, @@ -13,7 +13,6 @@ 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'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx b/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx index e3da110d994..f8b941921e1 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/CloseTicketLink.tsx @@ -1,9 +1,9 @@ +import { useSupportTicketCloseMutation } from '@linode/queries'; import { ActionsPanel, Typography } from '@linode/ui'; import * as React from 'react'; import { makeStyles } from 'tss-react/mui'; import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; -import { useSupportTicketCloseMutation } from 'src/queries/support'; import type { Theme } from '@mui/material/styles'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx index a977c0b73e2..1582191ebbd 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/SupportTicketDetail.tsx @@ -1,6 +1,11 @@ +import { + useInfiniteSupportTicketRepliesQuery, + useProfile, + useSupportTicketQuery, +} from '@linode/queries'; import { CircleProgress, ErrorState, Stack } from '@linode/ui'; -import { styled } from '@mui/material/styles'; import Grid from '@mui/material/Grid2'; +import { styled } from '@mui/material/styles'; import { createLazyRoute } from '@tanstack/react-router'; import { isEmpty } from 'ramda'; import * as React from 'react'; @@ -9,11 +14,6 @@ import { Waypoint } from 'react-waypoint'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { LandingHeader } from 'src/components/LandingHeader'; -import { useProfile } from '@linode/queries'; -import { - useInfiniteSupportTicketRepliesQuery, - useSupportTicketQuery, -} from 'src/queries/support'; import { sanitizeHTML } from 'src/utilities/sanitizeHTML'; import { ExpandableTicketPanel } from '../ExpandableTicketPanel'; diff --git a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx index 2c11ef71073..ec28e6d064f 100644 --- a/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx +++ b/packages/manager/src/features/Support/SupportTicketDetail/TabbedReply/ReplyContainer.tsx @@ -1,4 +1,5 @@ import { uploadAttachment } from '@linode/api-v4'; +import { useSupportTicketReplyMutation } from '@linode/queries'; import { Accordion, Notice } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import { lensPath, set } from 'ramda'; @@ -6,7 +7,6 @@ import * as React from 'react'; import { debounce } from 'throttle-debounce'; import { makeStyles } from 'tss-react/mui'; -import { useSupportTicketReplyMutation } from 'src/queries/support'; import { getAPIErrorOrDefault, getErrorMap } from 'src/utilities/errorUtils'; import { storage } from 'src/utilities/storage'; diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx index de5fa30859a..03041bf5042 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDialog.tsx @@ -1,5 +1,6 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { uploadAttachment } from '@linode/api-v4/lib/support'; +import { useCreateSupportTicketMutation } from '@linode/queries'; import { Accordion, ActionsPanel, @@ -17,7 +18,6 @@ import { Controller, FormProvider, useForm } from 'react-hook-form'; import { useLocation } from 'react-router-dom'; import { debounce } from 'throttle-debounce'; -import { useCreateSupportTicketMutation } from 'src/queries/support'; import { sendSupportTicketExitEvent } from 'src/utilities/analytics/customEventAnalytics'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { storage, supportTicketStorageDefaults } from 'src/utilities/storage'; diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx index 0f42f3bcb09..e0e8332821b 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketsLanding.tsx @@ -12,7 +12,7 @@ import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { SupportTicketDialog } from './SupportTicketDialog'; -import TicketList from './TicketList'; +import { TicketList } from './TicketList'; import type { AttachmentError } from '../SupportTicketDetail/SupportTicketDetail'; import type { BaseQueryParams } from '@linode/utilities'; diff --git a/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx b/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx index 1a0c82886b3..be2d7404c3b 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketList.test.tsx @@ -3,10 +3,12 @@ import * as React from 'react'; import { supportTicketFactory } from 'src/factories'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; -import { Props, TicketList } from './TicketList'; +import { TicketList } from './TicketList'; + +import type { Props } from './TicketList'; beforeAll(() => mockMatchMedia()); diff --git a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx index fddf8f1804a..fbbe76c282c 100644 --- a/packages/manager/src/features/Support/SupportTickets/TicketList.tsx +++ b/packages/manager/src/features/Support/SupportTickets/TicketList.tsx @@ -1,3 +1,4 @@ +import { useSupportTicketsQuery } from '@linode/queries'; import * as React from 'react'; import { Hidden } from 'src/components/Hidden'; @@ -13,7 +14,6 @@ import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading' import { TableSortCell } from 'src/components/TableSortCell'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; -import { useSupportTicketsQuery } from 'src/queries/support'; import { TicketRow } from './TicketRow'; import { getStatusFilter, useTicketSeverityCapability } from './ticketUtils'; @@ -187,5 +187,3 @@ export const TicketList = (props: Props) => { ); }; - -export default TicketList; diff --git a/packages/manager/src/queries/support.ts b/packages/manager/src/queries/support.ts index 08b9af4bb58..b1ed05d14fb 100644 --- a/packages/manager/src/queries/support.ts +++ b/packages/manager/src/queries/support.ts @@ -1,118 +1,7 @@ -import { - closeSupportTicket, - createReply, - createSupportTicket, - getTicket, - getTicketReplies, - getTickets, -} from '@linode/api-v4/lib/support'; -import { createQueryKeys } from '@lukemorales/query-key-factory'; -import { - keepPreviousData, - useInfiniteQuery, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; +import { supportQueries } from '@linode/queries'; -import type { - ReplyRequest, - SupportReply, - SupportTicket, - TicketRequest, -} from '@linode/api-v4/lib/support'; -import type { - APIError, - Filter, - Params, - ResourcePage, -} from '@linode/api-v4/lib/types'; import type { EventHandlerData } from '@linode/queries'; -const supportQueries = createQueryKeys('support', { - ticket: (id: number) => ({ - contextQueries: { - replies: { - queryFn: ({ pageParam }) => - getTicketReplies(id, { page: pageParam as number, page_size: 25 }), - queryKey: null, - }, - }, - queryFn: () => getTicket(id), - queryKey: [id], - }), - tickets: (params: Params, filter: Filter) => ({ - queryFn: () => getTickets(params, filter), - queryKey: [params, filter], - }), -}); - -export const useSupportTicketsQuery = (params: Params, filter: Filter) => - useQuery, APIError[]>({ - ...supportQueries.tickets(params, filter), - placeholderData: keepPreviousData, - }); - -export const useSupportTicketQuery = (id: number) => - useQuery(supportQueries.ticket(id)); - -export const useCreateSupportTicketMutation = () => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: createSupportTicket, - onSuccess(ticket) { - queryClient.invalidateQueries({ queryKey: supportQueries.tickets._def }); - queryClient.setQueryData( - supportQueries.ticket(ticket.id).queryKey, - ticket - ); - }, - }); -}; - -export const useInfiniteSupportTicketRepliesQuery = (id: number) => - useInfiniteQuery, APIError[]>({ - ...supportQueries.ticket(id)._ctx.replies, - getNextPageParam: ({ page, pages }) => { - if (page === pages) { - return undefined; - } - return page + 1; - }, - initialPageParam: 1, - }); - -export const useSupportTicketReplyMutation = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: createReply, - onSuccess(data, variables) { - queryClient.invalidateQueries({ - queryKey: supportQueries.tickets._def, - }); - queryClient.invalidateQueries({ - queryKey: supportQueries.ticket(variables.ticket_id).queryKey, - }); - }, - }); -}; - -export const useSupportTicketCloseMutation = (id: number) => { - const queryClient = useQueryClient(); - return useMutation<{}, APIError[]>({ - mutationFn: () => closeSupportTicket(id), - onSuccess() { - queryClient.invalidateQueries({ - queryKey: supportQueries.tickets._def, - }); - queryClient.invalidateQueries({ - queryKey: supportQueries.ticket(id).queryKey, - }); - }, - }); -}; - export const supportTicketEventHandler = ({ event, invalidateQueries, diff --git a/packages/queries/.changeset/pr-11904-added-1742539115195.md b/packages/queries/.changeset/pr-11904-added-1742539115195.md new file mode 100644 index 00000000000..0db6258bfbb --- /dev/null +++ b/packages/queries/.changeset/pr-11904-added-1742539115195.md @@ -0,0 +1,5 @@ +--- +"@linode/queries": Added +--- + +Created `support/` directory and migrated relevant query keys and hooks ([#11904](https://github.com/linode/manager/pull/11904)) diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index 6002c8b1230..d207c07d896 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -8,6 +8,7 @@ export * from './nodebalancers'; export * from './placementGroups'; export * from './profile'; export * from './regions'; +export * from './support'; export * from './vlans'; export * from './vpcs'; export * from './volumes'; diff --git a/packages/queries/src/support/index.ts b/packages/queries/src/support/index.ts new file mode 100644 index 00000000000..5f9360ade81 --- /dev/null +++ b/packages/queries/src/support/index.ts @@ -0,0 +1 @@ +export * from './support'; diff --git a/packages/queries/src/support/support.ts b/packages/queries/src/support/support.ts new file mode 100644 index 00000000000..123e0e3a414 --- /dev/null +++ b/packages/queries/src/support/support.ts @@ -0,0 +1,113 @@ +import { + closeSupportTicket, + createReply, + createSupportTicket, + getTicket, + getTicketReplies, + getTickets, +} from '@linode/api-v4/lib/support'; +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; + +import type { + ReplyRequest, + SupportReply, + SupportTicket, + TicketRequest, +} from '@linode/api-v4'; +import type { + APIError, + Filter, + Params, + ResourcePage, +} from '@linode/api-v4/lib/types'; + +export const supportQueries = createQueryKeys('support', { + ticket: (id: number) => ({ + contextQueries: { + replies: { + queryFn: ({ pageParam }) => + getTicketReplies(id, { page: pageParam as number, page_size: 25 }), + queryKey: null, + }, + }, + queryFn: () => getTicket(id), + queryKey: [id], + }), + tickets: (params: Params, filter: Filter) => ({ + queryFn: () => getTickets(params, filter), + queryKey: [params, filter], + }), +}); + +export const useSupportTicketsQuery = (params: Params, filter: Filter) => + useQuery, APIError[]>({ + ...supportQueries.tickets(params, filter), + placeholderData: keepPreviousData, + }); + +export const useSupportTicketQuery = (id: number) => + useQuery(supportQueries.ticket(id)); + +export const useCreateSupportTicketMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createSupportTicket, + onSuccess(ticket) { + queryClient.invalidateQueries({ queryKey: supportQueries.tickets._def }); + queryClient.setQueryData( + supportQueries.ticket(ticket.id).queryKey, + ticket + ); + }, + }); +}; + +export const useInfiniteSupportTicketRepliesQuery = (id: number) => + useInfiniteQuery, APIError[]>({ + ...supportQueries.ticket(id)._ctx.replies, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + }); + +export const useSupportTicketReplyMutation = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createReply, + onSuccess(data, variables) { + queryClient.invalidateQueries({ + queryKey: supportQueries.tickets._def, + }); + queryClient.invalidateQueries({ + queryKey: supportQueries.ticket(variables.ticket_id).queryKey, + }); + }, + }); +}; + +export const useSupportTicketCloseMutation = (id: number) => { + const queryClient = useQueryClient(); + return useMutation<{}, APIError[]>({ + mutationFn: () => closeSupportTicket(id), + onSuccess() { + queryClient.invalidateQueries({ + queryKey: supportQueries.tickets._def, + }); + queryClient.invalidateQueries({ + queryKey: supportQueries.ticket(id).queryKey, + }); + }, + }); +}; From 53d108941f9b562f8597f19530c7e851066341ad Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Mon, 31 Mar 2025 09:04:47 -0400 Subject: [PATCH 43/84] upcoming: [M3-9108] - Update Firewall landing table and Firewall Detail to account for Linode Interfaces and Default Firewalls (#11920) * add links and default chip * hide stuff behind feature flag * Added changeset: Update Firewall Landing table to account for Linode Interface devices and Default Firewalls * add default firewall paper in firewall detail * update conditions * update spacing * update spacing again... * Added changeset: Add Default Firewall chips to Firewall Detail page * what is spacing * spacing update * Update packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> * feedback pt1 * feedback pt2 * fix test * create hook for shared logic with default firewall chips * update firewall landing links * Update packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> * update naming feedback * update spacing - ux feedback --------- Co-authored-by: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Co-authored-by: Hana Xu <115299789+hana-akamai@users.noreply.github.com> --- ...r-11920-upcoming-features-1742908940171.md | 5 + ...r-11920-upcoming-features-1742912363636.md | 5 + .../Account/DefaultFirewalls.test.tsx | 4 +- .../src/features/Account/DefaultFirewalls.tsx | 5 +- .../Devices/FirewallDeviceTable.tsx | 3 +- .../Firewalls/FirewallDetail/index.tsx | 57 +++++++- .../FirewallLanding/FirewallActionMenu.tsx | 37 +++-- .../FirewallLanding/FirewallLanding.tsx | 2 +- .../FirewallLanding/FirewallRow.test.tsx | 51 ++++++- .../Firewalls/FirewallLanding/FirewallRow.tsx | 134 +++++++++++++++--- .../Firewalls/FirewallLanding/constants.ts | 5 + .../components/DefaultFirewallChip.tsx | 15 +- .../Firewalls/components/FirewallSelect.tsx | 29 ++-- .../components/FirewallSelectOption.tsx | 24 ++-- .../FirewallSelectOption.utils.test.tsx | 14 +- .../components/FirewallSelectOption.utils.tsx | 8 +- .../src/features/Firewalls/shared.test.ts | 27 +++- .../manager/src/features/Firewalls/shared.ts | 16 ++- .../LinodeFirewallsActionMenu.tsx | 9 +- .../VPCInterfaceDetailsContent.tsx | 6 +- .../NodeBalancerFirewallsActionMenu.tsx | 4 +- .../useDefaultFirewallChipInformation.ts | 42 ++++++ 22 files changed, 399 insertions(+), 103 deletions(-) create mode 100644 packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md create mode 100644 packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md create mode 100644 packages/manager/src/hooks/useDefaultFirewallChipInformation.ts diff --git a/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md b/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md new file mode 100644 index 00000000000..84d25a7c2ed --- /dev/null +++ b/packages/manager/.changeset/pr-11920-upcoming-features-1742908940171.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update Firewall Landing table to account for Linode Interface devices and Default Firewalls ([#11920](https://github.com/linode/manager/pull/11920)) diff --git a/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md b/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md new file mode 100644 index 00000000000..2c67bcdf023 --- /dev/null +++ b/packages/manager/.changeset/pr-11920-upcoming-features-1742912363636.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add Default Firewall chips to Firewall Detail page ([#11920](https://github.com/linode/manager/pull/11920)) diff --git a/packages/manager/src/features/Account/DefaultFirewalls.test.tsx b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx index 6043c5765c0..a1d4b9478d1 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.test.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.test.tsx @@ -20,7 +20,9 @@ describe('NetworkInterfaces', () => { HttpResponse.json(makeResourcePage(firewallFactory.buildList(1))) ) ); - const { getByTestId, getByText } = renderWithTheme(); + const { getByTestId, getByText } = renderWithTheme(, { + flags: { linodeInterfaces: { enabled: true } }, + }); // Loading state should render expect(getByTestId(loadingTestId)).toBeInTheDocument(); diff --git a/packages/manager/src/features/Account/DefaultFirewalls.tsx b/packages/manager/src/features/Account/DefaultFirewalls.tsx index 524d6c0198d..8ebcb910a3d 100644 --- a/packages/manager/src/features/Account/DefaultFirewalls.tsx +++ b/packages/manager/src/features/Account/DefaultFirewalls.tsx @@ -19,6 +19,8 @@ import { useSnackbar } from 'notistack'; import * as React from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + import { FirewallSelect } from '../Firewalls/components/FirewallSelect'; import type { UpdateFirewallSettings } from '@linode/api-v4'; @@ -27,12 +29,13 @@ const DEFAULT_FIREWALL_PLACEHOLDER = 'None'; export const DefaultFirewalls = () => { const { enqueueSnackbar } = useSnackbar(); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { data: firewallSettings, error: firewallSettingsError, isLoading: isLoadingFirewallSettings, - } = useFirewallSettingsQuery(); + } = useFirewallSettingsQuery({ enabled: isLinodeInterfacesEnabled }); const { mutateAsync: updateFirewallSettings } = useMutateFirewallSettings(); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx index 903bd4d3259..6f249c535c3 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Devices/FirewallDeviceTable.tsx @@ -17,6 +17,7 @@ import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { getLinodeIdFromInterfaceDevice } from '../../shared'; import { formattedTypes } from './constants'; import { FirewallDeviceRow } from './FirewallDeviceRow'; @@ -69,7 +70,7 @@ export const FirewallDeviceTable = React.memo( const updatedDevices = devices.map((device) => { if (device.entity.type === 'interface') { - const linodeId = Number(device.entity.url.split('/')[4]); + const linodeId = getLinodeIdFromInterfaceDevice(device.entity); const associatedLinode = linodesWithInterfaces?.find( (linode) => linode.id === linodeId ); diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx index 28df22916cf..5344eaaf0c4 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/index.tsx @@ -1,11 +1,13 @@ import { useAllFirewallDevicesQuery, useFirewallQuery, + useFirewallSettingsQuery, useGrants, useMutateFirewall, useProfile, } from '@linode/queries'; -import { CircleProgress, ErrorState } from '@linode/ui'; +import { Chip, CircleProgress, ErrorState, Paper } from '@linode/ui'; +import { Typography } from '@mui/material'; import { useParams } from '@tanstack/react-router'; import * as React from 'react'; @@ -15,6 +17,7 @@ import { GenerateFirewallDialog } from 'src/components/GenerateFirewallDialog/Ge import { LandingHeader } from 'src/components/LandingHeader'; import { LinkButton } from 'src/components/LinkButton'; import { NotFound } from 'src/components/NotFound'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; import { SafeTabPanel } from 'src/components/Tabs/SafeTabPanel'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; @@ -25,8 +28,14 @@ import { useTabs } from 'src/hooks/useTabs'; import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; -import { checkIfUserCanModifyFirewall } from '../shared'; -import { SuspenseLoader } from 'src/components/SuspenseLoader'; +import { + FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME, + getFirewallDefaultEntities, +} from '../components/FirewallSelectOption.utils'; +import { + checkIfUserCanModifyFirewall, + getLinodeIdFromInterfaceDevice, +} from '../shared'; const FirewallRulesLanding = React.lazy(() => import('./Rules/FirewallRulesLanding').then((module) => ({ @@ -57,6 +66,14 @@ export const FirewallDetail = () => { const firewallId = Number(id); + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled, + }); + + const defaultEntities = + firewallSettings && + getFirewallDefaultEntities(firewallId, firewallSettings); + const userCanModifyFirewall = checkIfUserCanModifyFirewall( firewallId, profile, @@ -75,7 +92,7 @@ export const FirewallDetail = () => { isLinodeInterfacesEnabled && device.entity.type === 'interface' ) { - const linodeId = device.entity.url.split('/')[4]; + const linodeId = getLinodeIdFromInterfaceDevice(device.entity); if (!acc.seenLinodeIdsForInterfaces.has(linodeId)) { acc.linodeCount += 1; } @@ -86,7 +103,7 @@ export const FirewallDetail = () => { { linodeCount: 0, nodebalancerCount: 0, - seenLinodeIdsForInterfaces: new Set(), + seenLinodeIdsForInterfaces: new Set(), } ) || { linodeCount: 0, @@ -173,6 +190,36 @@ export const FirewallDetail = () => { {...secureVMFirewallBanner.firewallDetails} /> )} + {isLinodeInterfacesEnabled && + defaultEntities && + defaultEntities.length > 0 && ( + ({ + alignItems: 'center', + columnGap: 1, + display: 'flex', + flexWrap: 'wrap', + margin: `${theme.spacingFunction(8)} 0`, + padding: `${theme.spacingFunction(8)} ${theme.spacingFunction( + 16 + )}`, + rowGap: 1, + })} + > + ({ marginRight: theme.spacingFunction(8) })} + > + Default + + {defaultEntities.map((defaultEntity) => ( + + ))} + + )} }> diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx index cce9abb8f58..9d16b6f055e 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallActionMenu.tsx @@ -1,13 +1,21 @@ -import { FirewallStatus } from '@linode/api-v4/lib/firewalls'; -import { Theme, useTheme } from '@mui/material/styles'; +import { useGrants, useProfile } from '@linode/queries'; +import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import * as React from 'react'; -import { Action, ActionMenu } from 'src/components/ActionMenu/ActionMenu'; +import { ActionMenu } from 'src/components/ActionMenu/ActionMenu'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { useGrants, useProfile } from '@linode/queries'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; import { checkIfUserCanModifyFirewall } from '../shared'; +import { + DEFAULT_FIREWALL_TOOLTIP_TEXT, + NO_PERMISSIONS_TOOLTIP_TEXT, +} from './constants'; + +import type { FirewallStatus } from '@linode/api-v4/lib/firewalls'; +import type { Theme } from '@mui/material/styles'; +import type { Action } from 'src/components/ActionMenu/ActionMenu'; export interface ActionHandlers { [index: string]: any; @@ -20,21 +28,21 @@ interface Props extends ActionHandlers { firewallID: number; firewallLabel: string; firewallStatus: FirewallStatus; + isDefaultFirewall: boolean; } -export const noPermissionTooltipText = - "You don't have permissions to modify this Firewall."; - export const FirewallActionMenu = React.memo((props: Props) => { const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); const { data: profile } = useProfile(); const { data: grants } = useGrants(); + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); const { firewallID, firewallLabel, firewallStatus, + isDefaultFirewall, triggerDeleteFirewall, triggerDisableFirewall, triggerEnableFirewall, @@ -46,12 +54,15 @@ export const FirewallActionMenu = React.memo((props: Props) => { grants ); - const disabledProps = !userCanModifyFirewall - ? { - disabled: true, - tooltip: noPermissionTooltipText, - } - : {}; + const disabledProps = + !userCanModifyFirewall || (isLinodeInterfacesEnabled && isDefaultFirewall) + ? { + disabled: true, + tooltip: isDefaultFirewall + ? DEFAULT_FIREWALL_TOOLTIP_TEXT + : NO_PERMISSIONS_TOOLTIP_TEXT, + } + : {}; const actions: Action[] = [ { diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx index 4a8842f12c2..9609bb1e2ff 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallLanding.tsx @@ -1,3 +1,4 @@ +import { useFirewallsQuery } from '@linode/queries'; import { Button, CircleProgress, ErrorState } from '@linode/ui'; import { useLocation, useNavigate } from '@tanstack/react-router'; import * as React from 'react'; @@ -19,7 +20,6 @@ import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; import { useSecureVMNoticesEnabled } from 'src/hooks/useSecureVMNoticesEnabled'; -import { useFirewallsQuery } from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { CreateFirewallDrawer } from './CreateFirewallDrawer'; diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx index 6dd12b34133..5ec4a411e72 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.test.tsx @@ -20,6 +20,18 @@ import { getRuleString, } from './FirewallRow'; +const queryMocks = vi.hoisted(() => ({ + useFirewallSettingsQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useFirewallSettingsQuery: queryMocks.useFirewallSettingsQuery, + }; +}); + beforeAll(() => mockMatchMedia()); describe('FirewallRow', () => { @@ -54,6 +66,27 @@ describe('FirewallRow', () => { triggerEnableFirewall: mockTriggerEnableFirewall, }; + it('renders a TableRow with the default firewall chip, status, rules, and Linodes', () => { + queryMocks.useFirewallSettingsQuery.mockReturnValue({ + data: { + default_firewall_ids: { + linode: null, + nodebalancer: null, + public_interface: 1, + vpc_interface: null, + }, + }, + }); + const { getByTestId, getByText } = render( + wrapWithTableBody(, { + flags: { linodeInterfaces: { enabled: true } }, + }) + ); + getByTestId('firewall-row-1'); + getByText(firewall.label); + getByText('DEFAULT'); + }); + it('renders a TableRow with label, status, rules, and Linodes', () => { const { getByTestId, getByText } = render( wrapWithTableBody() @@ -68,21 +101,33 @@ describe('FirewallRow', () => { describe('getDeviceLinks', () => { it('should return a single Link if one Device is attached', () => { const device = firewallDeviceFactory.build(); - const links = getDeviceLinks([device.entity]); + const links = getDeviceLinks({ + entities: [device.entity], + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { getByText } = renderWithTheme(links); expect(getByText(device.entity.label ?? '')); }); it('should render up to three comma-separated links', () => { const devices = firewallDeviceFactory.buildList(3); - const links = getDeviceLinks(devices.map((device) => device.entity)); + const links = getDeviceLinks({ + entities: devices.map((device) => device.entity), + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { queryAllByTestId } = renderWithTheme(links); expect(queryAllByTestId('firewall-row-link')).toHaveLength(3); }); it('should render "plus N more" text for any devices over three', () => { const devices = firewallDeviceFactory.buildList(13); - const links = getDeviceLinks(devices.map((device) => device.entity)); + const links = getDeviceLinks({ + entities: devices.map((device) => device.entity), + isLoading: false, + linodesWithInterfaceDevices: undefined, + }); const { getByText, queryAllByTestId } = renderWithTheme(links); expect(queryAllByTestId('firewall-row-link')).toHaveLength(3); expect(getByText(/10 more/)); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx index f5b2c02701c..3d08b8316f1 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/FirewallRow.tsx @@ -1,22 +1,60 @@ +import { useAllLinodesQuery } from '@linode/queries'; import { capitalize } from '@linode/utilities'; import React from 'react'; import { Hidden } from 'src/components/Hidden'; import { Link } from 'src/components/Link'; +import { Skeleton } from 'src/components/Skeleton'; import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { DefaultFirewallChip } from '../components/DefaultFirewallChip'; +import { getLinodeIdFromInterfaceDevice } from '../shared'; import { FirewallActionMenu } from './FirewallActionMenu'; import type { ActionHandlers } from './FirewallActionMenu'; -import type { Firewall, FirewallDeviceEntity } from '@linode/api-v4'; +import type { + Filter, + Firewall, + FirewallDeviceEntity, + Linode, +} from '@linode/api-v4'; export interface FirewallRowProps extends Firewall, ActionHandlers {} export const FirewallRow = React.memo((props: FirewallRowProps) => { const { entities, id, label, rules, status, ...actionHandlers } = props; + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { + defaultNumEntities, + isDefault, + tooltipText, + } = useDefaultFirewallChipInformation(id); + + const neededLinodeIdsForInterfaceDevices = entities + .slice(0, 3) // only take the first three entities since we only show those entity links + .filter((entity) => entity.type === 'interface') + .map((entity) => { + return { id: getLinodeIdFromInterfaceDevice(entity) }; + }); + + const filterForInterfaceDeviceLinodes: Filter = { + ['+or']: neededLinodeIdsForInterfaceDevices, + }; + + // only fire this query if we have linode interface devices. We fetch the Linodes those devices are attached to + // so that we can add a label to the devices for sorting and display purposes + const { data: linodesWithInterfaceDevices, isLoading } = useAllLinodesQuery( + {}, + filterForInterfaceDeviceLinodes, + isLinodeInterfacesEnabled && neededLinodeIdsForInterfaceDevices.length > 0 + ); + const count = getCountOfRules(rules); return ( @@ -25,6 +63,13 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {label} + {isLinodeInterfacesEnabled && isDefault && ( + + )} @@ -32,7 +77,14 @@ export const FirewallRow = React.memo((props: FirewallRowProps) => { {getRuleString(count)} - {getDevicesCellString(entities)} + + {getDevicesCellString({ + entities, + isLinodeInterfacesEnabled, + isLoading, + linodesWithInterfaceDevices, + })} + { firewallID={id} firewallLabel={label} firewallStatus={status} + isDefaultFirewall={isDefault} {...actionHandlers} /> @@ -77,31 +130,76 @@ export const getCountOfRules = (rules: Firewall['rules']): [number, number] => { return [(rules.inbound || []).length, (rules.outbound || []).length]; }; -const getDevicesCellString = (entities: FirewallDeviceEntity[]) => { - if (entities.length === 0) { +interface DeviceLinkInputs { + entities: FirewallDeviceEntity[]; + isLinodeInterfacesEnabled: boolean; + isLoading: boolean; + linodesWithInterfaceDevices: Linode[] | undefined; +} +const getDevicesCellString = (inputs: DeviceLinkInputs) => { + const { + entities, + isLinodeInterfacesEnabled, + isLoading, + linodesWithInterfaceDevices, + } = inputs; + const filteredEntities = isLinodeInterfacesEnabled + ? entities + : entities.filter((entity) => entity.type !== 'interface'); + + if (filteredEntities.length === 0) { return 'None assigned'; } - return getDeviceLinks(entities); + return getDeviceLinks({ + entities: filteredEntities, + isLoading, + linodesWithInterfaceDevices, + }); }; -export const getDeviceLinks = (entities: FirewallDeviceEntity[]) => { +export const getDeviceLinks = ( + inputs: Omit +) => { + const { entities, isLoading, linodesWithInterfaceDevices } = inputs; const firstThree = entities.slice(0, 3); + if (isLoading) { + return ; + } + return ( <> - {firstThree.map((entity, idx) => ( - - {idx > 0 && ', '} - - {entity.label} - - - ))} + {firstThree.map((entity, idx) => { + // TODO @Linode Interfaces - switch to parent entity when endpoints are updated + const isInterfaceDevice = entity.type === 'interface'; + let entityLabel = entity.label; + let entityLink = `/${entity.type}s/${entity.id}/${ + entity.type === 'linode' ? 'networking' : 'summary' + }`; + + if (isInterfaceDevice) { + const parentEntityId = getLinodeIdFromInterfaceDevice(entity); + entityLabel = + linodesWithInterfaceDevices?.find( + (linode) => linode.id === parentEntityId + )?.label ?? entity.label; + entityLink = `/linodes/${parentEntityId}/networking/interfaces/${entity.id}`; + } + + return ( + + {idx > 0 && ', '} + + {entityLabel} + + + ); + })} {entities.length > 3 && , plus {entities.length - 3} more.} ); diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts index d2523ab4dbf..fee644396f3 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts +++ b/packages/manager/src/features/Firewalls/FirewallLanding/constants.ts @@ -12,3 +12,8 @@ export const NODEBALANCER_HELPER_TEXT = export const STRENGTHEN_TEMPLATE_RULES = 'It is recommended to further strengthen these rules by limiting the allowed IPv4 and IPv6 ranges.'; + +export const NO_PERMISSIONS_TOOLTIP_TEXT = + "You don't have permissions to modify this Firewall."; +export const DEFAULT_FIREWALL_TOOLTIP_TEXT = + 'This firewall is used as an interface default and cannot be modified. Change the firewall default assignment in Account Settings to modify the firewall.'; diff --git a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx index 7258979ff4f..60f9fe8834b 100644 --- a/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx +++ b/packages/manager/src/features/Firewalls/components/DefaultFirewallChip.tsx @@ -1,17 +1,28 @@ import { Chip, Tooltip } from '@linode/ui'; import React from 'react'; +import type { ChipProps } from '@linode/ui'; + interface Props { + chipProps?: Partial; + defaultNumEntities: number; tooltipText: React.ReactNode; } export const DefaultFirewallChip = (props: Props) => { + const { chipProps, defaultNumEntities, tooltipText } = props; return ( - + 1 ? ` (${defaultNumEntities})` : '' + }`} + size="small" + {...chipProps} + /> ); }; diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx index ddd6e66190c..cf86f651a07 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelect.tsx @@ -1,15 +1,11 @@ -import { - useAllFirewallsQuery, - useFirewallSettingsQuery, -} from '@linode/queries'; +import { useAllFirewallsQuery } from '@linode/queries'; import { Autocomplete } from '@linode/ui'; import React, { useMemo } from 'react'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; import { DefaultFirewallChip } from './DefaultFirewallChip'; import { FirewallSelectOption } from './FirewallSelectOption'; -import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; import type { Firewall } from '@linode/api-v4'; import type { EnhancedAutocompleteProps } from '@linode/ui'; @@ -53,19 +49,13 @@ export const FirewallSelect = ( ) => { const { errorText, hideDefaultChips, loading, value, ...rest } = props; - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled && !hideDefaultChips, - }); - - const defaultDescription = - firewallSettings && - value && - getDefaultFirewallDescription(value, firewallSettings); - const isDefault = !!defaultDescription; + const { + defaultNumEntities, + isDefault, + tooltipText, + } = useDefaultFirewallChipInformation(value, hideDefaultChips); const selectedFirewall = useMemo( () => firewalls?.find((firewall) => firewall.id === value) ?? null, @@ -86,7 +76,10 @@ export const FirewallSelect = ( textFieldProps={{ InputProps: { endAdornment: isDefault && !hideDefaultChips && ( - + ), }, }} diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx index 4ebdf5f236a..92fdf56934b 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.tsx @@ -1,11 +1,9 @@ -import { useFirewallSettingsQuery } from '@linode/queries'; import { Box, SelectedIcon, Stack } from '@linode/ui'; import React from 'react'; -import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; +import { useDefaultFirewallChipInformation } from 'src/hooks/useDefaultFirewallChipInformation'; import { DefaultFirewallChip } from './DefaultFirewallChip'; -import { getDefaultFirewallDescription } from './FirewallSelectOption.utils'; import type { Firewall } from '@linode/api-v4'; import type { AutocompleteRenderOptionState } from '@mui/material'; @@ -24,16 +22,11 @@ interface Props { export const FirewallSelectOption = (props: Props) => { const { hideDefaultChip, listItemProps, option, state } = props; - const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); - const { data: firewallSettings } = useFirewallSettingsQuery({ - enabled: isLinodeInterfacesEnabled && !hideDefaultChip, - }); - - const defaultDescription = - firewallSettings && - getDefaultFirewallDescription(option.id, firewallSettings); - - const isDefault = !!defaultDescription; + const { + defaultNumEntities, + isDefault, + tooltipText, + } = useDefaultFirewallChipInformation(option.id); return (
  • @@ -41,7 +34,10 @@ export const FirewallSelectOption = (props: Props) => { {option.label} {isDefault && !hideDefaultChip && ( - + )} {state.selected && } diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx index 0d2eef0bb6e..dd7c9060eab 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.test.tsx @@ -3,10 +3,10 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { getDefaultFirewallDescription, - getEntitiesThatFirewallIsDefaultFor, + getFirewallDefaultEntities, } from './FirewallSelectOption.utils'; -describe('getEntitiesThatFirewallIsDefaultFor', () => { +describe('getFirewallDefaultEntities', () => { it('returns entities that a firewall is a default for', () => { const firewallSettings = firewallSettingsFactory.build({ default_firewall_ids: { @@ -17,7 +17,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(4, firewallSettings)).toEqual([ + expect(getFirewallDefaultEntities(4, firewallSettings)).toEqual([ 'linode', 'nodebalancer', 'public_interface', @@ -34,9 +34,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( - [] - ); + expect(getFirewallDefaultEntities(1, firewallSettings)).toEqual([]); }); it('returns an empty array if the user has no default firewalls set', () => { @@ -49,9 +47,7 @@ describe('getEntitiesThatFirewallIsDefaultFor', () => { }, }); - expect(getEntitiesThatFirewallIsDefaultFor(1, firewallSettings)).toEqual( - [] - ); + expect(getFirewallDefaultEntities(1, firewallSettings)).toEqual([]); }); }); diff --git a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx index 5fddaeed7c9..9f991daaf3e 100644 --- a/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx +++ b/packages/manager/src/features/Firewalls/components/FirewallSelectOption.utils.tsx @@ -8,7 +8,7 @@ export type FirewallDefaultEntity = keyof FirewallSettings['default_firewall_ids /** * Maps an entity that supports default firewalls to a readable name. */ -const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< +export const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< FirewallDefaultEntity, string > = { @@ -19,7 +19,7 @@ const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< }; /** - * getEntitiesThatFirewallIsDefaultFor + * getFirewallDefaultEntities * * @param firewallId The ID of the Firewall * @param firewallSettings The account FirewallSettings from the API @@ -27,7 +27,7 @@ const FIREWALL_DEFAULT_ENTITY_TO_READABLE_NAME: Record< * @returns An array of entities that this Firewall is a default for. * @example ['nodebalancer', 'vpc_interface'] */ -export function getEntitiesThatFirewallIsDefaultFor( +export function getFirewallDefaultEntities( firewallId: number, firewallSettings: FirewallSettings ) { @@ -56,7 +56,7 @@ export function getDefaultFirewallDescription( firewallId: number, firewallSettings: FirewallSettings ) { - const entitiesThatFirewallIsDefaultFor = getEntitiesThatFirewallIsDefaultFor( + const entitiesThatFirewallIsDefaultFor = getFirewallDefaultEntities( firewallId, firewallSettings ); diff --git a/packages/manager/src/features/Firewalls/shared.test.ts b/packages/manager/src/features/Firewalls/shared.test.ts index dfec1c0560f..092edb562c5 100644 --- a/packages/manager/src/features/Firewalls/shared.test.ts +++ b/packages/manager/src/features/Firewalls/shared.test.ts @@ -1,12 +1,16 @@ -import { FirewallRuleType } from '@linode/api-v4/lib/firewalls/types'; - import { allIPv4, allIPv6, generateAddressesLabel, + getLinodeIdFromInterfaceDevice, predefinedFirewallFromRule, } from './shared'; +import type { + FirewallDeviceEntityType, + FirewallRuleType, +} from '@linode/api-v4/lib/firewalls/types'; + const addresses = { ipv4: [allIPv4], ipv6: [allIPv6], @@ -144,3 +148,22 @@ describe('generateAddressLabel', () => { ); }); }); + +describe('getLinodeIdFromInterfaceDevice', () => { + it('returns the ID', () => { + const entity = { + id: 123, + label: null, + type: 'interface' as FirewallDeviceEntityType, + url: '/v4/linode/instances/123/interfaces/123', + }; + + expect(getLinodeIdFromInterfaceDevice(entity)).toEqual(123); + expect( + getLinodeIdFromInterfaceDevice({ + ...entity, + url: '/v4/linode/instances/456/interfaces/123', + }) + ).toEqual(456); + }); +}); diff --git a/packages/manager/src/features/Firewalls/shared.ts b/packages/manager/src/features/Firewalls/shared.ts index efdf5c7b857..7b80e847261 100644 --- a/packages/manager/src/features/Firewalls/shared.ts +++ b/packages/manager/src/features/Firewalls/shared.ts @@ -2,7 +2,7 @@ import { truncateAndJoinList } from '@linode/utilities'; import { capitalize } from '@linode/utilities'; import type { PORT_PRESETS } from './FirewallDetail/Rules/shared'; -import type { Grants, Profile } from '@linode/api-v4'; +import type { FirewallDeviceEntity, Grants, Profile } from '@linode/api-v4'; import type { Firewall, FirewallRuleProtocol, @@ -268,3 +268,17 @@ export const getFirewallDescription = (firewall: Firewall) => { ]; return description.join(', '); }; + +// TODO @Linode Interfaces - probably get rid of this once the API changes to FirewallDevice come in +/** + * Utility function to extract the Linode ID from firewall interface device entities. For Interface devices, + * the URL is "/v4/linode/instances/123/interfaces/123" + * + * Assumptions: the entity device being passed into this function always has type "interface". The URL is + * always in the above format. + */ +export const getLinodeIdFromInterfaceDevice = ( + entity: FirewallDeviceEntity +): number => { + return Number(entity.url.split('/')[4]); +}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx index 848557129eb..1c8a85bafdd 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeFirewalls/LinodeFirewallsActionMenu.tsx @@ -1,10 +1,11 @@ +import { useGrants, useProfile } from '@linode/queries'; import * as React from 'react'; -import { Action } from 'src/components/ActionMenu/ActionMenu'; import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuAction'; -import { noPermissionTooltipText } from 'src/features/Firewalls/FirewallLanding/FirewallActionMenu'; +import { NO_PERMISSIONS_TOOLTIP_TEXT } from 'src/features/Firewalls/FirewallLanding/constants'; import { checkIfUserCanModifyFirewall } from 'src/features/Firewalls/shared'; -import { useGrants, useProfile } from '@linode/queries'; + +import type { Action } from 'src/components/ActionMenu/ActionMenu'; interface LinodeFirewallsActionMenuProps { firewallID: number; @@ -28,7 +29,7 @@ export const LinodeFirewallsActionMenu = ( const disabledProps = !userCanModifyFirewall ? { disabled: true, - tooltip: noPermissionTooltipText, + tooltip: NO_PERMISSIONS_TOOLTIP_TEXT, } : {}; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx index e92fb1b5099..9bed4dcac59 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/LinodeInterfaces/InterfaceDetailsDrawer/VPCInterfaceDetailsContent.tsx @@ -17,18 +17,16 @@ export const VPCInterfaceDetailsContent = (props: VPCInterfaceData) => { <> {ipv4.addresses.map((address) => address.nat_1_1_address ? ( - <> + - + ) : ( { const disabledProps = !userCanModifyFirewall ? { disabled: true, - tooltip: noPermissionTooltipText, + tooltip: NO_PERMISSIONS_TOOLTIP_TEXT, } : {}; diff --git a/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts new file mode 100644 index 00000000000..77e5e9c3ce5 --- /dev/null +++ b/packages/manager/src/hooks/useDefaultFirewallChipInformation.ts @@ -0,0 +1,42 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; + +import { + getDefaultFirewallDescription, + getFirewallDefaultEntities, +} from 'src/features/Firewalls/components/FirewallSelectOption.utils'; +import { useIsLinodeInterfacesEnabled } from 'src/utilities/linodes'; + +/** + * Hook to obtain the information regarding Default Firewalls which can be used for + * for Default Firewall chips. + * + * Determines if the given firewall (via ID) is a default firewall + * Determines the tooltip and chip text to be used for the chip + */ +export const useDefaultFirewallChipInformation = ( + firewallId: null | number | undefined, + hideDefaultChips?: boolean +) => { + const { isLinodeInterfacesEnabled } = useIsLinodeInterfacesEnabled(); + + const { data: firewallSettings } = useFirewallSettingsQuery({ + enabled: isLinodeInterfacesEnabled && !hideDefaultChips, + }); + + const tooltipText = + firewallId && + firewallSettings && + getDefaultFirewallDescription(firewallId, firewallSettings); + const defaultNumEntities = + firewallSettings && firewallId + ? getFirewallDefaultEntities(firewallId, firewallSettings).length + : 0; + + const isDefault = !!tooltipText; + + return { + defaultNumEntities, + isDefault, + tooltipText, + }; +}; From 78c5ce024219cd8c1052679aa65958cc723f4699 Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Mon, 31 Mar 2025 11:38:34 -0400 Subject: [PATCH 44/84] test [M3-9256]: display cypress test results in html report (#11795) * initial commit of poc * initial commit * fix * Added changeset: html presentation for cypress test results * cleanup packages * cleanup * fix lock file * trying to fix lock file * cleanup * fix ts error for mochawesome * enable multiple cypress reporters * fix junit config * fix junit reporting * fix junit rpt * refactor component vs core test suites * remove debug code * inline assets option * demo changes * remove debug code * revert change * revert format change --- .gitignore | 1 + docker-compose.yml | 1 + docs/development-guide/08-testing.md | 1 + .../pr-11795-tests-1741634978355.md | 5 + packages/manager/cypress.config.ts | 28 +- packages/manager/cypress/support/e2e.ts | 2 + .../plugins/configure-multi-reporters.ts | 33 ++ .../support/plugins/configure-test-suite.ts | 38 --- .../cypress/support/plugins/html-report.ts | 33 ++ .../cypress/support/plugins/junit-report.ts | 61 ++-- .../util/cypress-mochawesome-reporter.d.ts | 1 + packages/manager/package.json | 5 +- pnpm-lock.yaml | 315 +++++++++++++++++- 13 files changed, 436 insertions(+), 88 deletions(-) create mode 100644 packages/manager/.changeset/pr-11795-tests-1741634978355.md create mode 100644 packages/manager/cypress/support/plugins/configure-multi-reporters.ts delete mode 100644 packages/manager/cypress/support/plugins/configure-test-suite.ts create mode 100644 packages/manager/cypress/support/plugins/html-report.ts create mode 100644 packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts diff --git a/.gitignore b/.gitignore index 7e58fb3e68e..fe569e46f54 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,7 @@ packages/manager/test-report.xml **/manager/cypress/downloads/ **/manager/cypress/results/ **/manager/cypress/screenshots/ +**/manager/cypress/reports/ packages/manager/cypress/fixtures/example.json diff --git a/docker-compose.yml b/docker-compose.yml index d7ccac0bb01..f37da3eaccd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,7 @@ x-e2e-env: # Cypress reporting. CY_TEST_JUNIT_REPORT: ${CY_TEST_JUNIT_REPORT} + CY_TEST_HTML_REPORT: ${CY_TEST_HTML_REPORT} CY_TEST_USER_REPORT: ${CY_TEST_USER_REPORT} # Cloud Manager build environment. diff --git a/docs/development-guide/08-testing.md b/docs/development-guide/08-testing.md index 8d51f0c4208..ac2e8fe9db4 100644 --- a/docs/development-guide/08-testing.md +++ b/docs/development-guide/08-testing.md @@ -210,6 +210,7 @@ Environment variables related to Cypress logging and reporting, as well as repor |---------------------------------|----------------------------------------------------|------------------|----------------------------| | `CY_TEST_USER_REPORT` | Log test account information when tests begin | `1` | Unset; disabled by default | | `CY_TEST_JUNIT_REPORT` | Enable JUnit reporting | `1` | Unset; disabled by default | +| `CY_TEST_HTML_REPORT` | Generate html report containing E2E test results | `1` | Unset; disabled by default | | `CY_TEST_DISABLE_FILE_WATCHING` | Disable file watching in Cypress UI | `1` | Unset; disabled by default | | `CY_TEST_DISABLE_RETRIES` | Disable test retries on failure in CI | `1` | Unset; disabled by default | | `CY_TEST_FAIL_ON_MANAGED` | Fail affected tests when Managed is enabled | `1` | Unset; disabled by default | diff --git a/packages/manager/.changeset/pr-11795-tests-1741634978355.md b/packages/manager/.changeset/pr-11795-tests-1741634978355.md new file mode 100644 index 00000000000..ccb15fb62b6 --- /dev/null +++ b/packages/manager/.changeset/pr-11795-tests-1741634978355.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +html presentation for cypress test results ([#11795](https://github.com/linode/manager/pull/11795)) diff --git a/packages/manager/cypress.config.ts b/packages/manager/cypress.config.ts index b8596bed4d9..d3c582dff3b 100644 --- a/packages/manager/cypress.config.ts +++ b/packages/manager/cypress.config.ts @@ -3,7 +3,10 @@ import { defineConfig } from 'cypress'; import { setupPlugins } from './cypress/support/plugins'; import { configureBrowser } from './cypress/support/plugins/configure-browser'; import { configureFileWatching } from './cypress/support/plugins/configure-file-watching'; -import { configureTestSuite } from './cypress/support/plugins/configure-test-suite'; +import { + enableJunitE2eReport, + enableJunitComponentReport, +} from './cypress/support/plugins/junit-report'; import { discardPassedTestRecordings } from './cypress/support/plugins/discard-passed-test-recordings'; import { loadEnvironmentConfig } from './cypress/support/plugins/load-env-config'; import { nodeVersionCheck } from './cypress/support/plugins/node-version-check'; @@ -13,14 +16,15 @@ import { configureApi } from './cypress/support/plugins/configure-api'; import { fetchAccount } from './cypress/support/plugins/fetch-account'; import { fetchLinodeRegions } from './cypress/support/plugins/fetch-linode-regions'; import { splitCypressRun } from './cypress/support/plugins/split-run'; -import { enableJunitReport } from './cypress/support/plugins/junit-report'; import { generateTestWeights } from './cypress/support/plugins/generate-weights'; import { logTestTagInfo } from './cypress/support/plugins/test-tagging-info'; import cypressViteConfig from './cypress/vite.config'; import { featureFlagOverrides } from './cypress/support/plugins/feature-flag-override'; import { postRunCleanup } from './cypress/support/plugins/post-run-cleanup'; import { resetUserPreferences } from './cypress/support/plugins/reset-user-preferences'; - +import { enableHtmlReport } from './cypress/support/plugins/html-report'; +import { configureMultiReporters } from './cypress/support/plugins/configure-multi-reporters'; +import cypressOnFix from 'cypress-on-fix'; /** * Exports a Cypress configuration object. * @@ -62,11 +66,14 @@ export default defineConfig({ viewportWidth: 500, viewportHeight: 500, - setupNodeEvents(on, config) { + setupNodeEvents(cypressOn, config) { + const on = cypressOnFix(cypressOn); return setupPlugins(on, config, [ loadEnvironmentConfig, discardPassedTestRecordings, - enableJunitReport('Component', true), + enableJunitComponentReport, + enableHtmlReport, + configureMultiReporters, ]); }, }, @@ -76,18 +83,15 @@ export default defineConfig({ // This can be overridden using `CYPRESS_BASE_URL`. baseUrl: 'http://localhost:3000', - - // This is overridden when `CY_TEST_SUITE` is defined. - // See `cypress/support/plugins/configure-test-suite.ts`. specPattern: 'cypress/e2e/core/**/*.spec.{ts,tsx}', - setupNodeEvents(on, config) { + setupNodeEvents(cypressOn, config) { + const on = cypressOnFix(cypressOn); return setupPlugins(on, config, [ loadEnvironmentConfig, nodeVersionCheck, configureApi, configureFileWatching, - configureTestSuite, configureBrowser, vitePreprocess, discardPassedTestRecordings, @@ -98,8 +102,10 @@ export default defineConfig({ featureFlagOverrides, logTestTagInfo, splitCypressRun, - enableJunitReport(), generateTestWeights, + enableJunitE2eReport, + enableHtmlReport, + configureMultiReporters, postRunCleanup, ]); }, diff --git a/packages/manager/cypress/support/e2e.ts b/packages/manager/cypress/support/e2e.ts index 5996d3d71aa..0c614affa44 100644 --- a/packages/manager/cypress/support/e2e.ts +++ b/packages/manager/cypress/support/e2e.ts @@ -14,6 +14,8 @@ // *********************************************************** import '@testing-library/cypress/add-commands'; +// reporter needs to register for events in order to attach media to test results in html report +import 'cypress-mochawesome-reporter/register'; // Cypress command and assertion setup. import chaiString from 'chai-string'; import 'cypress-axe'; diff --git a/packages/manager/cypress/support/plugins/configure-multi-reporters.ts b/packages/manager/cypress/support/plugins/configure-multi-reporters.ts new file mode 100644 index 00000000000..262f50fc223 --- /dev/null +++ b/packages/manager/cypress/support/plugins/configure-multi-reporters.ts @@ -0,0 +1,33 @@ +import { CypressPlugin } from './plugin'; +// The name of the environment variable to read when checking report configuration. +const envVarJunit = 'CY_TEST_JUNIT_REPORT'; +const envVarHtml = 'CY_TEST_HTML_REPORT'; + +/** + * Configure multiple reporters to be used by Cypress + * Multireporter uses between 0 and 2 reporters (junit, html) + * and for either core or component directory + * + * @returns Cypress configuration object. + */ +export const configureMultiReporters: CypressPlugin = (_on, config) => { + const arrReporters = []; + if (config.env[envVarJunit]) { + console.log('Junit reporting configuration added.'); + arrReporters.push('mocha-junit-reporter'); + } + if (config.env[envVarHtml]) { + console.log('Html reporting configuration added.'); + arrReporters.push('cypress-mochawesome-reporter'); + } + if (arrReporters.length > 0) { + config.reporter = 'cypress-multi-reporters'; + if (!config.reporterOptions) { + config.reporterOptions = {}; + } + config.reporterOptions.reporterEnabled = arrReporters.join(', '); + } else { + console.log('No reporters configured.'); + } + return config; +}; diff --git a/packages/manager/cypress/support/plugins/configure-test-suite.ts b/packages/manager/cypress/support/plugins/configure-test-suite.ts deleted file mode 100644 index 0e431ee07a9..00000000000 --- a/packages/manager/cypress/support/plugins/configure-test-suite.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CypressPlugin } from './plugin'; - -// The name of the environment variable to read when checking suite configuration. -const envVarName = 'CY_TEST_SUITE'; - -/** - * Overrides the Cypress test suite according to `CY_TEST_SUITE` environment variable. - * - * If `CY_TEST_SUITE` is undefined or invalid, the 'core' test suite will be run - * by default. - * - * The resolved test suite name can be read by tests and other plugins via - * `Cypress.env('cypress_test_suite')`. - * - * @returns Cypress configuration object. - */ -export const configureTestSuite: CypressPlugin = (_on, config) => { - const suiteName = (() => { - switch (config.env[envVarName]) { - case 'synthetic': - return 'synthetic'; - - case 'core': - default: - if (!!config.env[envVarName] && config.env[envVarName] !== 'core') { - const desiredSuite = config.env[envVarName]; - console.warn( - `Unknown test suite '${desiredSuite}'. Running 'core' test suite instead.` - ); - } - return 'core'; - } - })(); - - config.env['cypress_test_suite'] = suiteName; - config.specPattern = `cypress/e2e/${suiteName}/**/*.spec.{ts,tsx}`; - return config; -}; diff --git a/packages/manager/cypress/support/plugins/html-report.ts b/packages/manager/cypress/support/plugins/html-report.ts new file mode 100644 index 00000000000..ebcd7a528c2 --- /dev/null +++ b/packages/manager/cypress/support/plugins/html-report.ts @@ -0,0 +1,33 @@ +import { CypressPlugin } from './plugin'; +import cypressReporterLib from 'cypress-mochawesome-reporter/lib'; +const { beforeRunHook, afterRunHook } = cypressReporterLib; + +// The name of the environment variable to read when checking report configuration. +const envVarName = 'CY_TEST_HTML_REPORT'; + +/** + * @returns Cypress configuration object. + */ +export const enableHtmlReport: CypressPlugin = async function (on, config) { + if (!!config.env[envVarName]) { + if (!config.reporterOptions) { + config.reporterOptions = {}; + } + config.reporterOptions.cypressMochawesomeReporterReporterOptions = { + reportPageTitle: 'Cloud Manager E2e Test Results', + inlineAssets: true, + embeddedScreenshots: true, + videoOnFailOnly: true, + charts: true, + quiet: true, + }; + on('before:run', async (results) => { + await beforeRunHook(results); + }); + + on('after:run', async () => { + await afterRunHook(); + }); + } + return config; +}; diff --git a/packages/manager/cypress/support/plugins/junit-report.ts b/packages/manager/cypress/support/plugins/junit-report.ts index 33117a64823..e2d1b3393ef 100644 --- a/packages/manager/cypress/support/plugins/junit-report.ts +++ b/packages/manager/cypress/support/plugins/junit-report.ts @@ -8,42 +8,37 @@ const capitalize = (str: string): string => { }; /** - * Returns a plugin to enable JUnit reporting when `CY_TEST_JUNIT_REPORT` is defined. - * - * If no suite name is specified, this function will attempt to determine the - * suite name using the Cypress configuration object. - * - * @param suiteName - Optional suite name in the JUnit output. - * * @returns Cypress configuration object. */ -export const enableJunitReport = ( - suiteName?: string, - jenkinsMode: boolean = false -): CypressPlugin => { - return (_on, config) => { - if (!!config.env[envVarName]) { - // Use `suiteName` if it is specified. - // Otherwise, attempt to determine the test suite name using - // our Cypress configuration. - const testSuite = suiteName || config.env['cypress_test_suite'] || 'core'; - - const testSuiteName = `${capitalize(testSuite)} Test Suite`; +export const enableJunitE2eReport: CypressPlugin = (_on, config) => { + const testSuiteName = 'core'; + return getCommonJunitConfig(testSuiteName, config); +}; - // Cypress doesn't know to look for modules in the root `node_modules` - // directory, so we have to pass a relative path. - // See also: https://github.com/cypress-io/cypress/issues/6406 - config.reporter = 'node_modules/mocha-junit-reporter'; +/** + * @returns Cypress configuration object. + */ +export const enableJunitComponentReport: CypressPlugin = (_on, config) => { + const testSuiteName = 'component'; + return getCommonJunitConfig(testSuiteName, config); +}; - // See also: https://www.npmjs.com/package/mocha-junit-reporter#full-configuration-options - config.reporterOptions = { - mochaFile: 'cypress/results/test-results-[hash].xml', - rootSuiteTitle: 'Cloud Manager Cypress Tests', - testsuitesTitle: testSuiteName, - jenkinsMode, - suiteTitleSeparatedBy: jenkinsMode ? '→' : ' ', - }; +const getCommonJunitConfig = ( + testSuite: string, + config: Cypress.PluginConfigOptions +) => { + if (!!config.env[envVarName]) { + if (!config.reporterOptions) { + config.reporterOptions = {}; } - return config; - }; + const testSuiteName = `${capitalize(testSuite)} Test Suite`; + config.reporterOptions.mochaJunitReporterReporterOptions = { + mochaFile: 'cypress/results/test-results-[hash].xml', + rootSuiteTitle: 'Cloud Manager Cypress Tests', + testsuitesTitle: testSuiteName, + jenkinsMode: true, + suiteTitleSeparatedBy: '→', + }; + } + return config; }; diff --git a/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts b/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts new file mode 100644 index 00000000000..383f99e586b --- /dev/null +++ b/packages/manager/cypress/support/util/cypress-mochawesome-reporter.d.ts @@ -0,0 +1 @@ +declare module 'cypress-mochawesome-reporter/lib'; diff --git a/packages/manager/package.json b/packages/manager/package.json index 5c3b8257edd..97936057512 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -25,12 +25,12 @@ "@hookform/resolvers": "3.9.1", "@linode/api-v4": "workspace:*", "@linode/design-language-system": "^4.0.0", - "@linode/validation": "workspace:*", "@linode/queries": "workspace:*", "@linode/search": "workspace:*", "@linode/shared": "workspace:*", "@linode/ui": "workspace:*", "@linode/utilities": "workspace:*", + "@linode/validation": "workspace:*", "@lukemorales/query-key-factory": "^1.3.4", "@mui/icons-material": "^6.4.5", "@mui/material": "^6.4.5", @@ -184,6 +184,9 @@ "cypress": "14.0.1", "cypress-axe": "^1.6.0", "cypress-file-upload": "^5.0.8", + "cypress-mochawesome-reporter": "^3.8.2", + "cypress-multi-reporters": "^2.0.5", + "cypress-on-fix": "^1.1.0", "cypress-real-events": "^1.14.0", "cypress-vite": "^1.6.0", "dotenv": "^16.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63a9746e989..834bb3fd42c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -482,6 +482,15 @@ importers: cypress-file-upload: specifier: ^5.0.8 version: 5.0.8(cypress@14.0.1) + cypress-mochawesome-reporter: + specifier: ^3.8.2 + version: 3.8.2(cypress@14.0.1)(mocha@10.8.2) + cypress-multi-reporters: + specifier: ^2.0.5 + version: 2.0.5(mocha@10.8.2) + cypress-on-fix: + specifier: ^1.1.0 + version: 1.1.0 cypress-real-events: specifier: ^1.14.0 version: 1.14.0(cypress@14.0.1) @@ -3262,6 +3271,10 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + camelcase@6.3.0: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} @@ -3390,6 +3403,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -3432,6 +3448,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -3559,6 +3579,22 @@ packages: peerDependencies: cypress: '>3.0.0' + cypress-mochawesome-reporter@3.8.2: + resolution: {integrity: sha512-oJZkNzhNmN9ZD+LmZyFuPb8aWaIijyHyqYh52YOBvR6B6ckfJNCHP3A98a+/nG0H4t46CKTNwo+wNpMa4d2kjA==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + cypress: '>=6.2.0' + + cypress-multi-reporters@2.0.5: + resolution: {integrity: sha512-5ReXlNE7C/9/rpDI3z0tAJbPXsTHK7P3ogvUtBntQlmctRQ+sSMts7dIQY5MTb0XfBSge3CuwvNvaoqtw90KSQ==} + engines: {node: '>=6.0.0'} + peerDependencies: + mocha: '>=3.1.2' + + cypress-on-fix@1.1.0: + resolution: {integrity: sha512-qGdbC0vZLmR3lYPpWWZvMqgDTeA2v04zu3DBdBmJHbG+BjwlFNYGnL7Y+X4LBrB+AyCCCeCuXhV80UXA90UhWg==} + cypress-real-events@1.14.0: resolution: {integrity: sha512-XmI8y3OZLh6cjRroPalzzS++iv+pGCaD9G9kfIbtspgv7GVsDt30dkZvSXfgZb4rAN+3pOkMVB7e0j4oXydW7Q==} peerDependencies: @@ -3641,6 +3677,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -3670,6 +3709,10 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + decamelize@4.0.0: resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} engines: {node: '>=10'} @@ -3870,6 +3913,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -4144,6 +4190,10 @@ packages: find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -4210,6 +4260,14 @@ packages: framebus@6.0.0: resolution: {integrity: sha512-bL9V68hVaVBCY9rveoWbPFFI9hAXIJtESs51B+9XmzvMt38+wP8b4VdiJsavjMS6NfPZ/afQ/jc2qaHmSGI1kQ==} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -4222,6 +4280,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fsu@1.1.1: + resolution: {integrity: sha512-xQVsnjJ/5pQtcKh+KjUoZGzVWn4uNkchxTF6Lwjr4Gf7nQr8fmUfhKJ62zE77+xQg9xnxi5KUps7XGs+VC986A==} + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -4818,6 +4879,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} @@ -4920,6 +4984,10 @@ packages: localforage@1.10.0: resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -4934,9 +5002,21 @@ packages: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.isempty@4.4.0: + resolution: {integrity: sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==} + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isobject@3.0.2: + resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==} + lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -5230,6 +5310,20 @@ packages: engines: {node: '>= 14.0.0'} hasBin: true + mochawesome-merge@4.4.1: + resolution: {integrity: sha512-QCzsXrfH5ewf4coUGvrAOZSpRSl9Vg39eqL2SpKKGkUw390f18hx9C90BNWTA4f/teD2nA0Inb1yxYPpok2gvg==} + engines: {node: '>=10.0.0'} + hasBin: true + + mochawesome-report-generator@6.2.0: + resolution: {integrity: sha512-Ghw8JhQFizF0Vjbtp9B0i//+BOkV5OWcQCPpbO0NGOoxV33o+gKDYU0Pr2pGxkIHnqZ+g5mYiXF7GMNgAcDpSg==} + hasBin: true + + mochawesome@7.1.3: + resolution: {integrity: sha512-Vkb3jR5GZ1cXohMQQ73H3cZz7RoxGjjUo0G5hu0jLaW+0FdUxUwg3Cj29bqQdh0rFcnyV06pWmqmi5eBPnEuNQ==} + peerDependencies: + mocha: '>=7' + moment@2.30.1: resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} @@ -5361,6 +5455,10 @@ packages: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} + opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} @@ -5379,10 +5477,18 @@ packages: outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -5391,6 +5497,10 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -5837,6 +5947,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requireindex@1.1.0: resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} engines: {node: '>=0.10.5'} @@ -5962,6 +6075,9 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -6237,6 +6353,12 @@ packages: resolution: {integrity: sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==} engines: {node: '>=10.0.0'} + tcomb-validation@3.4.1: + resolution: {integrity: sha512-urVVMQOma4RXwiVCa2nM2eqrAomHROHvWPuj6UkDGz/eb5kcy0x6P0dVt6kzpUZtYMNoAqJLWmz1BPtxrtjtrA==} + + tcomb@3.2.29: + resolution: {integrity: sha512-di2Hd1DB2Zfw6StGv861JoAF5h/uQVu/QJp2g8KVbtfKnoHdBQl5M32YWq6mnSYBQ1vFFrns5B1haWJL7rKaOQ==} + terser@5.36.0: resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} engines: {node: '>=10'} @@ -6526,6 +6648,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -6576,6 +6702,10 @@ packages: v8-compile-cache@2.4.0: resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + value-equal@1.0.1: resolution: {integrity: sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==} @@ -6759,6 +6889,9 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which-typed-array@1.1.15: resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} engines: {node: '>= 0.4'} @@ -6837,6 +6970,9 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6849,6 +6985,10 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -6861,6 +7001,10 @@ packages: resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} engines: {node: '>=10'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + yargs@16.2.0: resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} engines: {node: '>=10'} @@ -7433,7 +7577,7 @@ snapshots: '@eslint/eslintrc@0.4.3': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) espree: 7.3.1 globals: 13.24.0 ignore: 4.0.6 @@ -7455,7 +7599,7 @@ snapshots: '@humanwhocodes/config-array@0.5.0': dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -7639,7 +7783,7 @@ snapshots: '@kwsites/file-exists@1.1.1': dependencies: - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8747,7 +8891,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.7.3) '@typescript-eslint/utils': 6.21.0(eslint@7.32.0)(typescript@5.7.3) - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) eslint: 7.32.0 ts-api-utils: 1.4.0(typescript@5.7.3) optionalDependencies: @@ -8794,7 +8938,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.3.7 + debug: 4.4.0(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -9293,6 +9437,8 @@ snapshots: callsites@3.1.0: {} + camelcase@5.3.1: {} + camelcase@6.3.0: {} caniuse-lite@1.0.30001680: {} @@ -9427,6 +9573,12 @@ snapshots: cli-width@4.1.0: {} + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -9469,6 +9621,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@12.1.0: {} commander@13.1.0: {} @@ -9582,6 +9736,32 @@ snapshots: dependencies: cypress: 14.0.1 + cypress-mochawesome-reporter@3.8.2(cypress@14.0.1)(mocha@10.8.2): + dependencies: + commander: 10.0.1 + cypress: 14.0.1 + fs-extra: 10.1.0 + mochawesome: 7.1.3(mocha@10.8.2) + mochawesome-merge: 4.4.1 + mochawesome-report-generator: 6.2.0 + transitivePeerDependencies: + - mocha + + cypress-multi-reporters@2.0.5(mocha@10.8.2): + dependencies: + debug: 4.4.0(supports-color@8.1.1) + lodash: 4.17.21 + mocha: 10.8.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + cypress-on-fix@1.1.0: + dependencies: + debug: 4.4.0(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + cypress-real-events@1.14.0(cypress@14.0.1): dependencies: cypress: 14.0.1 @@ -9707,6 +9887,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dateformat@4.6.3: {} + dayjs@1.11.13: {} debug@3.2.7(supports-color@8.1.1): @@ -9725,6 +9907,8 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decamelize@1.2.0: {} + decamelize@4.0.0: {} decimal.js-light@2.5.1: {} @@ -10019,6 +10203,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -10408,6 +10594,11 @@ snapshots: find-root@1.1.0: {} + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -10479,6 +10670,18 @@ snapshots: dependencies: '@braintree/uuid': 0.1.0 + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 @@ -10491,6 +10694,8 @@ snapshots: fsevents@2.3.3: optional: true + fsu@1.1.1: {} + function-bind@1.1.2: {} function.prototype.name@1.1.6: @@ -11088,6 +11293,10 @@ snapshots: json5@2.2.3: {} + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + jsonfile@6.1.0: dependencies: universalify: 2.0.1 @@ -11230,6 +11439,10 @@ snapshots: dependencies: lie: 3.1.1 + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -11240,8 +11453,16 @@ snapshots: lodash.get@4.4.2: {} + lodash.isempty@4.4.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isobject@3.0.2: {} + lodash.isplainobject@4.0.6: {} + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} lodash.once@4.1.1: {} @@ -11731,6 +11952,41 @@ snapshots: yargs-parser: 20.2.9 yargs-unparser: 2.0.0 + mochawesome-merge@4.4.1: + dependencies: + fs-extra: 7.0.1 + glob: 7.2.3 + yargs: 15.4.1 + + mochawesome-report-generator@6.2.0: + dependencies: + chalk: 4.1.2 + dateformat: 4.6.3 + escape-html: 1.0.3 + fs-extra: 10.1.0 + fsu: 1.1.1 + lodash.isfunction: 3.0.9 + opener: 1.5.2 + prop-types: 15.8.1 + tcomb: 3.2.29 + tcomb-validation: 3.4.1 + validator: 13.12.0 + yargs: 17.7.2 + + mochawesome@7.1.3(mocha@10.8.2): + dependencies: + chalk: 4.1.2 + diff: 5.2.0 + json-stringify-safe: 5.0.1 + lodash.isempty: 4.4.0 + lodash.isfunction: 3.0.9 + lodash.isobject: 3.0.2 + lodash.isstring: 4.0.1 + mocha: 10.8.2 + mochawesome-report-generator: 6.2.0 + strip-ansi: 6.0.1 + uuid: 8.3.2 + moment@2.30.1: {} mrmime@2.0.0: {} @@ -11872,6 +12128,8 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 + opener@1.5.2: {} + optionator@0.8.3: dependencies: deep-is: 0.1.4 @@ -11896,10 +12154,18 @@ snapshots: outvariant@1.4.3: {} + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -11908,6 +12174,8 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -12358,6 +12626,8 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + requireindex@1.1.0: {} requires-port@1.0.0: {} @@ -12494,6 +12764,8 @@ snapshots: dependencies: randombytes: 2.1.0 + set-blocking@2.0.0: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -12826,6 +13098,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + tcomb-validation@3.4.1: + dependencies: + tcomb: 3.2.29 + + tcomb@3.2.29: {} + terser@5.36.0: dependencies: '@jridgewell/source-map': 0.3.6 @@ -13113,6 +13391,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universalify@0.1.2: {} + universalify@0.2.0: {} universalify@2.0.1: {} @@ -13162,6 +13442,8 @@ snapshots: v8-compile-cache@2.4.0: {} + validator@13.12.0: {} + value-equal@1.0.1: {} verror@1.10.0: @@ -13362,6 +13644,8 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.3 + which-module@2.0.1: {} + which-typed-array@1.1.15: dependencies: available-typed-arrays: 1.0.7 @@ -13432,12 +13716,19 @@ snapshots: xmlchars@2.2.0: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@3.1.1: {} yaml@2.6.1: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} @@ -13449,6 +13740,20 @@ snapshots: flat: 5.0.2 is-plain-obj: 2.1.0 + yargs@15.4.1: + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + yargs@16.2.0: dependencies: cliui: 7.0.4 From 3871670873e33eca362aef73088ed800c66db5f3 Mon Sep 17 00:00:00 2001 From: dmcintyr-akamai Date: Mon, 31 Mar 2025 11:40:29 -0400 Subject: [PATCH 45/84] test: [M3-7510] - Cypress tests for Databases create page for restricted users (#11912) * initial commit * merge develop * delete undesired file * remove debug code * add testid to create db form * add aria label to table * Added changeset: Tests for restricted user on database page --- .../pr-11912-tests-1743006972477.md | 5 + .../core/databases/create-database.spec.ts | 93 ++++++++++++++++++- .../DatabaseCreate/DatabaseCreate.tsx | 2 +- .../DatabaseLanding/DatabaseLandingTable.tsx | 7 +- 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 packages/manager/.changeset/pr-11912-tests-1743006972477.md diff --git a/packages/manager/.changeset/pr-11912-tests-1743006972477.md b/packages/manager/.changeset/pr-11912-tests-1743006972477.md new file mode 100644 index 00000000000..eb810b5b1d3 --- /dev/null +++ b/packages/manager/.changeset/pr-11912-tests-1743006972477.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) diff --git a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts index db6a3dee064..d053ff241fb 100644 --- a/packages/manager/cypress/e2e/core/databases/create-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/create-database.spec.ts @@ -3,7 +3,7 @@ import { mockDatabaseEngineTypes, mockDatabaseNodeTypes, } from 'support/constants/databases'; -import { mockGetAccount } from 'support/intercepts/account'; +import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; import { mockCreateDatabase, mockGetDatabaseEngines, @@ -11,10 +11,22 @@ import { mockGetDatabases, } from 'support/intercepts/databases'; import { mockGetEvents } from 'support/intercepts/events'; +import { + mockGetProfile, + mockGetProfileGrants, +} from 'support/intercepts/profile'; import { ui } from 'support/ui'; +import { randomLabel } from 'support/util/random'; import { getRegionById } from 'support/util/regions'; -import { accountFactory, databaseFactory, eventFactory } from 'src/factories'; +import { + accountFactory, + accountUserFactory, + databaseFactory, + eventFactory, + grantsFactory, + profileFactory, +} from 'src/factories'; import type { Database } from '@linode/api-v4'; import type { databaseClusterConfiguration } from 'support/constants/databases'; @@ -158,3 +170,80 @@ describe('create a database cluster, mocked data', () => { } ); }); + +describe('restricted user cannot create database', () => { + beforeEach(() => { + // Mock setup for user profile, account user, and user grants with restricted permissions, + const mockProfile = profileFactory.build({ + restricted: true, + username: randomLabel(), + }); + + const mockUser = accountUserFactory.build({ + restricted: true, + user_type: 'default', + username: mockProfile.username, + }); + + const mockGrants = grantsFactory.build({ + global: { + add_databases: false, + }, + }); + + mockGetProfile(mockProfile); + mockGetProfileGrants(mockGrants); + mockGetUser(mockUser); + mockGetDatabases([]).as('getDatabases'); + }); + it('cannot create database on landing page', () => { + // Login and wait for application to load + cy.visitWithLogin('/databases'); + cy.wait('@getDatabases'); + // Assert that Create Database button is visible and disabled + ui.button + .findByTitle('Create Database Cluster') + .should('be.visible') + .and('be.disabled') + .trigger('mouseover'); + + // Assert that tooltip is visible with message + ui.tooltip + .findByText( + "You don't have permissions to create Databases. Please contact your account administrator to request the necessary permissions." + ) + .should('be.visible'); + + // table not present for restricted user + cy.get('table[aria-label="Database Clusters"]').should('not.exist'); + // link to Docs should exist + cy.findByText('Getting Started Guides').should('be.visible'); + cy.findByText('Video Playlist').should('be.visible'); + }); + + it('cannot create database from Create menu', () => { + // Login and wait for application to load + cy.visitWithLogin('/databases/create'); + + // table present for restricted user but its inputs will be disabled + cy.get('table[aria-label="List of Linode Plans"]').should('exist'); + // Assert that Create Database button is visible and disabled + ui.button + .findByTitle('Create Database Cluster') + .should('be.visible') + .and('be.disabled') + .trigger('mouseover'); + + // Info message is visible + cy.findByText( + "You don't have permissions to create this Database. Please contact your account administrator to request the necessary permissions." + ); + + // all form inputs are disabled + cy.get('[data-testid="db-create-form"]').within(() => { + cy.get('input').each((input) => { + cy.wrap(input).should('be.disabled'); + }); + }); + }); +}); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 17c953b0301..6ddb8c98abd 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -280,7 +280,7 @@ const DatabaseCreate = () => { return ( <> -
    + -
  • +
    Date: Mon, 31 Mar 2025 10:02:23 -0700 Subject: [PATCH 46/84] refactor: [M3-9390] - Move Tags queries (#11897) * refactor: [M3-9390] - Move Tags queries * Add changesets * PR feedback and merge conflicts --- .../manager/.changeset/pr-11897-removed-1742493750939.md | 5 +++++ packages/manager/src/components/TagCell/AddTag.tsx | 8 +++++--- packages/manager/src/components/TagsInput/TagsInput.tsx | 7 +++++-- packages/manager/src/factories/tags.ts | 3 ++- .../queries/.changeset/pr-11897-added-1742493818548.md | 5 +++++ packages/queries/src/index.ts | 1 + packages/queries/src/tags/index.ts | 1 + .../{manager/src/queries => queries/src/tags}/tags.ts | 3 ++- 8 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 packages/manager/.changeset/pr-11897-removed-1742493750939.md create mode 100644 packages/queries/.changeset/pr-11897-added-1742493818548.md create mode 100644 packages/queries/src/tags/index.ts rename packages/{manager/src/queries => queries/src/tags}/tags.ts (95%) diff --git a/packages/manager/.changeset/pr-11897-removed-1742493750939.md b/packages/manager/.changeset/pr-11897-removed-1742493750939.md new file mode 100644 index 00000000000..b22f472ac88 --- /dev/null +++ b/packages/manager/.changeset/pr-11897-removed-1742493750939.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +Moved Tags-related queries and dependencies to shares `queries` package ([#11897](https://github.com/linode/manager/pull/11897)) diff --git a/packages/manager/src/components/TagCell/AddTag.tsx b/packages/manager/src/components/TagCell/AddTag.tsx index 6f7a4f50b6d..96b22b92cb4 100644 --- a/packages/manager/src/components/TagCell/AddTag.tsx +++ b/packages/manager/src/components/TagCell/AddTag.tsx @@ -1,10 +1,12 @@ +import { + updateTagsSuggestionsData, + useAllTagsQuery, + useProfile, +} from '@linode/queries'; import { Autocomplete } from '@linode/ui'; import { useQueryClient } from '@tanstack/react-query'; import * as React from 'react'; -import { useProfile } from '@linode/queries'; -import { updateTagsSuggestionsData, useAllTagsQuery } from 'src/queries/tags'; - interface AddTagProps { addTag: (tag: string) => Promise; existingTags: string[]; diff --git a/packages/manager/src/components/TagsInput/TagsInput.tsx b/packages/manager/src/components/TagsInput/TagsInput.tsx index 2529e0909dd..6dce5122a9a 100644 --- a/packages/manager/src/components/TagsInput/TagsInput.tsx +++ b/packages/manager/src/components/TagsInput/TagsInput.tsx @@ -1,11 +1,14 @@ +import { + updateTagsSuggestionsData, + useAllTagsQuery, + useProfile, +} from '@linode/queries'; import { Autocomplete, Chip } from '@linode/ui'; import CloseIcon from '@mui/icons-material/Close'; import { useQueryClient } from '@tanstack/react-query'; import { concat } from 'ramda'; import * as React from 'react'; -import { useProfile } from '@linode/queries'; -import { updateTagsSuggestionsData, useAllTagsQuery } from 'src/queries/tags'; import { getErrorMap } from 'src/utilities/errorUtils'; import type { APIError } from '@linode/api-v4/lib/types'; diff --git a/packages/manager/src/factories/tags.ts b/packages/manager/src/factories/tags.ts index b4f42bfbb61..5f100f7992f 100644 --- a/packages/manager/src/factories/tags.ts +++ b/packages/manager/src/factories/tags.ts @@ -1,6 +1,7 @@ -import { Tag } from '@linode/api-v4/lib/tags/types'; import { Factory } from '@linode/utilities'; +import type { Tag } from '@linode/api-v4'; + export const tagFactory = Factory.Sync.makeFactory({ label: Factory.each((id) => `tag-${id + 1}`), }); diff --git a/packages/queries/.changeset/pr-11897-added-1742493818548.md b/packages/queries/.changeset/pr-11897-added-1742493818548.md new file mode 100644 index 00000000000..f0b443fb5ad --- /dev/null +++ b/packages/queries/.changeset/pr-11897-added-1742493818548.md @@ -0,0 +1,5 @@ +--- +"@linode/queries": Added +--- + +Created `tags/` directory and migrated relevant query keys and hooks ([#11897](https://github.com/linode/manager/pull/11897)) diff --git a/packages/queries/src/index.ts b/packages/queries/src/index.ts index d207c07d896..4c3385b59f0 100644 --- a/packages/queries/src/index.ts +++ b/packages/queries/src/index.ts @@ -8,6 +8,7 @@ export * from './nodebalancers'; export * from './placementGroups'; export * from './profile'; export * from './regions'; +export * from './tags'; export * from './support'; export * from './vlans'; export * from './vpcs'; diff --git a/packages/queries/src/tags/index.ts b/packages/queries/src/tags/index.ts new file mode 100644 index 00000000000..6e2133be61e --- /dev/null +++ b/packages/queries/src/tags/index.ts @@ -0,0 +1 @@ +export * from './tags'; diff --git a/packages/manager/src/queries/tags.ts b/packages/queries/src/tags/tags.ts similarity index 95% rename from packages/manager/src/queries/tags.ts rename to packages/queries/src/tags/tags.ts index ac0dc241c8f..c8deeed0094 100644 --- a/packages/manager/src/queries/tags.ts +++ b/packages/queries/src/tags/tags.ts @@ -1,9 +1,10 @@ import { getTags } from '@linode/api-v4'; -import { queryPresets } from '@linode/queries'; import { getAll } from '@linode/utilities'; import { createQueryKeys } from '@lukemorales/query-key-factory'; import { useQuery } from '@tanstack/react-query'; +import { queryPresets } from '../base'; + import type { APIError, Filter, Params, Tag } from '@linode/api-v4'; import type { QueryClient } from '@tanstack/react-query'; From 7e23502d3cf57091e4de63802617fde3c72a809a Mon Sep 17 00:00:00 2001 From: santoshp210-akamai <159890961+santoshp210-akamai@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:06:57 +0530 Subject: [PATCH 47/84] upcoming: [DI-23769] - multiple error handling (#11874) * upcoming: [DI-23769] - Added util to handle and render Multiple Errors in forms, Added a Notice based component to render list or single accordingly * upcoming: [DI-23769] - Fixed linting issue * upcoming: [DI-23769] - Typecheck fix * upcoming: [DI-23769] - Added changeset * upcoming: [DI-23769] - Removed AlertsNoticeMessage component, enhanced the AlertListNoticeMessages to remove redundant components * upcoming: [DI-23769] - Fixing the failing tests * upcoming: [DI-23769] - Removed the wrapper component * upcoming: [DI-23769] - Minor UI changes * upcoming: [DI-23769] - Addressing review comments * upcoming: [DI-23769] - Fixing spacing inconsistency * upcoming: [DI-23769] - Fixed Error Notice message width in Resources across all window sizes --------- Co-authored-by: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com> --- ...r-11874-upcoming-features-1742317523478.md | 5 + .../AlertsResources/AlertsResources.tsx | 37 +++++-- .../CreateAlert/CreateAlertDefinition.tsx | 35 ++++-- .../CreateAlert/Criteria/MetricCriteria.tsx | 102 +++++++++++------- .../AddChannelListing.tsx | 41 ++++--- .../Alerts/EditAlert/EditAlertDefinition.tsx | 32 ++++-- .../Utils/AlertListNoticeMessages.test.tsx | 58 ++++++++++ .../Alerts/Utils/AlertListNoticeMessages.tsx | 62 +++++++++++ .../Alerts/Utils/AlertsNoticeMessage.tsx | 37 ------- .../CloudPulse/Alerts/Utils/utils.test.ts | 72 ++++++++++++- .../features/CloudPulse/Alerts/Utils/utils.ts | 100 +++++++++++++++++ .../features/CloudPulse/Alerts/constants.ts | 24 +++++ 12 files changed, 481 insertions(+), 124 deletions(-) create mode 100644 packages/manager/.changeset/pr-11874-upcoming-features-1742317523478.md create mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/AlertListNoticeMessages.test.tsx create mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/AlertListNoticeMessages.tsx delete mode 100644 packages/manager/src/features/CloudPulse/Alerts/Utils/AlertsNoticeMessage.tsx diff --git a/packages/manager/.changeset/pr-11874-upcoming-features-1742317523478.md b/packages/manager/.changeset/pr-11874-upcoming-features-1742317523478.md new file mode 100644 index 00000000000..1f45910637c --- /dev/null +++ b/packages/manager/.changeset/pr-11874-upcoming-features-1742317523478.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Add AlertListNoticeMessages component for handling multiple API error messages, update AddChannelListing and MetricCriteria components to display these errors, Add handleMultipleError util method for aggregating, mapping the errors to fields. ([#11874](https://github.com/linode/manager/pull/11874)) diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx index ef852381734..1773151424c 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsResources/AlertsResources.tsx @@ -1,6 +1,6 @@ import { useRegionsQuery } from '@linode/queries'; import { Checkbox, CircleProgress, Stack, Typography } from '@linode/ui'; -import { Grid } from '@mui/material'; +import { Grid, useTheme } from '@mui/material'; import React from 'react'; import EntityIcon from 'src/assets/icons/entityIcons/alertsresources.svg'; @@ -9,6 +9,8 @@ import { useFlags } from 'src/hooks/useFlags'; import { useResourcesQuery } from 'src/queries/cloudpulse/resources'; import { StyledPlaceholder } from '../AlertsDetail/AlertDetail'; +import { MULTILINE_ERROR_SEPARATOR } from '../constants'; +import { AlertListNoticeMessages } from '../Utils/AlertListNoticeMessages'; import { getAlertResourceFilterProps, getFilteredResources, @@ -17,7 +19,6 @@ import { getSupportedRegionIds, scrollToElement, } from '../Utils/AlertResourceUtils'; -import { AlertsNoticeMessage } from '../Utils/AlertsNoticeMessage'; import { AlertResourcesFilterRenderer } from './AlertsResourcesFilterRenderer'; import { AlertsResourcesNotice } from './AlertsResourcesNotice'; import { databaseTypeClassMap, serviceToFiltersMap } from './constants'; @@ -126,6 +127,7 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { } = useRegionsQuery(); const flags = useFlags(); + const theme = useTheme(); // Validate launchDarkly region ids with the ids from regionOptions prop const supportedRegionIds = getSupportedRegionIds( @@ -336,7 +338,15 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { } const filtersToRender = serviceToFiltersMap[serviceType ?? '']; - + const noticeStyles: React.CSSProperties = { + alignItems: 'center', + backgroundColor: theme.tokens.alias.Background.Normal, + borderRadius: 1, + display: 'flex', + flexWrap: 'nowrap', + marginBottom: 0, + padding: theme.spacingFunction(16), + }; return ( {!hideLabel && ( @@ -415,13 +425,24 @@ export const AlertResources = React.memo((props: AlertResourcesProp) => { )} {errorText?.length && ( - + + + )} {maxSelectionCount !== undefined && ( - + + + )} {isSelectionsNeeded && !isDataLoadingError && diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx index 7ef28a337ed..37240c8bca9 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/CreateAlertDefinition.tsx @@ -12,8 +12,16 @@ import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { useFlags } from 'src/hooks/useFlags'; import { useCreateAlertDefinition } from 'src/queries/cloudpulse/alerts'; -import { CREATE_ALERT_SUCCESS_MESSAGE } from '../constants'; -import { enhanceValidationSchemaWithEntityIdValidation } from '../Utils/utils'; +import { + CREATE_ALERT_ERROR_FIELD_MAP, + MULTILINE_ERROR_SEPARATOR, + SINGLELINE_ERROR_SEPARATOR, + CREATE_ALERT_SUCCESS_MESSAGE +} from '../constants'; +import { + enhanceValidationSchemaWithEntityIdValidation, + handleMultipleError, +} from '../Utils/utils'; import { MetricCriteriaField } from './Criteria/MetricCriteria'; import { TriggerConditions } from './Criteria/TriggerConditions'; import { CloudPulseAlertSeveritySelect } from './GeneralInformation/AlertSeveritySelect'; @@ -28,6 +36,7 @@ import type { MetricCriteriaForm, TriggerConditionForm, } from './types'; +import type { APIError } from '@linode/api-v4'; import type { ObjectSchema } from 'yup'; const triggerConditionInitialValues: TriggerConditionForm = { @@ -116,15 +125,19 @@ export const CreateAlertDefinition = () => { }); alertCreateExit(); } catch (errors) { - for (const error of errors) { - if (error.field) { - setError(error.field, { message: error.reason }); - } else { - enqueueSnackbar(`Alert failed: ${error.reason}`, { - variant: 'error', - }); - setError('root', { message: error.reason }); - } + handleMultipleError({ + errorFieldMap: CREATE_ALERT_ERROR_FIELD_MAP, + errors, + multiLineErrorSeparator: MULTILINE_ERROR_SEPARATOR, + setError, + singleLineErrorSeparator: SINGLELINE_ERROR_SEPARATOR, + }); + + const rootError = errors.find((error: APIError) => !error.field); + if (rootError) { + enqueueSnackbar(`Creating alert failed: ${rootError.reason}`, { + variant: 'error', + }); } } }); diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx index db0e92292ed..3bd8d686cb5 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/Criteria/MetricCriteria.tsx @@ -1,9 +1,16 @@ import { Button, Stack, Typography } from '@linode/ui'; import * as React from 'react'; -import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; +import { + Controller, + useFieldArray, + useFormContext, + useWatch, +} from 'react-hook-form'; import { useGetCloudPulseMetricDefinitionsByServiceType } from 'src/queries/cloudpulse/services'; +import { MULTILINE_ERROR_SEPARATOR } from '../../constants'; +import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages'; import { convertToSeconds } from '../utilities'; import { Metric } from './Metric'; @@ -66,45 +73,60 @@ export const MetricCriteriaField = (props: MetricCriteriaProps) => { }); return ( - - 3. Criteria - - {fields !== null && - fields.length !== 0 && - fields.map((field, index) => { - return ( - remove(index)} - showDeleteIcon={fields.length > 1} + ( + + 3. Criteria + {formState.isSubmitted && + fieldState.error && + fieldState.error.message?.length && ( + - ); - })} - - - + )} + + {fields !== null && + fields.length !== 0 && + fields.map((field, index) => { + return ( + remove(index)} + showDeleteIcon={fields.length > 1} + /> + ); + })} + + + + )} + control={control} + name={name} + /> ); }; diff --git a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx index 16569bd8243..02c0606c882 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/CreateAlert/NotificationChannels/AddChannelListing.tsx @@ -1,11 +1,12 @@ -import { Box, Button, Notice, Stack, Typography } from '@linode/ui'; +import { Box, Button, Stack, Typography } from '@linode/ui'; import { capitalize } from '@linode/utilities'; import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { useAllAlertNotificationChannelsQuery } from 'src/queries/cloudpulse/alerts'; -import { channelTypeOptions } from '../../constants'; +import { MULTILINE_ERROR_SEPARATOR, channelTypeOptions } from '../../constants'; +import { AlertListNoticeMessages } from '../../Utils/AlertListNoticeMessages'; import { getAlertBoxStyles } from '../../Utils/utils'; import { ClearIconButton } from '../Criteria/ClearIconButton'; import { AddNotificationChannelDrawer } from './AddNotificationChannelDrawer'; @@ -140,32 +141,38 @@ export const AddChannelListing = (props: AddChannelListingProps) => { return ( ( - <> - - 4. Notification Channels - - {(formState.isSubmitted || fieldState.isTouched) && fieldState.error && ( - - {fieldState.error.message} - - )} - - {selectedNotifications.length > 0 && - selectedNotifications.map((notification, id) => ( + + 4. Notification Channels + {(formState.isSubmitted || fieldState.isTouched) && + fieldState.error && + fieldState.error.message?.length && ( + + )} + {selectedNotifications.length > 0 && ( + + {selectedNotifications.map((notification, id) => ( ))} - + + )} - - {existingConfigsArray.length > 0 && - existingConfigsArray.map((option) => ( - ( + {configs.map((config, index) => ( + { + return ( handleRemoveConfig(index)} /> - )} - control={control} - key={option.label} - name={option.label} - /> - ))} - {existingConfigsArray.length === 0 && ( + ); + }} + control={control} + key={config.label} + name={`configs.${index}.value`} + /> + ))} + {configs.length === 0 && ( No advanced configurations have been added. @@ -182,7 +233,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { }} secondaryButtonProps={{ label: 'Cancel', - onClick: onClose, + onClick: handleClose, }} /> diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.style.ts index d9087b7abd9..09c8fa54488 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.style.ts +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.style.ts @@ -18,7 +18,10 @@ export const StyledBox = styled(Box, { export const StyledChip = styled(Chip, { label: 'StyledChip', })(({ theme }) => ({ - backgroundColor: theme.tokens.color.Amber[5], + backgroundColor: + theme.palette.mode === 'dark' + ? theme.tokens.alias.Background.Warningsubtle + : theme.tokens.color.Amber[5], color: theme.tokens.alias.Accent.Warning.Primary, font: theme.tokens.alias.Typography.Heading.Overline, textTransform: theme.tokens.alias.Typography.Heading.OverlineTextCase, diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx index 272bb1f1da6..acd1a3c046d 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx @@ -1,13 +1,19 @@ import { Autocomplete, FormControlLabel, + IconButton, TextField, Toggle, Typography, } from '@linode/ui'; +import Close from '@mui/icons-material/Close'; import React from 'react'; -import { formatConfigValue } from '../../utilities'; +import { + formatConfigValue, + isConfigBoolean, + isConfigStringWithEnum, +} from '../../utilities'; import { StyledBox, StyledChip, @@ -19,43 +25,35 @@ import type { ConfigValue } from '@linode/api-v4'; interface Props { configItem?: ConfigurationOption; - configValue?: ConfigValue; engine: string; errorText: string | undefined; onChange: (config: ConfigValue) => void; + onRemove?: (label: string) => void; } export const DatabaseConfigurationItem = (props: Props) => { - const { configItem, configValue, engine, errorText, onChange } = props; + const { configItem, engine, errorText, onChange, onRemove } = props; const configLabel = configItem?.label || ''; const renderInputField = () => { - if ( - configItem?.type === 'boolean' || - (Array.isArray(configItem?.type) && configItem?.type.includes('boolean')) - ) { + if (configItem && isConfigBoolean(configItem)) { return ( onChange(e.target.checked)} /> } - label={formatConfigValue(String(configValue))} + label={formatConfigValue(String(configItem.value))} /> ); } - if ( - (configItem?.type === 'string' && configItem.enum) || - (Array.isArray(configItem?.type) && - configItem?.type.includes('string') && - configItem.enum) - ) { + if (configItem && isConfigStringWithEnum(configItem)) { const options = configItem.enum?.map((option) => ({ label: option })) || []; const selectedValue = options.find( - (option) => option.label === String(configValue) + (option) => option.label === String(configItem.value) ); return ( { if (configItem?.type === 'number' || configItem?.type === 'integer') { return ( onChange(Number(e.target.value))} type="number" - value={Number(configValue)} + value={Number(configItem.value)} /> ); } - if (configItem?.type === 'string') { + if ( + configItem?.type === 'string' || + (Array.isArray(configItem?.type) && + configItem?.type.includes('string') && + !configItem.enum) + ) { return ( onChange(e.target.value)} + placeholder={String(configItem.example)} type="text" - value={configValue ? String(configValue) : ''} + value={configItem.value ? String(configItem.value) : ''} /> ); } - return null; }; @@ -137,6 +134,16 @@ export const DatabaseConfigurationItem = (props: Props) => { )} {renderInputField()} + + {configItem?.isNew && configItem && onRemove && ( + onRemove(configItem?.label)} + size="large" + > + + + )} ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx index bd86cb64583..87f6daef80e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect.tsx @@ -1,10 +1,13 @@ import { Autocomplete, TextField } from '@linode/ui'; import React from 'react'; +import { GroupHeader, GroupItems } from './DatabaseAdvancedConfiguration.style'; + import type { ConfigValue, ConfigurationItem } from '@linode/api-v4'; export interface ConfigurationOption extends ConfigurationItem { category: string; + isNew?: boolean; label: string; value?: ConfigValue; } @@ -24,8 +27,8 @@ export const DatabaseConfigurationSelect = (props: Props) => { return ( { - if (option.category === 'Other') { - return 'Other'; + if (option.category === 'other') { + return 'other'; } return option.category; }} @@ -35,6 +38,17 @@ export const DatabaseConfigurationSelect = (props: Props) => { onChange={(_, selected) => { onChange(selected!); }} + options={[...configurations].sort((a, b) => { + if (a.category === 'other') return 1; + if (b.category === 'other') return -1; + return a.category.localeCompare(b.category); + })} + renderGroup={(params) => ( +
  • + {params.group} + {params.children} +
  • + )} renderInput={(params) => ( { clearIcon={null} getOptionLabel={(option) => option.label} label={''} - options={configurations} value={selectedConfig ?? null} /> ); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts index c6420ce478f..dbc39604847 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.style.ts @@ -1,6 +1,6 @@ import { Typography } from '@linode/ui'; -import { styled } from '@mui/material/styles'; import Grid2 from '@mui/material/Grid2'; +import { styled } from '@mui/material/styles'; export const StyledGridContainer = styled(Grid2, { label: 'StyledGridContainer', @@ -28,10 +28,7 @@ export const StyledGridContainer = styled(Grid2, { export const StyledLabelTypography = styled(Typography, { label: 'StyledLabelTypography', })(({ theme }) => ({ - background: - theme.palette.mode === 'dark' - ? theme.bg.tableHeader - : theme.palette.grey[200], + background: theme.tokens.alias.Background.Neutral, color: theme.palette.mode === 'dark' ? theme.color.grey6 : 'inherit', font: theme.font.bold, height: '100%', diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index b8b0d78bd0d..0834e92f814 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -8,10 +8,13 @@ import { databaseTypeFactory, } from 'src/factories'; import { + convertEngineConfigToOptions, convertExistingConfigsToArray, findConfigItem, + formatConfigPayload, formatConfigValue, getDatabasesDescription, + getDefaultConfigValue, hasPendingUpdates, isDateOutsideBackup, isDefaultDatabase, @@ -25,6 +28,7 @@ import { import { HttpResponse, http, server } from 'src/mocks/testServer'; import { wrapWithTheme } from 'src/utilities/testHelpers'; +import type { ConfigurationOption } from './DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect'; import type { AccountCapability, Database, @@ -34,7 +38,6 @@ import type { PendingUpdates, } from '@linode/api-v4'; import type { TimeOption } from 'src/features/Databases/DatabaseDetail/DatabaseBackups/DatabaseBackups'; -import { ConfigurationOption } from './DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect'; const setup = (capabilities: AccountCapability[], flags: any) => { const account = accountFactory.build({ capabilities }); @@ -403,10 +406,10 @@ describe('toFormatedDate', () => { const today = DateTime.utc(); const mockTodayWithHours = DateTime.fromObject({ day: today.day, - month: today.month, - year: today.year, hour: today.hour, minute: 0, + month: today.month, + year: today.year, }).toFormat('yyyy-MM-dd HH:mm'); const result = toFormatedDate(selectedDate, undefined); expect(result).toContain(mockTodayWithHours); @@ -607,31 +610,25 @@ describe('findConfigItem', () => { const expectedNestedConfig = { description: - 'Enable the pg_stat_monitor extension. Enabling this extension will cause the cluster to be restarted. When this extension is enabled, pg_stat_statements results for utility commands are unreliable', - restart_cluster: true, - type: 'boolean', + 'The number of seconds that the mysqld server waits for a connect packet before responding with Bad handshake', + example: 10, + maximum: 3600, + minimum: 2, + restart_cluster: false, + type: 'integer', }; it('should return the correct ConfigurationItem for a given targetKey', () => { - const result = findConfigItem( - mockConfigs.engine_config, - 'binlog_retention_period' - ); + const result = findConfigItem(mockConfigs, 'binlog_retention_period'); expect(result).toEqual(expectedConfig); }); it('should return the correct ConfigurationItem for a nested key', () => { - const result = findConfigItem( - mockConfigs.engine_config, - 'pg_stat_monitor_enable' - ); + const result = findConfigItem(mockConfigs, 'connect_timeout'); expect(result).toEqual(expectedNestedConfig); }); it('should return undefined if the targetKey does not exist', () => { - const result = findConfigItem( - mockConfigs.engine_config, - 'non_existing_key' - ); + const result = findConfigItem(mockConfigs, 'non_existing_key'); expect(result).toBeUndefined(); }); }); @@ -692,3 +689,125 @@ describe('convertExistingConfigsToArray', () => { expect(result).toEqual(expectedOptions); }); }); + +describe('convertEngineConfigToOptions', () => { + it('should correctly convert a flat configuration', () => { + const configs = { + binlog_retention_period: { type: 'integer' }, + service_log: { type: ['boolean', 'null'] }, + }; + const expectedConfigOptions = [ + { + category: 'other', + enum: [], + label: 'binlog_retention_period', + type: 'integer', + }, + { + category: 'other', + enum: [], + label: 'service_log', + type: ['boolean', 'null'], + }, + ]; + expect(convertEngineConfigToOptions(configs)).toEqual( + expectedConfigOptions + ); + }); + + it('should correctly convert a nested configuration', () => { + const configs = { + mysql: { + connect_timeout: { type: 'integer' }, + default_time_zone: { type: 'string' }, + }, + }; + const expectedConfigOptions = [ + { + category: 'mysql', + enum: [], + label: 'connect_timeout', + type: 'integer', + }, + { + category: 'mysql', + enum: [], + label: 'default_time_zone', + type: 'string', + }, + ]; + expect(convertEngineConfigToOptions(configs)).toEqual( + expectedConfigOptions + ); + }); +}); + +describe('formatConfigPayload', () => { + it('should correctly format a flat configuration', () => { + const formData = [ + { category: 'other', label: 'binlog_retention_period', value: 600 }, + ]; + const configurations = [ + { category: 'other', label: 'binlog_retention_period' }, + ]; + expect(formatConfigPayload(formData, configurations)).toEqual({ + binlog_retention_period: 600, + }); + }); + + it('should correctly format a nested configuration', () => { + const formData = [ + { category: '', label: 'connect_timeout', value: 10 }, + { category: 'mysql', label: 'default_time_zone', value: '+03:00' }, + ]; + const configurations = [ + { category: 'mysql', label: 'connect_timeout' }, + { category: 'mysql', label: 'default_time_zone' }, + ]; + expect(formatConfigPayload(formData, configurations)).toEqual({ + mysql: { + connect_timeout: 10, + default_time_zone: '+03:00', + }, + }); + }); +}); + +describe('getDefaultConfigValue', () => { + it('should return false for boolean type', () => { + const config: ConfigurationOption = { + category: '', + label: '', + type: 'boolean', + }; + expect(getDefaultConfigValue(config)).toBe(false); + }); + + it('should return first enum value for string with enum', () => { + const config: ConfigurationOption = { + category: '', + enum: ['option1', 'option2'], + label: '', + type: 'string', + }; + expect(getDefaultConfigValue(config)).toBe('option1'); + }); + + it('should return 0 for number type', () => { + const config: ConfigurationOption = { + category: '', + label: '', + type: 'number', + }; + expect(getDefaultConfigValue(config)).toBe(0); + }); + + it('should return 0 for integer type', () => { + const config: ConfigurationOption = { + category: '', + label: '', + type: 'integer', + }; + expect(getDefaultConfigValue(config)).toBe(0); + }); +}); diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 1031c88dbd4..0ef3190b4ba 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -7,6 +7,7 @@ import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; import type { ConfigurationOption } from './DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationSelect'; import type { + ConfigCategoryValues, ConfigurationItem, DatabaseEngine, DatabaseFork, @@ -269,6 +270,54 @@ export const formatConfigValue = (configValue: string) => ? ' - ' : configValue; +/** + * Converts a nested database engine configuration into a flat array of configuration options. + * + * @param allConfigs + * @returns An array of structured configuration options. + */ +export const convertEngineConfigToOptions = ( + allConfigs: DatabaseEngineConfig | undefined +) => { + const options: ConfigurationOption[] = []; + + const processConfig = ( + config: Record< + string, + ConfigurationItem | Record + >, + parentCategory: string = '' + ) => { + for (const key in config) { + const value = config[key] as ConfigurationItem; + if (typeof value === 'object') { + // If it has "type" property, add option to the list + if ('type' in value) { + // If parentCategory is empty, use 'Other' as the category + const category = parentCategory || 'other'; + options.push({ + ...value, + category: category, + enum: value.enum ?? [], + label: key, + type: value.type, + }); + } + // Else, it's a nested category, so recurse + else { + processConfig(value as Record, key); + } + } + } + }; + + if (allConfigs !== undefined) { + processConfig(allConfigs); + } + + return options; +}; + /** * Recursively searches for a configuration item by its key within a nested configuration object. * @@ -281,12 +330,12 @@ export const findConfigItem = ( | Record | ConfigurationItem> | undefined, targetKey: string -): ConfigurationItem | undefined => { +): ConfigurationOption | undefined => { for (const key in configs) { const value = configs[key]; if (key === targetKey) { - return value as ConfigurationItem; + return value as ConfigurationOption; } if (typeof value === 'object' && value !== null) { @@ -321,7 +370,7 @@ export const convertExistingConfigsToArray = ( for (const subKey in value) { const subValue = value[subKey]; - const foundConfig = findConfigItem(allConfigs?.engine_config, subKey); + const foundConfig = findConfigItem(allConfigs, subKey); if (foundConfig) { options.push({ ...foundConfig, @@ -332,7 +381,7 @@ export const convertExistingConfigsToArray = ( } } } else { - const foundConfig = findConfigItem(allConfigs?.engine_config, key); + const foundConfig = findConfigItem(allConfigs, key); if (foundConfig) { options.push({ ...foundConfig, @@ -345,3 +394,68 @@ export const convertExistingConfigsToArray = ( } return options; }; + +/** + * Formats the configuration payload by organizing form data into categorized fields. + * + * @param formData + * @param configurations + * @returns A structured object where configurations are grouped by category. + */ +export const formatConfigPayload = ( + formData: ConfigurationOption[], + configurations: ConfigurationOption[] +) => { + const formattedConfigData: DatabaseInstanceAdvancedConfig = {}; + + configurations.forEach(({ category, label }) => { + // Find the matching config from the formData + const formConfig = formData.find((config) => config.label === label); + + if (formConfig && formConfig.value !== undefined) { + if (category === 'other') { + formattedConfigData[label] = formConfig.value; + } else { + if (!formattedConfigData[category]) { + formattedConfigData[category] = {} as ConfigCategoryValues; + } + (formattedConfigData[category] as ConfigCategoryValues)[label] = + formConfig.value; + } + } + }); + + return formattedConfigData; +}; + +export const isConfigBoolean = (config: ConfigurationOption) => { + return ( + config?.type === 'boolean' || + (Array.isArray(config?.type) && config?.type.includes('boolean')) + ); +}; + +export const isConfigStringWithEnum = (config: ConfigurationOption) => { + return ( + (config?.type === 'string' && config.enum) || + (Array.isArray(config?.type) && + config?.type.includes('string') && + config.enum) + ); +}; + +/** + * Determines the default value for a configuration item based on its type. + * + * @param config - The configuration object + * @returns - The default value for the given configuration + */ +export const getDefaultConfigValue = (config: ConfigurationOption) => { + return isConfigBoolean(config) + ? false + : isConfigStringWithEnum(config) + ? config.enum?.[0] ?? '' + : config?.type === 'number' || config?.type === 'integer' + ? 0 + : ''; +}; diff --git a/packages/validation/src/databases.schema.ts b/packages/validation/src/databases.schema.ts index 60ad4ed0a2b..278f9603d4e 100644 --- a/packages/validation/src/databases.schema.ts +++ b/packages/validation/src/databases.schema.ts @@ -1,4 +1,4 @@ -import { number } from 'yup'; +import { boolean, mixed, number } from 'yup'; import { array, object, string } from 'yup'; const LABEL_MESSAGE = 'Label must be between 3 and 32 characters'; @@ -33,3 +33,120 @@ export const updateDatabaseSchema = object({ .nullable(), type: string().notRequired(), }); + +/** + * Creates a base Yup validator based on the field type. + */ +const createValidator = (key: string, field: any) => { + const fieldTypes = Array.isArray(field.type) ? field.type : [field.type]; + + switch (true) { + case fieldTypes.includes('integer'): + return number().integer(`${key} must be a whole number`); + + case fieldTypes.includes('number'): + return number(); + + case fieldTypes.includes('string'): + return string(); + + case fieldTypes.includes('boolean'): + return boolean(); + + default: + return null; + } +}; + +/** + * Applies validation constraints (min, max, length, pattern) to a Yup validator. + */ +const applyConstraints = (validator: any, key: string, field: any) => { + if (!validator) return null; + + if (field.minimum !== undefined) { + validator = validator.min( + field.minimum, + `${key} must be at least ${field.minimum}` + ); + } + if (field.maximum !== undefined) { + validator = validator.max( + field.maximum, + `${key} must be at most ${field.maximum}` + ); + } + if (field.minLength !== undefined) { + validator = validator.min( + field.minLength, + `${key} must be at least ${field.minLength} characters` + ); + } + if (field.maxLength !== undefined) { + validator = validator.max( + field.maxLength, + `${key} must be at most ${field.maxLength} characters` + ); + } + if (field.pattern) { + let pattern = field.pattern; + if (key === 'default_time_zone') { + pattern = '^(SYSTEM|[+-](0[0-9]|1[0-2]):([0-5][0-9]))$'; + } + validator = validator.matches( + new RegExp(pattern), + `Please ensure that ${key} follows the format ${field.example}` + ); + } + + return validator; +}; + +/** + * Processes a single field from the API configuration and adds it to the schema. + */ +const processField = (schemaShape: Record, field: any) => { + if (!field.label || !field.type) { + return; + } + + const key = field.label; + let validator = createValidator(key, field); + validator = applyConstraints(validator, key, field); + + if (validator) { + schemaShape[key] = object().shape({ value: validator }); + } +}; + +/** + * Main function that creates a Yup validation schema dynamically based on API configurations. + */ +export const createDynamicAdvancedConfigSchema = (allConfigurations: any[]) => { + if (!Array.isArray(allConfigurations) || allConfigurations.length === 0) { + return object().shape({}); + } + + const schemaShape: Record = {}; + + allConfigurations.forEach((field) => processField(schemaShape, field)); + return object().shape({ + configs: array().of( + object({ + label: string().required(), + value: mixed().when('label', (label, schema) => { + if (Array.isArray(label)) { + label = label[0]; + } + + if (typeof label !== 'string' || !schemaShape[label]) { + return schema; + } + + const valueSchema = schemaShape[label]?.fields?.value; + return valueSchema ? valueSchema : schema; + }), + }) + ), + }); +}; From 4383b7def601bc3670f944752bdb64d9d005810d Mon Sep 17 00:00:00 2001 From: mpolotsk-akamai <157619599+mpolotsk-akamai@users.noreply.github.com> Date: Tue, 1 Apr 2025 18:59:35 +0200 Subject: [PATCH 50/84] refactor: [UIE-8626] - DBaaS cleanup: Deprecated Types, DatabaseCreate, DatabaseSummary (#11909) * refactor: [UIE-8626] - DBaaS cleanup (Deprecated Types, DatabaseCreate, DatabaseSummary) * Added changeset: DBaaS: unused functions getDatabaseType, getEngineDatabases, getDatabaseBackup * Added changeset: DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary * refactor: [UIE-8626] - restore previously deleted functions * test: [DBAAS1-1055] - Cypress update db cluster test for dbaas v2 view --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> Co-authored-by: Sakshi Tayal --- .../pr-11909-removed-1742830690604.md | 5 + packages/api-v4/src/databases/types.ts | 4 +- .../pr-11909-removed-1742830913597.md | 5 + .../core/databases/update-database.spec.ts | 202 +++++----- .../cypress/support/constants/databases.ts | 10 - .../DatabaseCreate/DatabaseClusterData.tsx | 9 - .../DatabaseCreate/DatabaseCreate.test.tsx | 61 +-- .../DatabaseCreate/DatabaseCreate.tsx | 81 +--- .../DatabaseCreateAccessControls.tsx | 118 ++---- .../DatabaseCreate/DatabaseEngineSelect.tsx | 12 +- .../DatabaseCreate/DatabaseNodeSelector.tsx | 6 +- .../Databases/DatabaseCreate/utilities.tsx | 27 -- .../DatabaseSummary/DatabaseSummary.test.tsx | 149 +------- .../DatabaseSummary/DatabaseSummary.tsx | 60 +-- ...tabaseSummaryClusterConfiguration.test.tsx | 69 ---- ...abaseSummaryClusterConfigurationLegacy.tsx | 151 -------- ...DatabaseSummaryConnectionDetailsLegacy.tsx | 354 ------------------ .../DatabaseLanding/DatabaseLogo.tsx | 18 +- 18 files changed, 197 insertions(+), 1144 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11909-removed-1742830690604.md create mode 100644 packages/manager/.changeset/pr-11909-removed-1742830913597.md delete mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx delete mode 100644 packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx diff --git a/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md b/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md new file mode 100644 index 00000000000..c1183527bc4 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Removed +--- + +DBaaS: unused functions getDatabaseType, getEngineDatabases, getDatabaseBackup ([#11909](https://github.com/linode/manager/pull/11909)) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 9ca08a223f5..07048a3fb77 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -41,9 +41,9 @@ export type DatabaseStatus = | 'resuming' | 'suspended' | 'suspending'; - +/** @deprecated TODO (UIE-8214) remove after migration */ export type DatabaseBackupType = 'snapshot' | 'auto'; - +/** @deprecated TODO (UIE-8214) remove after migration */ export interface DatabaseBackup { id: number; type: DatabaseBackupType; diff --git a/packages/manager/.changeset/pr-11909-removed-1742830913597.md b/packages/manager/.changeset/pr-11909-removed-1742830913597.md new file mode 100644 index 00000000000..ec908188ceb --- /dev/null +++ b/packages/manager/.changeset/pr-11909-removed-1742830913597.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Removed +--- + +DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary ([#11909](https://github.com/linode/manager/pull/11909)) diff --git a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts index fd998f69319..1ed3176d81b 100644 --- a/packages/manager/cypress/e2e/core/databases/update-database.spec.ts +++ b/packages/manager/cypress/e2e/core/databases/update-database.spec.ts @@ -17,7 +17,6 @@ import { mockUpdateDatabase, mockUpdateProvisioningDatabase, } from 'support/intercepts/databases'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { randomIp, @@ -144,33 +143,45 @@ const resetRootPassword = () => { }); }; -describe('Update database clusters', () => { - beforeEach(() => { - const mockAccount = accountFactory.build({ - capabilities: [ - 'Akamai Cloud Pulse', - 'Block Storage', - 'Cloud Firewall', - 'Disk Encryption', - 'Kubernetes', - 'Linodes', - 'LKE HA Control Planes', - 'Machine Images', - 'Managed Databases', - 'NodeBalancers', - 'Object Storage Access Key Regions', - 'Object Storage Endpoint Types', - 'Object Storage', - 'Placement Group', - 'Vlans', - ], - }); - mockAppendFeatureFlags({ - dbaasV2: { beta: false, enabled: false }, +/** + * Updates engine version if applicable and maintenance window for a given day and time. + * + * This requires that the 'Summary' or 'Settings' tab is currently active. + * + * @param engine - database engine for version upgrade. + * @param version - current database engine version to be upgraded. + */ +const upgradeEngineVersion = (engine: string, version: string) => { + const dbEngine = engine == 'mysql' ? 'MySQL' : 'PostgreSQL'; + cy.get('[data-qa-settings-section="Maintenance"]') + .should('be.visible') + .within(() => { + cy.findByText('Maintenance'); + cy.findByText('Version'); + cy.findByText(`${dbEngine} v${version}`); + ui.button.findByTitle('Upgrade Version').should('be.visible'); }); - mockGetAccount(mockAccount); - }); +}; +/** + * Updates maintenance window for a given day and time. + * + * This requires that the 'Summary' or 'Settings' tab is currently active. + * Assertion is made on the toast thrown while updating maintenance window. + * + * @param label - type of window (day/time) to update + * @param windowValue - maintenance window value to update + */ +const modifyMaintenanceWindow = (label: string, windowValue: string) => { + cy.findByText('Set a Weekly Maintenance Window'); + cy.findByTitle('Save Changes').should('be.visible').should('be.disabled'); + + ui.autocomplete.findByLabel(label).should('be.visible').type(windowValue); + cy.contains(windowValue).should('be.visible').click(); + ui.button.findByTitle('Save Changes').should('be.visible').click(); +}; + +describe('Update database clusters', () => { databaseConfigurations.forEach( (configuration: databaseClusterConfiguration) => { describe(`updates a ${configuration.linodeType} ${configuration.engine} v${configuration.version}.x ${configuration.clusterSize}-node cluster`, () => { @@ -192,12 +203,14 @@ describe('Update database clusters', () => { engine: configuration.dbType, id: randomNumber(1, 1000), label: initialLabel, - platform: 'rdbms-legacy', + platform: 'rdbms-default', region: configuration.region.id, status: 'active', type: configuration.linodeType, + version: configuration.version, }); + mockGetAccount(accountFactory.build()).as('getAccount'); mockGetDatabase(database).as('getDatabase'); mockGetDatabaseTypes(mockDatabaseNodeTypes).as('getDatabaseTypes'); mockResetPassword(database.id, database.engine).as( @@ -212,27 +225,29 @@ describe('Update database clusters', () => { cy.visitWithLogin(`/databases/${database.engine}/${database.id}`); cy.wait(['@getDatabase', '@getDatabaseTypes']); - cy.get('[data-qa-cluster-config]').within(() => { - cy.findByText(configuration.region.label).should('be.visible'); - cy.findByText(database.used_disk_size_gb + ' GB').should( - 'be.visible' - ); - cy.findByText(database.total_disk_size_gb + ' GB').should( - 'be.visible' - ); - }); + cy.findByText('Cluster Configuration'); + cy.findByText(configuration.region.label).should('be.visible'); + cy.findByText(database.total_disk_size_gb + ' GB').should( + 'be.visible' + ); + + cy.findByText('Connection Details'); + // "Show" button should be enabled to reveal password when DB is active. + ui.button + .findByTitle('Show') + .should('be.visible') + .should('be.enabled') + .click(); - cy.get('[data-qa-connection-details]').within(() => { - // "Show" button should be enabled to reveal password when DB is active. - cy.findByText('Show') - .closest('button') - .should('be.visible') - .should('be.enabled') - .click(); + cy.wait('@getCredentials'); + cy.findByText(`${initialPassword}`); - cy.wait('@getCredentials'); - cy.findByText(`= ${initialPassword}`); - }); + // "Hide" button should be enabled to hide password when password is revealed. + ui.button + .findByTitle('Hide') + .should('be.visible') + .should('be.enabled') + .click(); mockUpdateDatabase(database.id, database.engine, { ...database, @@ -244,6 +259,13 @@ describe('Update database clusters', () => { .should('be.visible') .should('have.text', updatedLabel); + // Navigate to "Settings" tab. + ui.tabList.findTabByTitle('Settings').click(); + + // Reset root password. + resetRootPassword(); + cy.wait('@resetRootPassword'); + // Remove allowed IP, manage IP access control. mockUpdateDatabase(database.id, database.engine, { ...database, @@ -262,25 +284,19 @@ describe('Update database clusters', () => { cy.findByText(newAllowedIp).should('be.visible'); }); - // Navigate to "Settings" tab. - ui.tabList.findTabByTitle('Settings').click(); - - // Reset root password. - resetRootPassword(); - cy.wait('@resetRootPassword'); - - // Change maintenance. + // Change maintenance window and databe version upgrade. mockUpdateDatabase(database.id, database.engine, database).as( 'updateDatabaseMaintenance' ); - cy.findByText('Monthly').should('be.visible').click(); + upgradeEngineVersion(database.engine, database.version); - ui.button - .findByTitle('Save Changes') - .should('be.visible') - .should('be.enabled') - .click(); + modifyMaintenanceWindow('Day of Week', 'Wednesday'); + cy.wait('@updateDatabaseMaintenance'); + ui.toast.assertMessage( + 'Maintenance Window settings saved successfully.' + ); + modifyMaintenanceWindow('Time', '12:00'); cy.wait('@updateDatabaseMaintenance'); ui.toast.assertMessage( 'Maintenance Window settings saved successfully.' @@ -308,7 +324,7 @@ describe('Update database clusters', () => { }, id: randomNumber(1, 1000), label: initialLabel, - platform: 'rdbms-legacy', + platform: 'rdbms-default', region: configuration.region.id, status: 'provisioning', type: configuration.linodeType, @@ -346,37 +362,15 @@ describe('Update database clusters', () => { .should('be.enabled') .click(); - cy.get('[data-qa-connection-details]').within(() => { - // DBaaS hostnames are not available until database/cluster has provisioned. - cy.findByText(hostnameRegex).should('be.visible'); + cy.findByText('Connection Details'); + // DBaaS hostnames are not available until database/cluster has provisioned. + cy.findByText(hostnameRegex).should('be.visible'); - // DBaaS passwords cannot be revealed until database/cluster has provisioned. - cy.findByText('Show') - .closest('button') - .should('be.visible') - .should('be.disabled'); - }); - - // Cannot add or remove allowed IPs before database/cluster has provisioned. - removeAllowedIp(allowedIp); - cy.wait('@updateDatabase'); - ui.dialog - .findByTitle(`Remove IP Address ${allowedIp}`) + // DBaaS passwords cannot be revealed until database/cluster has provisioned. + ui.button + .findByTitle('Show') .should('be.visible') - .within(() => { - cy.findByText(errorMessage).should('be.visible'); - ui.buttonGroup - .findButtonByTitle('Cancel') - .should('be.visible') - .click(); - }); - - manageAccessControl([randomIp()], 1); - cy.wait('@updateDatabase'); - ui.drawer.findByTitle('Manage Access').within(() => { - cy.findByText(errorMessage).should('be.visible'); - ui.drawerCloseButton.find().click(); - }); + .should('be.disabled'); // Navigate to "Settings" tab. ui.tabList.findTabByTitle('Settings').click(); @@ -397,15 +391,29 @@ describe('Update database clusters', () => { .click(); }); - // Cannot change maintenance schedule before database/cluster has provisioned. - cy.findByText('Monthly').should('be.visible').click(); - - ui.button - .findByTitle('Save Changes') + // Cannot add or remove allowed IPs before database/cluster has provisioned. + removeAllowedIp(allowedIp); + cy.wait('@updateDatabase'); + ui.dialog + .findByTitle(`Remove IP Address ${allowedIp}`) .should('be.visible') - .should('be.enabled') - .click(); + .within(() => { + cy.findByText(errorMessage).should('be.visible'); + ui.buttonGroup + .findButtonByTitle('Cancel') + .should('be.visible') + .click(); + }); + + manageAccessControl([randomIp()], 1); + cy.wait('@updateDatabase'); + ui.drawer.findByTitle('Manage Access').within(() => { + cy.findByText(errorMessage).should('be.visible'); + ui.drawerCloseButton.find().click(); + }); + // Cannot change maintenance schedule before database/cluster has provisioned. + modifyMaintenanceWindow('Day of Week', 'Wednesday'); cy.wait('@updateDatabase'); cy.findByText(errorMessage).should('be.visible'); }); diff --git a/packages/manager/cypress/support/constants/databases.ts b/packages/manager/cypress/support/constants/databases.ts index e463f331750..e34533ba34d 100644 --- a/packages/manager/cypress/support/constants/databases.ts +++ b/packages/manager/cypress/support/constants/databases.ts @@ -344,16 +344,6 @@ export const databaseConfigurations: databaseClusterConfiguration[] = [ region: chooseRegion({ capabilities: ['Managed Databases'] }), version: '5', }, - // { - // label: randomLabel(), - // linodeType: 'g6-dedicated-16', - // clusterSize: 1, - // dbType: 'mongodb', - // regionTypeahead: 'Atlanta', - // region: 'us-southeast', - // engine: 'MongoDB', - // version: '4', - // }, { clusterSize: 3, dbType: 'postgresql', diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx index ceca672a4a7..19a17b667bd 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseClusterData.tsx @@ -17,10 +17,7 @@ import type { ClusterSize, DatabaseEngine, Engine, - MySQLReplicationType, - PostgresReplicationType, Region, - ReplicationCommitTypes, } from '@linode/api-v4'; import type { FormikErrors } from 'formik'; export interface DatabaseCreateValues { @@ -32,12 +29,6 @@ export interface DatabaseCreateValues { engine: Engine; label: string; region: string; - /** @Deprecated used by rdbms-legacy PostgreSQL only */ - replication_commit_type?: ReplicationCommitTypes; - /** @Deprecated used by rdbms-legacy only */ - replication_type?: MySQLReplicationType | PostgresReplicationType; - /** @Deprecated used by rdbms-legacy only, rdbms-default always uses TLS */ - ssl_connection?: boolean; type: string; } diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx index 56b3df8cec8..d46facb6c46 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.test.tsx @@ -1,4 +1,5 @@ -import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; +import { waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { accountFactory, databaseTypeFactory } from 'src/factories'; @@ -7,7 +8,6 @@ import { HttpResponse, http, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import DatabaseCreate from './DatabaseCreate'; - const loadingTestId = 'circle-progress'; const queryMocks = vi.hoisted(() => ({ @@ -79,7 +79,7 @@ describe('Database Create', () => { // update node pricing if a plan is selected const radioBtn = getAllByText('Nanode 1 GB')[0]; - fireEvent.click(radioBtn); + await userEvent.click(radioBtn); expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); }); @@ -94,59 +94,28 @@ describe('Database Create', () => { }) ); - const { getAllByText, getByTestId } = renderWithTheme(, { - // Mock route history so the Plan Selection table displays prices without requiring a region in the DB Create flow. - MemoryRouter: { initialEntries: ['/databases/create'] }, - }); - - await waitForElementToBeRemoved(getByTestId(loadingTestId)); - - // default to $0 if no plan is selected - const nodeRadioBtns = getByTestId('database-nodes'); - expect(nodeRadioBtns).toHaveTextContent('$0/month $0/hr'); - - // update node pricing if a plan is selected - const radioBtn = getAllByText('Nanode 1 GB')[0]; - fireEvent.click(radioBtn); - expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); - expect(nodeRadioBtns).not.toHaveTextContent('$100/month $0.15/hr'); - expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); - }); - - it('should display the correct nodes for account with Managed Databases V2', async () => { - server.use( - http.get('*/account', () => { - const account = accountFactory.build({ - capabilities: ['Managed Databases Beta'], - }); - return HttpResponse.json(account); - }) + const { getAllByRole, getAllByText, getByTestId } = renderWithTheme( + , + { + // Mock route history so the Plan Selection table displays prices without requiring a region in the DB Create flow. + MemoryRouter: { initialEntries: ['/databases/create'] }, + } ); - const flags = { - dbaasV2: { - beta: true, - enabled: true, - }, - }; - - const { getAllByText, getByTestId } = renderWithTheme(, { - // Mock route history so the Plan Selection table displays prices without requiring a region in the DB Create flow. - MemoryRouter: { initialEntries: ['/databases/create'] }, - flags, - }); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const sharedTab = getAllByRole('tab')[1]; + await userEvent.click(sharedTab); // default to $0 if no plan is selected const nodeRadioBtns = getByTestId('database-nodes'); expect(nodeRadioBtns).toHaveTextContent('$0/month $0/hr'); // update node pricing if a plan is selected - const radioBtn = getAllByText('Nanode 1 GB')[0]; - fireEvent.click(radioBtn); + const radioBtn = getAllByText('Linode 2 GB')[0]; + + await userEvent.click(radioBtn); expect(nodeRadioBtns).toHaveTextContent('$60/month $0.09/hr'); - expect(nodeRadioBtns).toHaveTextContent('$100/month $0.15/hr'); + expect(nodeRadioBtns).not.toHaveTextContent('$100/month $0.15/hr'); expect(nodeRadioBtns).toHaveTextContent('$140/month $0.21/hr'); }); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 6ddb8c98abd..53ed7c76b53 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -1,12 +1,5 @@ import { useRegionsQuery } from '@linode/queries'; -import { - BetaChip, - CircleProgress, - Divider, - ErrorState, - Notice, - Paper, -} from '@linode/ui'; +import { CircleProgress, Divider, ErrorState, Notice, Paper } from '@linode/ui'; import { formatStorageUnits, scrollErrorIntoViewV2 } from '@linode/utilities'; import { createDatabaseSchema } from '@linode/validation/lib/databases.schema'; import Grid from '@mui/material/Grid2'; @@ -29,7 +22,6 @@ import { import { DatabaseNodeSelector } from 'src/features/Databases/DatabaseCreate/DatabaseNodeSelector'; import { DatabaseSummarySection } from 'src/features/Databases/DatabaseCreate/DatabaseSummarySection'; import { DatabaseLogo } from 'src/features/Databases/DatabaseLanding/DatabaseLogo'; -import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import { enforceIPMasks } from 'src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils'; import { typeLabelDetails } from 'src/features/Linodes/presentation'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; @@ -41,19 +33,11 @@ import { import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { validateIPs } from 'src/utilities/ipUtils'; -import { - ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT, - ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT_LEGACY, -} from '../constants'; +import { ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT } from '../constants'; import { DatabaseCreateAccessControls } from './DatabaseCreateAccessControls'; -import { - determineReplicationCommitType, - determineReplicationType, -} from './utilities'; import type { ClusterSize, - ComprehensiveReplicationType, CreateDatabasePayload, Engine, } from '@linode/api-v4/lib/databases/types'; @@ -64,11 +48,6 @@ import type { ExtendedIP } from 'src/utilities/ipUtils'; const DatabaseCreate = () => { const history = useHistory(); - const { - isDatabasesV2Beta, - isDatabasesV2Enabled, - isDatabasesV2GA, - } = useIsDatabasesEnabled(); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_databases', }); @@ -89,7 +68,7 @@ const DatabaseCreate = () => { error: typesError, isLoading: typesLoading, } = useDatabaseTypesQuery({ - platform: isDatabasesV2Enabled ? 'rdbms-default' : 'rdbms-legacy', + platform: 'rdbms-default', }); const formRef = React.useRef(null); @@ -107,9 +86,7 @@ const DatabaseCreate = () => { const handleIPValidation = () => { const validatedIps = validateIPs(values.allow_list, { allowEmptyAddress: true, - errorMessage: isDatabasesV2GA - ? ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT - : ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT_LEGACY, + errorMessage: ACCESS_CONTROLS_IP_VALIDATION_ERROR_TEXT, }); if (validatedIps.some((ip) => ip.error)) { @@ -176,13 +153,6 @@ const DatabaseCreate = () => { type: '', }; - if (!isDatabasesV2Enabled) { - // TODO (UIE-8214) remove POST GA - initialValues.replication_commit_type = undefined; // specific to Postgres - initialValues.replication_type = 'none' as ComprehensiveReplicationType; - initialValues.ssl_connection = true; - } - const { errors, handleSubmit, @@ -208,19 +178,8 @@ const DatabaseCreate = () => { 'cluster_size', values.cluster_size < 1 ? 3 : values.cluster_size ); - if (!isDatabasesV2Enabled) { - // TODO (UIE-8214) remove POST GA - setFieldValue( - 'replication_type', - determineReplicationType(values.cluster_size, values.engine) - ); - setFieldValue( - 'replication_commit_type', - determineReplicationCommitType(values.engine) - ); - } } - }, [setFieldValue, values.cluster_size, values.engine, isDatabasesV2Enabled]); + }, [setFieldValue, values.cluster_size, values.engine]); const selectedEngine = values.engine.split('/')[0] as Engine; @@ -272,10 +231,6 @@ const DatabaseCreate = () => { const handleNodeChange = (size: ClusterSize | undefined) => { setFieldValue('cluster_size', size); - if (!isDatabasesV2Enabled) { - // TODO (UIE-8214) remove POST GA - setFieldValue('replication_type', size === 1 ? 'none' : 'semi_synch'); - } }; return ( <> @@ -289,14 +244,6 @@ const DatabaseCreate = () => { position: 1, }, ], - labelOptions: { - suffixComponent: isDatabasesV2Beta ? ( - - ) : null, - }, pathname: location.pathname, }} title="Create" @@ -371,15 +318,13 @@ const DatabaseCreate = () => { onChange={(ips: ExtendedIP[]) => setFieldValue('allow_list', ips)} /> - {isDatabasesV2GA && ( - - - - )} + + + Your database node(s) will take approximately 15-30 minutes to @@ -394,7 +339,7 @@ const DatabaseCreate = () => { Create Database Cluster - {isDatabasesV2Enabled && } + ); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx index cd0883c22d5..35df59df9b9 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreateAccessControls.tsx @@ -12,12 +12,7 @@ import { makeStyles } from 'tss-react/mui'; import { Link } from 'src/components/Link'; import { MultipleIPInput } from 'src/components/MultipleIPInput/MultipleIPInput'; -import { - ipFieldPlaceholder, - ipV6FieldPlaceholder, -} from 'src/utilities/ipUtils'; - -import { useIsDatabasesEnabled } from '../utilities'; +import { ipV6FieldPlaceholder } from 'src/utilities/ipUtils'; import type { APIError } from '@linode/api-v4/lib/types'; import type { Theme } from '@mui/material/styles'; @@ -54,7 +49,6 @@ export const DatabaseCreateAccessControls = (props: Props) => { const { disabled = false, errors, ips, onBlur, onChange } = props; const { classes } = useStyles(); const [accessOption, setAccessOption] = useState('specific'); - const { isDatabasesV2GA } = useIsDatabasesEnabled(); const handleAccessOptionChange = (_: ChangeEvent, value: AccessOption) => { setAccessOption(value); @@ -68,40 +62,19 @@ export const DatabaseCreateAccessControls = (props: Props) => { Manage Access - {isDatabasesV2GA ? ( - <> - - Add IPv6 (recommended) or IPv4 addresses or ranges that should be - authorized to access this cluster.{' '} - - Learn more - - . - - - (Note: You can modify access controls after your database cluster is - active.) - - - ) : ( - <> - - Add any IPv4 address or range that should be authorized to access - this cluster. - - - By default, all public and private connections are denied.{' '} - - Learn more - - . - - - You can add or modify access controls after your database cluster is - active.{' '} - - - )} + + Add IPv6 (recommended) or IPv4 addresses or ranges that should be + authorized to access this cluster.{' '} + + Learn more + + . + + + (Note: You can modify access controls after your database cluster is + active.) + + {errors && errors.map((apiError: APIError) => ( @@ -111,48 +84,37 @@ export const DatabaseCreateAccessControls = (props: Props) => { variant="error" /> ))} - {isDatabasesV2GA ? ( - - } - data-qa-dbaas-radio="Specific" - disabled={disabled} - label="Specific Access (recommended)" - value="specific" - /> - 1 ? 'Add Another IP' : 'Add an IP'} - className={classes.multipleIPInput} - disabled={accessOption === 'none' || disabled} - ips={ips} - onBlur={onBlur} - onChange={onChange} - placeholder={ipV6FieldPlaceholder} - title="Allowed IP Addresses or Ranges" - /> - } - data-qa-dbaas-radio="None" - disabled={disabled} - label="No Access (Deny connections from all IP addresses)" - value="none" - /> - - ) : ( - + } + data-qa-dbaas-radio="Specific" disabled={disabled} + label="Specific Access (recommended)" + value="specific" + /> + 1 ? 'Add Another IP' : 'Add an IP'} + className={classes.multipleIPInput} + disabled={accessOption === 'none' || disabled} ips={ips} onBlur={onBlur} onChange={onChange} - placeholder={ipFieldPlaceholder} - title="Allowed IP Address(es) or Range(s)" + placeholder={ipV6FieldPlaceholder} + title="Allowed IP Addresses or Ranges" + /> + } + data-qa-dbaas-radio="None" + disabled={disabled} + label="No Access (Deny connections from all IP addresses)" + value="none" /> - )} + ); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx index 49e981702f6..d9269dbf0af 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseEngineSelect.tsx @@ -40,12 +40,6 @@ export const DatabaseEngineSelect = (props: Props) => { if (option.engine.match(/postgresql/i)) { return 'PostgreSQL'; } - if (option.engine.match(/mongodb/i)) { - return 'MongoDB'; - } - if (option.engine.match(/redis/i)) { - return 'Redis'; - } return 'Other'; }} onChange={(_, selected) => { @@ -56,13 +50,13 @@ export const DatabaseEngineSelect = (props: Props) => { return (
  • {option.flag} {option.label} diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseNodeSelector.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseNodeSelector.tsx index 0f3e4851b25..79e47c32023 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseNodeSelector.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseNodeSelector.tsx @@ -12,8 +12,6 @@ import { StyledChip } from 'src/features/components/PlansPanel/PlanSelection.sty import { determineInitialPlanCategoryTab } from 'src/features/components/PlansPanel/utils'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useIsDatabasesEnabled } from '../utilities'; - import type { ClusterSize, DatabaseClusterSizeObject, @@ -55,7 +53,6 @@ export const DatabaseNodeSelector = (props: Props) => { selectedTab, } = props; - const { isDatabasesV2Enabled } = useIsDatabasesEnabled(); const isRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_databases', }); @@ -107,7 +104,7 @@ export const DatabaseNodeSelector = (props: Props) => { }, ]; - if (hasDedicated && selectedTab === 0 && isDatabasesV2Enabled) { + if (hasDedicated && selectedTab === 0) { options.push({ label: ( @@ -146,7 +143,6 @@ export const DatabaseNodeSelector = (props: Props) => { selectedTab, nodePricing, displayTypes, - isDatabasesV2Enabled, currentClusterSize, selectedClusterSize, ]); diff --git a/packages/manager/src/features/Databases/DatabaseCreate/utilities.tsx b/packages/manager/src/features/Databases/DatabaseCreate/utilities.tsx index d40a57c1516..957a603f489 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/utilities.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/utilities.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import MongoDBIcon from 'src/assets/icons/mongodb.svg'; import MySQLIcon from 'src/assets/icons/mysql.svg'; import PostgreSQLIcon from 'src/assets/icons/postgresql.svg'; import { getDatabasesDescription } from 'src/features/Databases/utilities'; @@ -11,10 +10,6 @@ export const determineReplicationType = ( clusterSize: number, engine: string ) => { - if (/mongo/.test(engine)) { - return undefined; - } - // If engine is a MySQL or Postgres one and it's a standalone DB instance if (clusterSize === 1) { return 'none'; @@ -37,35 +32,13 @@ export const determineReplicationCommitType = (engine: string) => { return undefined; }; -export const determineStorageEngine = (engine: string) => { - // 'wiredtiger' is the default. - if (/mongo/.test(engine)) { - return 'wiredtiger'; - } - - return undefined; -}; - -export const determineCompressionType = (engine: string) => { - // 'none' is the default. - if (/mongo/.test(engine)) { - return 'none'; - } - - return undefined; -}; - interface EngineIconsProps { - mongodb: React.JSX.Element; mysql: React.JSX.Element; postgresql: React.JSX.Element; - redis: null; } export const engineIcons: EngineIconsProps = { - mongodb: , mysql: , postgresql: , - redis: null, }; export const getEngineOptions = (engines: DatabaseEngine[]) => { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx index 859fffcd0e9..5f6e0ab7026 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.test.tsx @@ -11,14 +11,12 @@ import { DatabaseSummary } from './DatabaseSummary'; import type { Database } from '@linode/api-v4'; const CLUSTER_CONFIGURATION = 'Cluster Configuration'; -const THREE_NODE = 'Primary (+2 Nodes)'; + const TWO_NODE = 'Primary (+1 Node)'; const VERSION = 'Version'; const CONNECTION_DETAILS = 'Connection Details'; const PRIVATE_NETWORK_HOST = 'Private Network Host'; -const PRIVATE_NETWORK_HOST_LABEL = 'private network host'; -const READONLY_HOST_LABEL = 'read-only host'; const GA_READONLY_HOST_LABEL = 'Read-only Host'; const ACCESS_CONTROLS = 'Access Controls'; @@ -27,12 +25,6 @@ const DEFAULT_PLATFORM = 'rdbms-default'; const DEFAULT_PRIMARY = 'db-mysql-default-primary.net'; const DEFAULT_STANDBY = 'db-mysql-default-standby.net'; -const LEGACY_PLATFORM = 'rdbms-legacy'; -const LEGACY_PRIMARY = 'db-mysql-legacy-primary.net'; -const LEGACY_SECONDARY = 'db-mysql-legacy-secondary.net'; - -const BUTTON_ACCESS_CONTROLS = 'button-access-control'; - const spy = vi.spyOn(utils, 'useIsDatabasesEnabled'); spy.mockReturnValue({ isDatabasesEnabled: true, @@ -71,143 +63,4 @@ describe('Database Summary', () => { expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(0); }); }); - - it('should render V2GA view legacy db', async () => { - const database = databaseFactory.build({ - cluster_size: 3, - hosts: { - primary: LEGACY_PRIMARY, - secondary: LEGACY_SECONDARY, - }, - platform: LEGACY_PLATFORM, - }) as Database; - - const { queryAllByText } = renderWithTheme( - - ); - - await waitFor(() => { - expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); - expect(queryAllByText(THREE_NODE)).toHaveLength(1); - expect(queryAllByText(VERSION)).toHaveLength(0); - - expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); - expect(queryAllByText(PRIVATE_NETWORK_HOST)).toHaveLength(1); - expect(queryAllByText(GA_READONLY_HOST_LABEL)).toHaveLength(0); - expect(queryAllByText(LEGACY_SECONDARY)).toHaveLength(1); - - expect(queryAllByText(ACCESS_CONTROLS)).toHaveLength(0); - }); - }); - - it('should render Beta view default db', async () => { - spy.mockReturnValue({ - isDatabasesEnabled: true, - isDatabasesV2Beta: true, - isDatabasesV2Enabled: true, - isDatabasesV2GA: false, - isUserExistingBeta: true, - isUserNewBeta: false, - }); - const database = databaseFactory.build({ - cluster_size: 2, - hosts: { - primary: DEFAULT_PRIMARY, - secondary: undefined, - standby: DEFAULT_STANDBY, - }, - platform: DEFAULT_PLATFORM, - }) as Database; - - const { getByTestId, queryAllByText } = renderWithTheme( - - ); - - await waitFor(() => { - expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); - expect(queryAllByText(TWO_NODE)).toHaveLength(1); - expect(queryAllByText(VERSION)).toHaveLength(1); - - expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); - expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(0); - expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(1); - expect(queryAllByText(/db-mysql-default-standby.net/)).toHaveLength(1); - - expect(getByTestId(BUTTON_ACCESS_CONTROLS)).toBeInTheDocument(); - }); - }); - - it('should render Beta view legacy db', async () => { - spy.mockReturnValue({ - isDatabasesEnabled: true, - isDatabasesV2Beta: true, - isDatabasesV2Enabled: true, - isDatabasesV2GA: false, - isUserExistingBeta: true, - isUserNewBeta: false, - }); - const database = databaseFactory.build({ - cluster_size: 3, - hosts: { - primary: LEGACY_PRIMARY, - secondary: LEGACY_SECONDARY, - standby: undefined, - }, - platform: LEGACY_PLATFORM, - }) as Database; - - const { getByTestId, queryAllByText } = renderWithTheme( - - ); - - await waitFor(() => { - expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); - expect(queryAllByText(THREE_NODE)).toHaveLength(1); - expect(queryAllByText(VERSION)).toHaveLength(1); - - expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); - expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(1); - expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(0); - expect(queryAllByText(/db-mysql-legacy-secondary.net/)).toHaveLength(1); - - expect(getByTestId(BUTTON_ACCESS_CONTROLS)).toBeInTheDocument(); - }); - }); - - it('should render V1 view legacy db', async () => { - spy.mockReturnValue({ - isDatabasesEnabled: true, - isDatabasesV2Beta: false, - isDatabasesV2Enabled: false, - isDatabasesV2GA: false, - isUserExistingBeta: false, - isUserNewBeta: false, - }); - const database = databaseFactory.build({ - cluster_size: 3, - hosts: { - primary: LEGACY_PRIMARY, - secondary: LEGACY_SECONDARY, - standby: undefined, - }, - platform: LEGACY_PLATFORM, - }) as Database; - - const { getByTestId, queryAllByText } = renderWithTheme( - - ); - - await waitFor(() => { - expect(queryAllByText(CLUSTER_CONFIGURATION)).toHaveLength(1); - expect(queryAllByText(THREE_NODE)).toHaveLength(1); - expect(queryAllByText(VERSION)).toHaveLength(1); - - expect(queryAllByText(CONNECTION_DETAILS)).toHaveLength(1); - expect(queryAllByText(PRIVATE_NETWORK_HOST_LABEL)).toHaveLength(1); - expect(queryAllByText(READONLY_HOST_LABEL)).toHaveLength(0); - expect(queryAllByText(/db-mysql-legacy-secondary.net/)).toHaveLength(1); - - expect(getByTestId(BUTTON_ACCESS_CONTROLS)).toBeInTheDocument(); - }); - }); }); diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx index 97a8f09d416..c97e81f0d26 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummary.tsx @@ -1,14 +1,9 @@ -import { Divider, Paper, Typography } from '@linode/ui'; +import { Paper } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import * as React from 'react'; -import { Link } from 'src/components/Link'; -import AccessControls from 'src/features/Databases/DatabaseDetail/AccessControls'; import ClusterConfiguration from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration'; import ConnectionDetails from 'src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryConnectionDetails'; -import ClusterConfigurationLegacy from 'src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy'; -import ConnectionDetailsLegacy from 'src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy'; -import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import type { Database } from '@linode/api-v4/lib/databases/types'; @@ -18,71 +13,28 @@ interface Props { } export const DatabaseSummary: React.FC = (props) => { - const { database, disabled } = props; - const { isDatabasesV2GA } = useIsDatabasesEnabled(); - - const description = ( - <> - - Add IPv4 addresses or ranges that should be authorized to access this - cluster. All other public and private connections are denied.{' '} - - Learn more - - . - - - You can add or modify access controls after your database cluster is - active. - - - ); + const { database } = props; return ( - {isDatabasesV2GA ? ( - - ) : ( - // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 - // TODO (UIE-8214) remove POST GA - - )} + - {isDatabasesV2GA ? ( - - ) : ( - // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 - // TODO (UIE-8214) remove POST GA - - )} + - {!isDatabasesV2GA && ( - // Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 - // AccessControls accessible through dropdown menu on landing page table and on settings tab - // TODO (UIE-8214) remove POST GA - <> - - - - )} ); }; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx index a27429a7f47..588b6f5fe1e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/DatabaseSummaryClusterConfiguration.test.tsx @@ -105,75 +105,6 @@ describe('DatabaseSummaryClusterConfiguration', () => { }); }); - it('should display correctly for legacy db', async () => { - queryMocks.useRegionsQuery.mockReturnValue({ - data: regionFactory.buildList(1, { - country: 'us', - id: 'us-southeast', - label: 'Atlanta, GA, USA', - status: 'ok', - }), - }); - - queryMocks.useDatabaseTypesQuery.mockReturnValue({ - data: databaseTypeFactory.buildList(1, { - class: 'nanode', - disk: 25600, - id: 'g6-nanode-1', - label: 'DBaaS - Nanode 1GB', - memory: 1024, - vcpus: 1, - }), - }); - - const database = databaseFactory.build({ - cluster_size: 1, - engine: 'mysql', - platform: 'rdbms-legacy', - region: 'us-southeast', - replication_type: 'none', - status: 'provisioning', - total_disk_size_gb: 15, - type: 'g6-nanode-1', - used_disk_size_gb: 2, - version: '8.0.30', - }) as Database; - - const { queryAllByText } = renderWithTheme( - - ); - - expect(queryMocks.useDatabaseTypesQuery).toHaveBeenCalledWith({ - platform: 'rdbms-legacy', - }); - - await waitFor(() => { - expect(queryAllByText('Status')).toHaveLength(1); - expect(queryAllByText('Provisioning')).toHaveLength(1); - - expect(queryAllByText('Plan')).toHaveLength(1); - expect(queryAllByText('Nanode 1 GB')).toHaveLength(1); - - expect(queryAllByText('Nodes')).toHaveLength(1); - expect(queryAllByText('Primary (1 Node)')).toHaveLength(1); - - expect(queryAllByText('CPUs')).toHaveLength(1); - expect(queryAllByText(1)).toHaveLength(1); - - expect(queryAllByText('Engine')).toHaveLength(1); - expect(queryAllByText('MySQL v8.0.30')).toHaveLength(1); - - expect(queryAllByText('Region')).toHaveLength(1); - expect(queryAllByText('Atlanta, GA, USA')).toHaveLength(1); - - expect(queryAllByText('RAM')).toHaveLength(1); - expect(queryAllByText('1 GB')).toHaveLength(1); - - expect(queryAllByText('Total Disk Size')).toHaveLength(1); - expect(queryAllByText('15 GB')).toHaveLength(1); - }); - }); - it('should return null when there is no matching type', async () => { queryMocks.useDatabaseTypesQuery.mockReturnValue({ data: databaseTypeFactory.buildList(1, { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx deleted file mode 100644 index 554f51d0eb2..00000000000 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryClusterConfigurationLegacy.tsx +++ /dev/null @@ -1,151 +0,0 @@ -import { useRegionsQuery } from '@linode/queries'; -import { Box, TooltipIcon, Typography } from '@linode/ui'; -import { convertMegabytesTo, formatStorageUnits } from '@linode/utilities'; -import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; - -import { DatabaseStatusDisplay } from 'src/features/Databases/DatabaseDetail/DatabaseStatusDisplay'; -import { DatabaseEngineVersion } from 'src/features/Databases/DatabaseEngineVersion'; -import { useDatabaseTypesQuery } from 'src/queries/databases/databases'; -import { useInProgressEvents } from 'src/queries/events/events'; - -import type { Region } from '@linode/api-v4'; -import type { - Database, - DatabaseType, -} from '@linode/api-v4/lib/databases/types'; -import type { Theme } from '@mui/material/styles'; - -const useStyles = makeStyles()((theme: Theme) => ({ - configs: { - fontSize: '0.875rem', - lineHeight: '22px', - }, - header: { - marginBottom: theme.spacing(2), - }, - label: { - font: theme.font.bold, - lineHeight: '22px', - width: theme.spacing(13), - }, - status: { - alignItems: 'center', - display: 'flex', - textTransform: 'capitalize', - }, -})); - -interface Props { - database: Database; -} - -/** - * Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 - * TODO (UIE-8214) remove POST GA - */ -export const DatabaseSummaryClusterConfigurationLegacy = (props: Props) => { - const { classes } = useStyles(); - const { database } = props; - - const { data: types } = useDatabaseTypesQuery({ - platform: database.platform, - }); - - const type = types?.find((type: DatabaseType) => type.id === database?.type); - - const { data: regions } = useRegionsQuery(); - - const region = regions?.find((r: Region) => r.id === database.region); - - const { data: events } = useInProgressEvents(); - - if (!database || !type) { - return null; - } - - const configuration = - database.cluster_size === 1 - ? 'Primary (1 Node)' - : database.cluster_size > 2 - ? `Primary (+${database.cluster_size - 1} Nodes)` - : `Primary (+${database.cluster_size - 1} Node)`; - - const sxTooltipIcon = { - marginLeft: '4px', - padding: '0px', - }; - - const STORAGE_COPY = - 'The total disk size is smaller than the selected plan capacity due to overhead from the OS.'; - - return ( - <> - - Cluster Configuration - -
    - - Status -
    - -
    -
    - - Version - - - - Nodes - {configuration} - - - Region - {region?.label ?? database.region} - - - Plan - {formatStorageUnits(type.label)} - - - RAM - {type.memory / 1024} GB - - - CPUs - {type.vcpus} - - {database.total_disk_size_gb ? ( - <> - - Total Disk Size - {database.total_disk_size_gb} GB - - - - Used - {database.used_disk_size_gb} GB - - - ) : ( - - Storage - {convertMegabytesTo(type.disk, true)} - - )} -
    - - ); -}; - -export default DatabaseSummaryClusterConfigurationLegacy; diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx deleted file mode 100644 index af6add09add..00000000000 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseSummary/legacy/DatabaseSummaryConnectionDetailsLegacy.tsx +++ /dev/null @@ -1,354 +0,0 @@ -import { getSSLFields } from '@linode/api-v4/lib/databases/databases'; -import { - Box, - Button, - CircleProgress, - TooltipIcon, - Typography, -} from '@linode/ui'; -import { downloadFile } from '@linode/utilities'; -import { useTheme } from '@mui/material'; -import { useSnackbar } from 'notistack'; -import * as React from 'react'; -import { makeStyles } from 'tss-react/mui'; - -import DownloadIcon from 'src/assets/icons/lke-download.svg'; -import { CopyTooltip } from 'src/components/CopyTooltip/CopyTooltip'; -import { DB_ROOT_USERNAME } from 'src/constants'; -import { useDatabaseCredentialsQuery } from 'src/queries/databases/databases'; -import { getErrorStringOrDefault } from 'src/utilities/errorUtils'; - -import type { Database, SSLFields } from '@linode/api-v4/lib/databases/types'; -import type { Theme } from '@mui/material/styles'; - -const useStyles = makeStyles()((theme: Theme) => ({ - actionBtnsCtn: { - display: 'flex', - justifyContent: 'flex-end', - marginTop: '10px', - padding: `${theme.spacing(1)} 0`, - }, - caCertBtn: { - '& svg': { - marginRight: theme.spacing(), - }, - '&:hover': { - backgroundColor: 'transparent', - opacity: 0.7, - }, - '&[disabled]': { - '& g': { - stroke: theme.tokens.color.Neutrals[30], - }, - '&:hover': { - backgroundColor: 'inherit', - textDecoration: 'none', - }, - // Override disabled background color defined for dark mode - backgroundColor: 'transparent', - color: theme.tokens.color.Neutrals[30], - cursor: 'default', - }, - color: theme.palette.primary.main, - font: theme.font.bold, - fontSize: '0.875rem', - lineHeight: '1.125rem', - marginLeft: theme.spacing(), - minHeight: 'auto', - minWidth: 'auto', - padding: 0, - }, - connectionDetailsCtn: { - '& p': { - lineHeight: '1.5rem', - }, - '& span': { - font: theme.font.bold, - }, - background: theme.bg.bgAccessRowTransparentGradient, - border: `1px solid ${ - theme.name === 'light' - ? theme.tokens.color.Neutrals[40] - : theme.tokens.color.Neutrals.Black - }`, - padding: '8px 15px', - }, - copyToolTip: { - '& svg': { - color: theme.palette.primary.main, - height: `16px !important`, - width: `16px !important`, - }, - marginRight: 12, - }, - error: { - color: theme.color.red, - marginLeft: theme.spacing(2), - }, - header: { - marginBottom: theme.spacing(2), - }, - inlineCopyToolTip: { - '& svg': { - height: `16px`, - width: `16px`, - }, - '&:hover': { - backgroundColor: 'transparent', - }, - display: 'inline-flex', - marginLeft: 4, - }, - progressCtn: { - '& circle': { - stroke: theme.palette.primary.main, - }, - alignSelf: 'flex-end', - marginBottom: 2, - marginLeft: 22, - }, - provisioningText: { - font: theme.font.normal, - fontStyle: 'italic', - }, - showBtn: { - color: theme.palette.primary.main, - fontSize: '0.875rem', - marginLeft: theme.spacing(), - minHeight: 'auto', - minWidth: 'auto', - padding: 0, - }, -})); - -interface Props { - database: Database; -} - -const sxTooltipIcon = { - marginLeft: '4px', - padding: '0px', -}; - -const privateHostCopy = - 'A private network host and a private IP can only be used to access a Database Cluster from Linodes in the same data center and will not incur transfer costs.'; - -/** - * Deprecated @since DBaaS V2 GA. Will be removed remove post GA release ~ Dec 2024 - * TODO (UIE-8214) remove POST GA - */ -export const DatabaseSummaryConnectionDetailsLegacy = (props: Props) => { - const { database } = props; - const { classes } = useStyles(); - const theme = useTheme(); - const { enqueueSnackbar } = useSnackbar(); - - const [showCredentials, setShowPassword] = React.useState(false); - const [isCACertDownloading, setIsCACertDownloading] = React.useState( - false - ); - - const { - data: credentials, - error: credentialsError, - isLoading: credentialsLoading, - refetch: getDatabaseCredentials, - } = useDatabaseCredentialsQuery(database.engine, database.id); - - const username = - database.platform === 'rdbms-default' - ? 'akmadmin' - : database.engine === 'postgresql' - ? 'linpostgres' - : DB_ROOT_USERNAME; - - const password = - showCredentials && credentials ? credentials?.password : '••••••••••'; - - const handleShowPasswordClick = () => { - setShowPassword((showCredentials) => !showCredentials); - }; - - React.useEffect(() => { - if (showCredentials && !credentials) { - getDatabaseCredentials(); - } - }, [credentials, getDatabaseCredentials, showCredentials]); - - const handleDownloadCACertificate = () => { - setIsCACertDownloading(true); - getSSLFields(database.engine, database.id) - .then((response: SSLFields) => { - // Convert to utf-8 from base64 - try { - const decodedFile = window.atob(response.ca_certificate); - downloadFile(`${database.label}-ca-certificate.crt`, decodedFile); - setIsCACertDownloading(false); - } catch (e) { - enqueueSnackbar('Error parsing your CA Certificate file', { - variant: 'error', - }); - setIsCACertDownloading(false); - return; - } - }) - .catch((errorResponse: any) => { - const error = getErrorStringOrDefault( - errorResponse, - 'Unable to download your CA Certificate' - ); - setIsCACertDownloading(false); - enqueueSnackbar(error, { variant: 'error' }); - }); - }; - - const disableShowBtn = ['failed', 'provisioning'].includes(database.status); - const disableDownloadCACertificateBtn = database.status === 'provisioning'; - const readOnlyHost = database?.hosts?.standby || database?.hosts?.secondary; - - const credentialsBtn = (handleClick: () => void, btnText: string) => { - return ( - - ); - }; - - const caCertificateJSX = ( - <> - - {disableDownloadCACertificateBtn && ( - - - - )} - - ); - - return ( - <> - - Connection Details - - - - username = {username} - - - - password = {password} - - {showCredentials && credentialsLoading ? ( -
    - -
    - ) : credentialsError ? ( - <> - - Error retrieving credentials. - - {credentialsBtn(() => getDatabaseCredentials(), 'Retry')} - - ) : ( - credentialsBtn( - handleShowPasswordClick, - showCredentials && credentials ? 'Hide' : 'Show' - ) - )} - {disableShowBtn && ( - - )} - {showCredentials && credentials && ( - - )} -
    - - - {database.hosts?.primary ? ( - <> - - host ={' '} - - {database.hosts?.primary} - {' '} - - - - ) : ( - - host ={' '} - - Your hostname will appear here once it is available. - - - )} - - - {readOnlyHost && ( - - - {database.platform === 'rdbms-default' ? ( - read-only host - ) : ( - private network host - )} - = {readOnlyHost} - - - {database.platform === 'rdbms-legacy' && ( - - )} - - )} - - port = {database.port} - - - ssl = {database.ssl_connection ? 'ENABLED' : 'DISABLED'} - -
    -
    - {database.ssl_connection ? caCertificateJSX : null} -
    - - ); -}; - -export default DatabaseSummaryConnectionDetailsLegacy; diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx index 85f20563d79..858e3124b4c 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx @@ -1,10 +1,9 @@ -import { BetaChip, Box, Typography } from '@linode/ui'; +import { Box, Typography } from '@linode/ui'; import { useTheme } from '@mui/material/styles'; import * as React from 'react'; import LogoWhite from 'src/assets/icons/db-logo-white.svg'; import Logo from 'src/assets/icons/db-logo.svg'; -import { useIsDatabasesEnabled } from 'src/features/Databases/utilities'; import type { SxProps, Theme } from '@mui/material/styles'; @@ -15,7 +14,6 @@ interface Props { export const DatabaseLogo = ({ sx }: Props) => { const theme = useTheme(); - const { isDatabasesV2GA } = useIsDatabasesEnabled(); return ( { sx={sx ? sx : { margin: '20px' }} > - {!isDatabasesV2GA && ( - - )} From 0669da790dbaf91a9d5a661656fe293f66829d8d Mon Sep 17 00:00:00 2001 From: jdamore-linode <97627410+jdamore-linode@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:08:58 -0400 Subject: [PATCH 51/84] test: [M3-9488] - Allow Cypress Volume tests to pass against other environments (#11939) * Remove trailing slash from API-v4 Linode GET request, fix Linode GET intercept path to match only on expected requests * Refactor BSE Volume attach Linode reboot notice tests into their own file, use mocks * Create Volume with region selected via `chooseRegion` util * Fix Volume upgrade tests in other envs by fixing mock region handling * Fix Volume create smoke test failures against alt envs by improving region handling * Added changeset: Allow Cypress Volume tests to pass against alternative environments * Added changeset: Remove trailing slash from outgoing Linode API GET request --- .../pr-11939-fixed-1743535108177.md | 5 + packages/api-v4/src/linodes/linodes.ts | 2 +- .../pr-11939-tests-1743444706872.md | 5 + .../smoke-linode-landing-table.spec.ts | 2 +- .../volumes/create-volume-encryption.spec.ts | 331 ++++++++++++++++++ .../core/volumes/create-volume.smoke.spec.ts | 48 ++- .../e2e/core/volumes/create-volume.spec.ts | 229 +----------- .../e2e/core/volumes/search-volumes.spec.ts | 6 +- .../e2e/core/volumes/upgrade-volume.spec.ts | 20 +- .../cypress/support/intercepts/linodes.ts | 2 +- 10 files changed, 407 insertions(+), 243 deletions(-) create mode 100644 packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md create mode 100644 packages/manager/.changeset/pr-11939-tests-1743444706872.md create mode 100644 packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts diff --git a/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md b/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md new file mode 100644 index 00000000000..c74fcdc193b --- /dev/null +++ b/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Fixed +--- + +Remove trailing slash from outgoing Linode API GET request ([#11939](https://github.com/linode/manager/pull/11939)) diff --git a/packages/api-v4/src/linodes/linodes.ts b/packages/api-v4/src/linodes/linodes.ts index 57cf449c934..c69795ffad7 100644 --- a/packages/api-v4/src/linodes/linodes.ts +++ b/packages/api-v4/src/linodes/linodes.ts @@ -72,7 +72,7 @@ export const getLinodeVolumes = ( */ export const getLinodes = (params?: Params, filter?: Filter) => Request>( - setURL(`${API_ROOT}/linode/instances/`), + setURL(`${API_ROOT}/linode/instances`), setMethod('GET'), setXFilter(filter), setParams(params) diff --git a/packages/manager/.changeset/pr-11939-tests-1743444706872.md b/packages/manager/.changeset/pr-11939-tests-1743444706872.md new file mode 100644 index 00000000000..5ad465a6cd1 --- /dev/null +++ b/packages/manager/.changeset/pr-11939-tests-1743444706872.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Allow Cypress Volume tests to pass against alternative environments ([#11939](https://github.com/linode/manager/pull/11939)) diff --git a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts index 66f6a126dab..b67e42ff902 100644 --- a/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/smoke-linode-landing-table.spec.ts @@ -74,7 +74,7 @@ describe('linode landing checks', () => { req.reply(mockAccountSettings); }).as('getAccountSettings'); cy.intercept('GET', apiMatcher('profile')).as('getProfile'); - cy.intercept('GET', apiMatcher('linode/instances/*'), (req) => { + cy.intercept('GET', apiMatcher('linode/instances*'), (req) => { req.reply(mockLinodesData); }).as('getLinodes'); cy.visitWithLogin('/', { preferenceOverrides }); diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts new file mode 100644 index 00000000000..bfd3af1a891 --- /dev/null +++ b/packages/manager/cypress/e2e/core/volumes/create-volume-encryption.spec.ts @@ -0,0 +1,331 @@ +/** + * @file UI tests involving Volume creation with Block Storage Encryption functionality. + */ +import { linodeFactory, regionFactory } from '@linode/utilities'; +import { mockGetAccount } from 'support/intercepts/account'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; +import { + mockGetLinodeDetails, + mockGetLinodeDisks, + mockGetLinodeVolumes, + mockGetLinodes, +} from 'support/intercepts/linodes'; +import { mockGetRegions } from 'support/intercepts/regions'; +import { mockGetVolume, mockGetVolumes } from 'support/intercepts/volumes'; +import { ui } from 'support/ui'; +import { randomLabel } from 'support/util/random'; + +import { + accountFactory, + linodeDiskFactory, + volumeFactory, +} from 'src/factories'; + +import type { Linode } from '@linode/api-v4'; + +/** + * Notice text that is expected to appear upon attempting to attach encrypted Volume to Linode without BSE capability. + */ +const CLIENT_LIBRARY_UPDATE_COPY = + 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; + +describe('Volume creation with Block Storage Encryption', () => { + describe('Reboot notice', () => { + const mockAccount = accountFactory.build({ + capabilities: ['Linodes', 'Block Storage Encryption'], + }); + + const mockRegion = regionFactory.build({ + capabilities: ['Linodes', 'Block Storage Encryption', 'Block Storage'], + }); + + const mockLinodeWithoutCapability = linodeFactory.build({ + capabilities: [], + label: randomLabel(), + region: mockRegion.id, + }); + + const mockLinodeWithCapability: Linode = { + ...mockLinodeWithoutCapability, + capabilities: ['Block Storage Encryption'], + }; + + const mockVolumeEncrypted = volumeFactory.build({ + encryption: 'enabled', + label: randomLabel(), + region: mockRegion.id, + }); + + /* + * Tests that confirm that the Linode reboot notice appears when expected. + * + * Some Linodes lack the capability to support Block Storage Encryption. The + * capability can be added, however, by rebooting the affected Linode(s) to + * allow a client library update to take place that enables support for + * encryption. + * + * These tests confirm that users are informed of this requirement and + * are prevented from completing Volume create/attach flows when this + * requirement is not met. + */ + describe('Notice is shown when expected', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + blockStorageEncryption: true, + }); + mockGetAccount(mockAccount); + mockGetRegions([mockRegion]); + mockGetLinodes([mockLinodeWithoutCapability]); + mockGetLinodeDetails( + mockLinodeWithoutCapability.id, + mockLinodeWithoutCapability + ); + mockGetLinodeVolumes(mockLinodeWithoutCapability.id, []); + mockGetLinodeDisks(mockLinodeWithoutCapability.id, [ + linodeDiskFactory.build(), + ]); + }); + + /* + * - Confirms notice appears when creating and attaching a new Volume via the Volume create page. + * - Confirms submit button is disabled while notice is present. + */ + it('shows notice on Volume create page when attaching new Volume to Linode without BSE', () => { + mockGetVolumes([]); + cy.visitWithLogin('/volumes/create'); + + // Select a region, then select a Linode that does not have the BSE capability. + ui.autocomplete.findByLabel('Region').type(mockRegion.label); + + ui.regionSelect.findItemByRegionId(mockRegion.id, [mockRegion]).click(); + + ui.autocomplete + .findByLabel('Linode') + .type(mockLinodeWithoutCapability.label); + + ui.autocompletePopper.find().within(() => { + cy.findByText(mockLinodeWithoutCapability.label).click(); + }); + + // Confirm that reboot notice is absent before clicking the "Encrypt Volume" checkbox. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + cy.findByText('Encrypt Volume').should('be.visible').click(); + + // Confirm that reboot notice appears after clicking the "Encrypt Volume" checkbox, + // and that Volume create submit button remains disabled. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + + ui.button + .findByTitle('Create Volume') + .scrollIntoView() + .should('be.visible') + .should('be.disabled'); + }); + + /* + * - Confirms notice appears when attaching an existing Volume via the Linode details page. + * - Confirms submit button is disabled while notice is present. + */ + it('shows notice on Linode details page when attaching existing Volume to Linode without BSE', () => { + mockGetVolumes([mockVolumeEncrypted]); + mockGetVolume(mockVolumeEncrypted); + cy.visitWithLogin(`/linodes/${mockLinodeWithoutCapability.id}/storage`); + + ui.button + .findByTitle('Add Volume') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle(`Create Volume for ${mockLinodeWithoutCapability.label}`) + .should('be.visible') + .within(() => { + cy.findByText('Attach Existing Volume') + .should('be.visible') + .click(); + + // Confirm that reboot notice is absent before Volume is selected. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + ui.autocomplete + .findByLabel('Volume') + .type(mockVolumeEncrypted.label); + + ui.autocompletePopper.find().within(() => { + cy.findByText(mockVolumeEncrypted.label) + .should('be.visible') + .click(); + }); + + // Confirm that selecting an encrypted Volume triggers reboot notice to appear. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button + .findByTitle('Attach Volume') + .scrollIntoView() + .should('be.disabled'); + }); + }); + + /* + * - Confirms notice appears when creating and attaching a new Volume via the Linode details page. + * - Confirms submit button is disabled while notice is present. + */ + it('shows notice on Linode details page when creating new Volume and attaching to Linode without BSE', () => { + mockGetVolumes([]); + cy.visitWithLogin(`/linodes/${mockLinodeWithoutCapability.id}/storage`); + + ui.button + .findByTitle('Add Volume') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle(`Create Volume for ${mockLinodeWithoutCapability.label}`) + .should('be.visible') + .within(() => { + cy.findByLabelText('Create and Attach Volume').should('be.checked'); + + // Confirm that reboot notice is absent before encryption is selected. + cy.findByLabelText('Encrypt Volume').should('not.be.checked'); + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + // Click the "Encrypt Volume" checkbox and confirm that notice appears. + cy.findByText('Encrypt Volume').should('be.visible').click(); + + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); + ui.button + .findByTitle('Create Volume') + .scrollIntoView() + .should('be.disabled'); + }); + }); + }); + + /* + * Tests that confirm that the Linode reboot notice is not shown when it shouldn't be. + * + * These tests confirm that users are not shown the Linode reboot notice when attaching + * encrypted Volumes to Linodes that already have the block storage encryption capability, + * and they are not prevented from attaching Volumes to Linodes in these cases. + */ + describe('Reboot notice is absent when expected', () => { + beforeEach(() => { + mockAppendFeatureFlags({ + blockStorageEncryption: true, + }); + mockGetAccount(mockAccount); + mockGetRegions([mockRegion]); + mockGetLinodes([mockLinodeWithCapability]); + mockGetLinodeDetails( + mockLinodeWithCapability.id, + mockLinodeWithCapability + ); + mockGetLinodeVolumes(mockLinodeWithCapability.id, []); + mockGetLinodeDisks(mockLinodeWithCapability.id, [ + linodeDiskFactory.build(), + ]); + }); + + /* + * - Confirms notice appears is absent when creating and attaching a new Volume via the Volume create page. + */ + it('does not show notice on Volume create page when attaching new Volume to Linode with BSE', () => { + mockGetVolumes([]); + cy.visitWithLogin('/volumes/create'); + + // Select a region, then select a Linode that has the BSE capability. + ui.autocomplete.findByLabel('Region').type(mockRegion.label); + + ui.regionSelect.findItemByRegionId(mockRegion.id, [mockRegion]).click(); + + ui.autocomplete + .findByLabel('Linode') + .type(mockLinodeWithCapability.label); + + ui.autocompletePopper.find().within(() => { + cy.findByText(mockLinodeWithCapability.label).click(); + }); + + cy.findByText('Encrypt Volume').should('be.visible').click(); + + // Confirm that reboot notice is absent after checking "Encrypt Volume", + // and the "Create Volume" button is enabled. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + ui.button.findByTitle('Create Volume').should('be.enabled'); + }); + + /* + * - Confirms notice is absent when attaching an existing Volume via the Linode details page. + */ + it('does not show notice on Linode details page when attaching existing Volume to Linode with BSE', () => { + mockGetVolumes([mockVolumeEncrypted]); + mockGetVolume(mockVolumeEncrypted); + cy.visitWithLogin(`/linodes/${mockLinodeWithCapability.id}/storage`); + + ui.button + .findByTitle('Add Volume') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle(`Create Volume for ${mockLinodeWithCapability.label}`) + .should('be.visible') + .within(() => { + cy.findByText('Attach Existing Volume') + .should('be.visible') + .click(); + + ui.autocomplete + .findByLabel('Volume') + .type(mockVolumeEncrypted.label); + + ui.autocompletePopper.find().within(() => { + cy.findByText(mockVolumeEncrypted.label) + .should('be.visible') + .click(); + }); + + // Confirm that reboot notice is absent and submit button is enabled. + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + ui.button.findByTitle('Attach Volume').should('be.enabled'); + }); + }); + + /* + * - Confirms notice is absent when creating and attaching a new Volume via the Linode details page. + */ + it('does not show notice on Linode details page when creating new Volume and attaching to Linode with BSE', () => { + mockGetVolumes([]); + cy.visitWithLogin(`/linodes/${mockLinodeWithCapability.id}/storage`); + + ui.button + .findByTitle('Add Volume') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle(`Create Volume for ${mockLinodeWithCapability.label}`) + .should('be.visible') + .within(() => { + cy.findByLabelText('Create and Attach Volume').should('be.checked'); + + // Confirm that reboot notice is absent before encryption is selected. + cy.findByLabelText('Encrypt Volume').should('not.be.checked'); + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + + // Click the "Encrypt Volume" checkbox and confirm that notice appears. + cy.findByText('Encrypt Volume').should('be.visible').click(); + + cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); + ui.button.findByTitle('Create Volume').should('be.enabled'); + }); + }); + }); + }); +}); diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts index 4edd06c7471..df637091da3 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.smoke.spec.ts @@ -3,6 +3,7 @@ import { linodeFactory } from '@linode/utilities'; import { volumeFactory, volumeTypeFactory } from '@src/factories'; import { mockGetLinodeDetails, + mockGetLinodeDisks, mockGetLinodeVolumes, mockGetLinodes, } from 'support/intercepts/linodes'; @@ -16,14 +17,13 @@ import { } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; import { randomLabel, randomNumber } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; import { PRICES_RELOAD_ERROR_NOTICE_TEXT, UNKNOWN_PRICE, } from 'src/utilities/pricing/constants'; -const region = 'US, Newark, NJ'; - /** * Asserts that a volume is listed and has the expected config information. * @@ -35,6 +35,7 @@ const region = 'US, Newark, NJ'; */ const validateBasicVolume = ( volumeLabel: string, + regionLabel: string, attachedLinodeLabel?: string ) => { const attached = attachedLinodeLabel ?? 'Unattached'; @@ -50,7 +51,7 @@ const validateBasicVolume = ( cy.findByText(volumeLabel) .closest('tr') .within(() => { - cy.findByText(region).should('be.visible'); + cy.findByText(regionLabel).should('be.visible'); cy.findByText(attached).should('be.visible'); }); }; @@ -78,7 +79,11 @@ const localStorageOverrides = { describe('volumes', () => { it('creates a volume without linode from volumes page', () => { - const mockVolume = volumeFactory.build({ label: randomLabel() }); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const mockVolume = volumeFactory.build({ + label: randomLabel(), + region: mockRegion.id, + }); const mockVolumeTypes = volumeTypeFactory.buildList(1); mockGetVolumes([]).as('getVolumes'); @@ -107,12 +112,16 @@ describe('volumes', () => { cy.findByText('Must provide a region or a Linode ID.').should('be.visible'); - ui.regionSelect.find().click().type('newark{enter}'); + ui.regionSelect.find().click().type(`${mockRegion.label}`); + ui.regionSelect + .findItemByRegionId(mockRegion.id) + .should('be.visible') + .click(); mockGetVolumes([mockVolume]).as('getVolumes'); ui.button.findByTitle('Create Volume').should('be.visible').click(); cy.wait(['@createVolume', '@getVolume', '@getVolumes']); - validateBasicVolume(mockVolume.label); + validateBasicVolume(mockVolume.label, mockRegion.label); ui.actionMenu .findByTitle(`Action menu for Volume ${mockVolume.label}`) @@ -123,18 +132,22 @@ describe('volumes', () => { }); it('creates volume from linode details', () => { + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); const mockLinode = linodeFactory.build({ id: randomNumber(), label: randomLabel(), + region: mockRegion.id, }); const newVolume = volumeFactory.build({ label: randomLabel(), linode_id: mockLinode.id, + region: mockRegion.id, }); mockCreateVolume(newVolume).as('createVolume'); mockGetLinodes([mockLinode]).as('getLinodes'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinodeDetail'); + mockGetLinodeDisks(mockLinode.id, []); mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); cy.visitWithLogin('/linodes', { @@ -182,11 +195,16 @@ describe('volumes', () => { }); it('detaches attached volume', () => { - const mockLinode = linodeFactory.build({ label: randomLabel() }); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const mockLinode = linodeFactory.build({ + label: randomLabel(), + region: mockRegion.id, + }); const mockAttachedVolume = volumeFactory.build({ label: randomLabel(), linode_id: mockLinode.id, linode_label: mockLinode.label, + region: mockRegion.id, }); mockDetachVolume(mockAttachedVolume.id).as('detachVolume'); @@ -229,7 +247,11 @@ describe('volumes', () => { }); it('does not allow creation of a volume with invalid pricing from volumes landing', () => { - const mockVolume = volumeFactory.build({ label: randomLabel() }); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const mockVolume = volumeFactory.build({ + label: randomLabel(), + region: mockRegion.id, + }); mockGetVolumes([]).as('getVolumes'); mockCreateVolume(mockVolume).as('createVolume'); @@ -245,7 +267,11 @@ describe('volumes', () => { cy.url().should('endWith', 'volumes/create'); - ui.regionSelect.find().click().type('newark{enter}'); + ui.regionSelect.find().click().type(mockRegion.label); + ui.regionSelect + .findItemByRegionId(mockRegion.id) + .should('be.visible') + .click(); cy.wait(['@getVolumeTypesError']); @@ -260,17 +286,21 @@ describe('volumes', () => { }); it('does not allow creation of a volume with invalid pricing from linode details', () => { + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); const mockLinode = linodeFactory.build({ id: randomNumber(), label: randomLabel(), + region: mockRegion.id, }); const newVolume = volumeFactory.build({ label: randomLabel(), + region: mockRegion.id, }); mockCreateVolume(newVolume).as('createVolume'); mockGetLinodes([mockLinode]).as('getLinodes'); mockGetLinodeDetails(mockLinode.id, mockLinode).as('getLinodeDetail'); + mockGetLinodeDisks(mockLinode.id, []); mockGetLinodeVolumes(mockLinode.id, []).as('getVolumes'); // Mock an error response to the /types endpoint so prices cannot be calculated. mockGetVolumeTypesError().as('getVolumeTypesError'); diff --git a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts index 5804c9f2d70..8a12ff4f8ee 100644 --- a/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/create-volume.spec.ts @@ -1,8 +1,4 @@ -import { - createLinodeRequestFactory, - linodeFactory, - regionFactory, -} from '@linode/utilities'; +import { createLinodeRequestFactory } from '@linode/utilities'; import { accountUserFactory, grantsFactory, @@ -10,31 +6,19 @@ import { } from '@src/factories'; import { authenticate } from 'support/api/authentication'; import { entityTag } from 'support/constants/cypress'; -import { mockGetAccount, mockGetUser } from 'support/intercepts/account'; -import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; -import { - mockGetLinodeDetails, - mockGetLinodes, -} from 'support/intercepts/linodes'; +import { mockGetUser } from 'support/intercepts/account'; import { mockGetProfile, mockGetProfileGrants, } from 'support/intercepts/profile'; -import { mockGetRegions } from 'support/intercepts/regions'; -import { - interceptCreateVolume, - mockGetVolume, - mockGetVolumes, -} from 'support/intercepts/volumes'; +import { interceptCreateVolume } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { createTestLinode } from 'support/util/linodes'; import { randomLabel, randomNumber, randomString } from 'support/util/random'; import { chooseRegion } from 'support/util/regions'; -import { accountFactory, volumeFactory } from 'src/factories'; - -import type { Linode, Region } from '@linode/api-v4'; +import type { Linode } from '@linode/api-v4'; // Local storage override to force volume table to list up to 100 items. // This is a workaround while we wait to get stuck volumes removed. @@ -43,18 +27,6 @@ const pageSizeOverride = { PAGE_SIZE: 100, }; -const mockRegions: Region[] = [ - regionFactory.build({ - capabilities: ['Linodes', 'Block Storage', 'Block Storage Encryption'], - id: 'us-east', - label: 'Newark, NJ', - site_type: 'core', - }), -]; - -const CLIENT_LIBRARY_UPDATE_COPY = - 'This Linode requires a client library update and will need to be rebooted prior to attaching an encrypted volume.'; - authenticate(); describe('volume create flow', () => { before(() => { @@ -156,10 +128,6 @@ describe('volume create flow', () => { .should('be.visible') .click(); - // @TODO BSE: once BSE is fully rolled out, check for the notice (selected linode doesn't have - // "Block Storage Encryption" capability + user checked "Encrypt Volume" checkbox) instead of the absence of it - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - cy.findByText('Create Volume').click(); cy.wait('@createVolume'); @@ -189,195 +157,6 @@ describe('volume create flow', () => { ); }); - /* - * - Checks for Block Storage Encryption client library update notice on the Volume Create page. - */ - it('displays a warning notice on Volume Create page re: rebooting for client library updates under the appropriate conditions', () => { - // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; volume being created is encrypted and the - // selected Linode does not support Block Storage Encryption - - // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions).as('getRegions'); - - const linodeRequest = createLinodeRequestFactory.build({ - booted: false, - label: randomLabel(), - region: mockRegions[0].id, - root_pass: randomString(16), - }); - - cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( - (linode: Linode) => { - cy.visitWithLogin('/volumes/create'); - cy.wait(['@getFeatureFlags', '@getAccount']); - - // Select a linode without the BSE capability - cy.findByLabelText('Linode').should('be.visible').click(); - cy.focused().type(linode.label); - - ui.autocompletePopper - .findByTitle(linode.label) - .should('be.visible') - .click(); - - // Check the "Encrypt Volume" checkbox - cy.get('[data-qa-checked]').should('be.visible').click(); - // }); - - // Ensure warning notice is displayed and "Create Volume" button is disabled - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Create Volume') - .should('be.visible') - .should('be.disabled'); - } - ); - }); - - /* - * - Checks for absence of Block Storage Encryption client library update notice on the Volume Create page - * when selected linode supports BSE - */ - it('does not display a warning notice on Volume Create page re: rebooting for client library updates when selected linode supports BSE', () => { - // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; volume being created is encrypted and the - // selected Linode supports Block Storage Encryption - - // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - // Mock linode - const mockLinode = linodeFactory.build({ - capabilities: ['Block Storage Encryption'], - id: 123456, - region: mockRegions[0].id, - }); - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions).as('getRegions'); - mockGetLinodes([mockLinode]).as('getLinodes'); - mockGetLinodeDetails(mockLinode.id, mockLinode); - - cy.visitWithLogin(`/volumes/create`); - cy.wait(['@getAccount', '@getRegions', '@getLinodes']); - - // Select a linode without the BSE capability - cy.findByLabelText('Linode').should('be.visible').click(); - cy.focused().type(mockLinode.label); - - ui.autocompletePopper - .findByTitle(mockLinode.label) - .should('be.visible') - .click(); - - // Check the "Encrypt Volume" checkbox - cy.get('[data-qa-checked]').should('be.visible').click(); - // }); - - // Ensure warning notice is not displayed and "Create Volume" button is enabled - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button - .findByTitle('Create Volume') - .should('be.visible') - .should('be.enabled'); - }); - - /* - * - Checks for Block Storage Encryption client library update notice in the Create/Attach Volume drawer from the - 'Storage' details page of an existing Linode. - */ - it('displays a warning notice re: rebooting for client library updates under the appropriate conditions in Create/Attach Volume drawer', () => { - // Conditions: Block Storage encryption feature flag is on; user has Block Storage Encryption capability; Linode does not support Block Storage Encryption and the user is trying to attach an encrypted volume - - // Mock feature flag -- @TODO BSE: Remove feature flag once BSE is fully rolled out - mockAppendFeatureFlags({ - blockStorageEncryption: true, - }).as('getFeatureFlags'); - - // Mock account response - const mockAccount = accountFactory.build({ - capabilities: ['Linodes', 'Block Storage Encryption'], - }); - - mockGetAccount(mockAccount).as('getAccount'); - mockGetRegions(mockRegions).as('getRegions'); - - const volume = volumeFactory.build({ - encryption: 'enabled', - region: mockRegions[0].id, - }); - - const linodeRequest = createLinodeRequestFactory.build({ - booted: false, - label: randomLabel(), - region: mockRegions[0].id, - root_pass: randomString(16), - }); - - cy.defer(() => createTestLinode(linodeRequest), 'creating Linode').then( - (linode: Linode) => { - mockGetVolumes([volume]).as('getVolumes'); - mockGetVolume(volume); - - cy.visitWithLogin(`/linodes/${linode.id}/storage`); - cy.wait(['@getFeatureFlags', '@getAccount']); - - // Click "Add Volume" button - cy.findByText('Add Volume').click(); - - // Check "Encrypt Volume" checkbox - cy.get('[data-qa-drawer="true"]').within(() => { - cy.get('[data-qa-checked]').should('be.visible').click(); - }); - - // Ensure client library update notice is displayed and the "Create Volume" button is disabled - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button.findByTitle('Create Volume').should('be.disabled'); - - // Ensure notice is cleared when switching views in drawer - cy.get('[data-qa-radio="Attach Existing Volume"]').click(); - cy.wait(['@getVolumes']); - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('not.exist'); - ui.button - .findByTitle('Attach Volume') - .should('be.visible') - .should('be.enabled'); - - // Ensure notice is displayed in "Attach Existing Volume" view when an encrypted volume is selected, & that the "Attach Volume" button is disabled - cy.findByPlaceholderText('Select a Volume') - .should('be.visible') - .click(); - cy.focused().type(`${volume.label}{downarrow}{enter}`); - ui.autocompletePopper - .findByTitle(volume.label) - .should('be.visible') - .click(); - - cy.findByText(CLIENT_LIBRARY_UPDATE_COPY).should('be.visible'); - ui.button - .findByTitle('Attach Volume') - .should('be.visible') - .should('be.disabled'); - } - ); - }); - /* * - Creates a volume from the 'Storage' details page of an existing Linode. * - Confirms that volume is listed correctly on Linode 'Storage' details page. diff --git a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts index 5370154172f..63887775207 100644 --- a/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/search-volumes.spec.ts @@ -3,6 +3,7 @@ import { authenticate } from 'support/api/authentication'; import { ui } from 'support/ui'; import { cleanUp } from 'support/util/cleanup'; import { randomLabel } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; import type { Volume } from '@linode/api-v4'; @@ -20,15 +21,16 @@ describe('Search Volumes', () => { */ it('creates two volumes and make sure they show up in the table and are searchable', () => { const createTwoVolumes = async (): Promise<[Volume, Volume]> => { + const volumeRegion = chooseRegion(); return Promise.all([ createVolume({ label: randomLabel(), - region: 'us-east', + region: volumeRegion.id, size: 10, }), createVolume({ label: randomLabel(), - region: 'us-east', + region: volumeRegion.id, size: 10, }), ]); diff --git a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts index 09b03d3f971..f6ff9e71a2d 100644 --- a/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts +++ b/packages/manager/cypress/e2e/core/volumes/upgrade-volume.spec.ts @@ -16,10 +16,14 @@ import { mockMigrateVolumes, } from 'support/intercepts/volumes'; import { ui } from 'support/ui'; +import { chooseRegion } from 'support/util/regions'; describe('volume upgrade/migration', () => { it('can upgrade an unattached volume to NVMe', () => { - const volume = volumeFactory.build(); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const volume = volumeFactory.build({ + region: mockRegion.id, + }); const migrationScheduledNotification = notificationFactory.build({ entity: { id: volume.id, type: 'volume' }, @@ -96,10 +100,14 @@ describe('volume upgrade/migration', () => { }); it('can upgrade an attached volume from the volumes landing page', () => { - const linode = linodeFactory.build(); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const linode = linodeFactory.build({ + region: mockRegion.id, + }); const volume = volumeFactory.build({ linode_id: linode.id, linode_label: linode.label, + region: mockRegion.id, }); const migrationScheduledNotification = notificationFactory.build({ @@ -110,6 +118,7 @@ describe('volume upgrade/migration', () => { mockGetVolumes([volume]).as('getVolumes'); mockMigrateVolumes().as('migrateVolumes'); mockGetLinodeDetails(linode.id, linode).as('getLinode'); + mockGetLinodeVolumes(linode.id, [volume]); mockGetLinodeDisks(linode.id, []); mockGetNotifications([migrationScheduledNotification]).as( 'getNotifications' @@ -142,7 +151,6 @@ describe('volume upgrade/migration', () => { `A Volume attached to Linode ${linode.label} will be upgraded to high-performance NVMe Block Storage.`, { exact: false } ).should('be.visible'); - ui.button .findByTitle('Enter Upgrade Queue') .should('be.visible') @@ -187,10 +195,14 @@ describe('volume upgrade/migration', () => { }); it('can upgrade an attached volume from the linode details page', () => { - const linode = linodeFactory.build(); + const mockRegion = chooseRegion({ capabilities: ['Block Storage'] }); + const linode = linodeFactory.build({ + region: mockRegion.id, + }); const volume = volumeFactory.build({ linode_id: linode.id, linode_label: linode.label, + region: mockRegion.id, }); const migrationScheduledNotification = notificationFactory.build({ diff --git a/packages/manager/cypress/support/intercepts/linodes.ts b/packages/manager/cypress/support/intercepts/linodes.ts index 5c3b04f2da8..163dbb07136 100644 --- a/packages/manager/cypress/support/intercepts/linodes.ts +++ b/packages/manager/cypress/support/intercepts/linodes.ts @@ -119,7 +119,7 @@ export const interceptGetLinodes = (): Cypress.Chainable => { export const mockGetLinodes = (linodes: Linode[]): Cypress.Chainable => { return cy.intercept( 'GET', - apiMatcher('linode/instances/**'), + apiMatcher('linode/instances*'), paginateResponse(linodes) ); }; From ae9e6285ffc162278785ed2a60763193b98413f8 Mon Sep 17 00:00:00 2001 From: Nikhil Agrawal <165884194+nikhagra-akamai@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:59:28 +0530 Subject: [PATCH 52/84] refactor: [DI-24423] - Added a refetch interval to alert details query (#11945) * refactor: [DI-24423] - Added a refetch interval to alert details query * added changeset * refactor: [DI-24423] - Updated failing test case * refactor: [DI-24423] - Updated changeset Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> --------- Co-authored-by: cpathipa <119517080+cpathipa@users.noreply.github.com> --- .../manager/.changeset/pr-11945-changed-1743511226551.md | 5 +++++ .../CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx | 2 +- .../features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx | 5 +++-- packages/manager/src/queries/cloudpulse/alerts.ts | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 packages/manager/.changeset/pr-11945-changed-1743511226551.md diff --git a/packages/manager/.changeset/pr-11945-changed-1743511226551.md b/packages/manager/.changeset/pr-11945-changed-1743511226551.md new file mode 100644 index 00000000000..0f3f58d3711 --- /dev/null +++ b/packages/manager/.changeset/pr-11945-changed-1743511226551.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx index 77624192104..10c91aa20ca 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.test.tsx @@ -105,7 +105,7 @@ describe('AlertDetail component tests', () => { queryMocks.useAlertDefinitionQuery.mockReturnValueOnce({ data: null, isError: false, - isFetching: true, + isLoading: true, }); const { getByTestId } = renderWithTheme(); diff --git a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx index 691e3127f1b..a351c28e009 100644 --- a/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx +++ b/packages/manager/src/features/CloudPulse/Alerts/AlertsDetail/AlertDetail.tsx @@ -29,7 +29,7 @@ export interface AlertRouteParams { export const AlertDetail = () => { const { alertId, serviceType } = useParams(); - const { data: alertDetails, isError, isFetching } = useAlertDefinitionQuery( + const { data: alertDetails, isError, isLoading } = useAlertDefinitionQuery( alertId, serviceType ); @@ -54,7 +54,7 @@ export const AlertDetail = () => { const nonSuccessBoxHeight = '600px'; const sectionMaxHeight = '785px'; - if (isFetching) { + if (isLoading) { return ( <> @@ -159,6 +159,7 @@ export const StyledPlaceholder = styled(Placeholder, { h1: { fontSize: theme.spacing(2), }, + padding: 0, svg: { maxHeight: theme.spacing(10), }, diff --git a/packages/manager/src/queries/cloudpulse/alerts.ts b/packages/manager/src/queries/cloudpulse/alerts.ts index 9300f5409af..c7d9df20150 100644 --- a/packages/manager/src/queries/cloudpulse/alerts.ts +++ b/packages/manager/src/queries/cloudpulse/alerts.ts @@ -61,6 +61,7 @@ export const useAlertDefinitionQuery = ( ) => { return useQuery({ ...queryFactory.alerts._ctx.alertByServiceTypeAndId(serviceType, alertId), + refetchInterval: 120000, }); }; From 4adcb87910560910122d99901fbef7a6ce3db227 Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:57:13 -0400 Subject: [PATCH 53/84] upcoming: [M3-9637] - Support more VPC features and default Firewalls on the Linode Create flow (#11915) * save progress * handle complex error case * more fixes * dial in UI * default firewalls work on Linode Create flow * clean up a bit * unit test a few things * Added changeset: Support more VPC features when using Linode Interfaces on the Linode Create page * Added changeset: Pre-select default firewalls on the Linode Create flow * not sure what to do here * not sure what to do here * some fixes and better error handling * fix clickable areas in VPC section * improve default firewall ux * hacky fix to improve error handling for duplicate purpose field * Revert "hacky fix to improve error handling for duplicate purpose field" This reverts commit 7ce8c8d5fb4f7accc784c0363cb2ac5f674e112f. --------- Co-authored-by: Banks Nussman --- ...r-11915-upcoming-features-1742915363501.md | 5 + ...r-11915-upcoming-features-1742915406867.md | 5 + .../Networking/InterfaceFirewall.tsx | 18 +- .../LinodeCreate/Networking/InterfaceType.tsx | 32 +++- .../LinodeCreate/Networking/Networking.tsx | 17 +- .../Linodes/LinodeCreate/Networking/VPC.tsx | 99 +++++++++- .../LinodeCreate/Networking/VPCRanges.tsx | 87 +++++++++ .../LinodeCreate/Networking/utilities.test.ts | 174 ++++++++++++------ .../LinodeCreate/Networking/utilities.ts | 69 ++++++- .../Linodes/LinodeCreate/utilities.ts | 37 ++-- packages/validation/src/linodes.schema.ts | 4 +- 11 files changed, 448 insertions(+), 99 deletions(-) create mode 100644 packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md create mode 100644 packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md create mode 100644 packages/manager/src/features/Linodes/LinodeCreate/Networking/VPCRanges.tsx diff --git a/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md b/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md new file mode 100644 index 00000000000..a8863fbd4d3 --- /dev/null +++ b/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915)) diff --git a/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md b/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md new file mode 100644 index 00000000000..3321319c8d8 --- /dev/null +++ b/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915)) diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceFirewall.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceFirewall.tsx index fa7b1d65a46..c2398652726 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceFirewall.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/InterfaceFirewall.tsx @@ -1,9 +1,9 @@ -import { useAllFirewallsQuery } from '@linode/queries'; -import { Autocomplete, Box, Stack } from '@linode/ui'; +import { Box, Stack } from '@linode/ui'; import React, { useState } from 'react'; import { useController, useFormContext, useWatch } from 'react-hook-form'; import { LinkButton } from 'src/components/LinkButton'; +import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; import { CreateFirewallDrawer } from 'src/features/Firewalls/FirewallLanding/CreateFirewallDrawer'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; @@ -26,31 +26,23 @@ export const InterfaceFirewall = ({ index }: Props) => { name: `linodeInterfaces.${index}.firewall_id`, }); - const { data: firewalls, error, isLoading } = useAllFirewallsQuery(); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); const isLinodeCreateRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_linodes', }); - const selectedFirewall = - firewalls?.find((firewall) => firewall.id === field.value) ?? null; - return ( - field.onChange(firewall?.id ?? null)} - options={firewalls ?? []} placeholder="None" - value={selectedFirewall} + value={field.value} /> { - const { control } = useFormContext(); + const { + control, + setValue, + getFieldState, + } = useFormContext(); + + const { data: firewallSettings } = useFirewallSettingsQuery(); const { field } = useController({ control, @@ -24,8 +34,26 @@ export const InterfaceType = ({ index }: Props) => { Network Connection { + // Change the interface purpose (Public, VPC, VLAN) + field.onChange(value); + + const defaultFirewall = getDefaultFirewallForInterfacePurpose( + value as InterfacePurpose, + firewallSettings + ); + + // Set the Firewall based on defaults if: + // - there is a default firewall for this interface type + // - the user has not touched the Firewall field + if ( + defaultFirewall && + !getFieldState(`linodeInterfaces.${index}.firewall_id`).isTouched + ) { + setValue(`linodeInterfaces.${index}.firewall_id`, defaultFirewall); + } + }} aria-labelledby="network-interface" - onChange={field.onChange} row sx={{ mb: '0px !important' }} value={field.value} diff --git a/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx b/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx index 9ecbf859fd3..bd1ebda2ab0 100644 --- a/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreate/Networking/Networking.tsx @@ -1,3 +1,4 @@ +import { useFirewallSettingsQuery } from '@linode/queries'; import { Button, Divider, @@ -13,6 +14,7 @@ import { useFieldArray, useFormContext, useWatch } from 'react-hook-form'; import { Firewall } from './Firewall'; import { InterfaceGeneration } from './InterfaceGeneration'; import { LinodeInterface } from './LinodeInterface'; +import { getDefaultInterfacePayload } from './utilities'; import type { LinodeCreateFormValues } from '../utilities'; @@ -22,6 +24,8 @@ export const Networking = () => { formState: { errors }, } = useFormContext(); + const { data: firewallSettings } = useFirewallSettingsQuery(); + const { append, fields, remove } = useFieldArray({ control, name: 'linodeInterfaces', @@ -42,16 +46,9 @@ export const Networking = () => { > Networking setSubnetCreateDrawerOpen(false)} - open={subnetCreateDrawerOpen} + onClose={onCloseSubnetDrawer} + open={location.pathname.includes('subnets/create')} vpcId={vpcId} /> { TableRowHead={SubnetTableRowHead} /> { - setSubnetUnassignLinodesDrawerOpen(false); - setSelectedLinode(undefined); - }} - open={subnetUnassignLinodesDrawerOpen} + open={ + params.subnetAction === 'unassign' || + params.linodeAction === 'unassign' + } + isFetching={isFetchingSubnet || isFetchingLinode} + onClose={onCloseSubnetDrawer} singleLinodeToBeUnassigned={selectedLinode} subnet={selectedSubnet} vpcId={vpcId} /> setSubnetAssignLinodesDrawerOpen(false)} - open={subnetAssignLinodesDrawerOpen} + isFetching={isFetchingSubnet} + onClose={onCloseSubnetDrawer} + open={params.subnetAction === 'assign'} subnet={selectedSubnet} vpcId={vpcId} vpcRegion={vpcRegion} /> setDeleteSubnetDialogOpen(false)} - open={deleteSubnetDialogOpen} + isFetching={isFetchingSubnet} + onClose={onCloseSubnetDrawer} + open={params.subnetAction === 'delete'} subnet={selectedSubnet} vpcId={vpcId} /> setEditSubnetsDrawerOpen(false)} - open={editSubnetsDrawerOpen} + isFetching={isFetchingSubnet} + onClose={onCloseSubnetDrawer} + open={params.subnetAction === 'edit'} subnet={selectedSubnet} vpcId={vpcId} /> setPowerActionDialogOpen(false)} + onClose={onCloseSubnetDrawer} /> ); diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx index 7bcebb30890..693c0a86242 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx @@ -1,20 +1,23 @@ -import { fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; -import { renderWithTheme } from 'src/utilities/testHelpers'; +import { vpcFactory } from 'src/factories'; +import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; import { VPCDeleteDialog } from './VPCDeleteDialog'; describe('VPC Delete Dialog', () => { const props = { - id: 1, - label: 'vpc-1', + isFetching: false, onClose: vi.fn(), open: true, + vpc: vpcFactory.build({ label: 'vpc-1' }), }; - it('renders a VPC delete dialog correctly', () => { - const screen = renderWithTheme(); + it('renders a VPC delete dialog correctly', async () => { + const screen = await renderWithThemeAndRouter( + + ); const vpcTitle = screen.getByText('Delete VPC vpc-1'); expect(vpcTitle).toBeVisible(); @@ -24,11 +27,14 @@ describe('VPC Delete Dialog', () => { const deleteButton = screen.getByText('Delete'); expect(deleteButton).toBeVisible(); }); - it('closes the VPC delete dialog as expected', () => { - const screen = renderWithTheme(); + + it('closes the VPC delete dialog as expected', async () => { + const screen = await renderWithThemeAndRouter( + + ); const cancelButton = screen.getByText('Cancel'); expect(cancelButton).toBeVisible(); - fireEvent.click(cancelButton); + await userEvent.click(cancelButton); expect(props.onClose).toBeCalled(); }); }); diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx index 6fe1deb4128..94563357774 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx @@ -1,27 +1,30 @@ +import { useDeleteVPCMutation } from '@linode/queries'; +import { useLocation, useNavigate } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; -import { useDeleteVPCMutation } from '@linode/queries'; + +import type { VPC } from '@linode/api-v4'; interface Props { - id?: number; - label?: string; + isFetching: boolean; onClose: () => void; open: boolean; + vpc?: VPC; } export const VPCDeleteDialog = (props: Props) => { - const { id, label, onClose, open } = props; + const { isFetching, onClose, open, vpc } = props; const { enqueueSnackbar } = useSnackbar(); const { error, isPending, mutateAsync: deleteVPC, reset, - } = useDeleteVPCMutation(id ?? -1); - const history = useHistory(); + } = useDeleteVPCMutation(vpc?.id ?? -1); + const navigate = useNavigate(); + const location = useLocation(); React.useEffect(() => { if (open) { @@ -35,8 +38,8 @@ export const VPCDeleteDialog = (props: Props) => { variant: 'success', }); onClose(); - if (history.location.pathname !== '/vpcs') { - history.push('/vpcs'); + if (location.pathname !== '/vpcs') { + navigate({ to: '/vpcs' }); } }); }; @@ -45,18 +48,19 @@ export const VPCDeleteDialog = (props: Props) => { ); }; diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx index e72eba7e5dc..3e51cb9104c 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx @@ -7,6 +7,7 @@ import { VPCEditDrawer } from './VPCEditDrawer'; describe('Edit VPC Drawer', () => { const props = { + isFetching: false, onClose: vi.fn(), open: true, vpc: vpcFactory.build(), diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 497c31d7846..891ad61dc28 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -10,13 +10,14 @@ import { NotFound } from 'src/components/NotFound'; import type { UpdateVPCPayload, VPC } from '@linode/api-v4'; interface Props { + isFetching: boolean; onClose: () => void; open: boolean; vpc?: VPC; } export const VPCEditDrawer = (props: Props) => { - const { onClose, open, vpc } = props; + const { isFetching, onClose, open, vpc } = props; const { data: profile } = useProfile(); const { data: grants } = useGrants(); @@ -45,8 +46,8 @@ export const VPCEditDrawer = (props: Props) => { mode: 'onBlur', resolver: yupResolver(updateVPCSchema), values: { - description: vpc?.description, - label: vpc?.label, + description: vpc?.description ?? '', + label: vpc?.label ?? '', }, }); @@ -70,6 +71,7 @@ export const VPCEditDrawer = (props: Props) => { return ( { - const { push } = useHistory(); + const navigate = useNavigate(); const isVPCCreationRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_vpcs', @@ -29,7 +29,7 @@ export const VPCEmptyState = () => { category: linkAnalyticsEvent.category, label: 'Create VPC', }); - push('/vpcs/create'); + navigate({ to: '/vpcs/create' }); }, tooltipText: getRestrictedResourceText({ action: 'create', diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx index 6ff4c148261..351e1d2d620 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx @@ -4,15 +4,18 @@ import * as React from 'react'; import { subnetFactory } from 'src/factories'; import { vpcFactory } from 'src/factories/vpcs'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; -import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; +import { HttpResponse, http, server } from 'src/mocks/testServer'; +import { + mockMatchMedia, + renderWithThemeAndRouter, +} from 'src/utilities/testHelpers'; import VPCLanding from './VPCLanding'; -beforeAll(() => mockMatchMedia()); - const loadingTestId = 'circle-progress'; +beforeAll(() => mockMatchMedia()); + describe('VPC Landing Table', () => { it('should render vpc landing table with items', async () => { server.use( @@ -24,12 +27,14 @@ describe('VPC Landing Table', () => { }) ); - const { getAllByText, getByTestId } = renderWithTheme(); - - // Loading state should render - expect(getByTestId(loadingTestId)).toBeInTheDocument(); + const { getAllByText, queryByTestId } = await renderWithThemeAndRouter( + + ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } // Static text and table column headers getAllByText('Label'); @@ -46,9 +51,14 @@ describe('VPC Landing Table', () => { }) ); - const { getByTestId, getByText } = renderWithTheme(); + const { getByText, queryByTestId } = await renderWithThemeAndRouter( + + ); - await waitForElementToBeRemoved(getByTestId(loadingTestId)); + const loadingState = queryByTestId(loadingTestId); + if (loadingState) { + await waitForElementToBeRemoved(loadingState); + } expect( getByText('Create a private and isolated network') diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx index effbe147675..efcb7d6effd 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx @@ -1,7 +1,7 @@ +import { useVPCQuery, useVPCsQuery } from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; -import { createLazyRoute } from '@tanstack/react-router'; +import { useNavigate, useParams } from '@tanstack/react-router'; import * as React from 'react'; -import { useHistory } from 'react-router-dom'; import { Hidden } from 'src/components/Hidden'; import { LandingHeader } from 'src/components/LandingHeader'; @@ -13,11 +13,16 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { getRestrictedResourceText } from 'src/features/Account/utils'; +import { + VPC_CREATE_ROUTE, + VPC_LANDING_ROUTE, + VPC_LANDING_TABLE_PREFERENCE_KEY, +} from 'src/features/VPCs/constants'; import { VPC_DOCS_LINK, VPC_LABEL } from 'src/features/VPCs/constants'; -import { useOrder } from 'src/hooks/useOrder'; -import { usePagination } from 'src/hooks/usePagination'; +import { useDialogData } from 'src/hooks/useDialogData'; +import { useOrderV2 } from 'src/hooks/useOrderV2'; +import { usePaginationV2 } from 'src/hooks/usePaginationV2'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; -import { useVPCsQuery } from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { VPCDeleteDialog } from './VPCDeleteDialog'; @@ -27,18 +32,22 @@ import { VPCRow } from './VPCRow'; import type { VPC } from '@linode/api-v4/lib/vpcs/types'; -const preferenceKey = 'vpcs'; -const VPC_CREATE_ROUTE = 'vpcs/create'; - const VPCLanding = () => { - const pagination = usePagination(1, preferenceKey); - const { handleOrderChange, order, orderBy } = useOrder( - { - order: 'desc', - orderBy: 'label', + const pagination = usePaginationV2({ + currentRoute: VPC_LANDING_ROUTE, + preferenceKey: VPC_LANDING_TABLE_PREFERENCE_KEY, + }); + + const { handleOrderChange, order, orderBy } = useOrderV2({ + initialRoute: { + defaultOrder: { + order: 'desc', + orderBy: 'label', + }, + from: VPC_LANDING_ROUTE, }, - `${preferenceKey}-order` - ); + preferenceKey: `${VPC_LANDING_TABLE_PREFERENCE_KEY}-order`, + }); const filter = { ['+order']: order, @@ -53,31 +62,42 @@ const VPCLanding = () => { filter ); - const history = useHistory(); - - const [selectedVPC, setSelectedVPC] = React.useState(); - - const [editVPCDrawerOpen, setEditVPCDrawerOpen] = React.useState(false); - const [deleteVPCDialogOpen, setDeleteVPCDialogOpen] = React.useState(false); + const navigate = useNavigate(); + const params = useParams({ strict: false }); const handleEditVPC = (vpc: VPC) => { - setSelectedVPC(vpc); - setEditVPCDrawerOpen(true); + navigate({ + params: { action: 'edit', vpcId: vpc.id }, + to: '/vpcs/$vpcId/$action', + }); }; const handleDeleteVPC = (vpc: VPC) => { - setSelectedVPC(vpc); - setDeleteVPCDialogOpen(true); + navigate({ + params: { action: 'delete', vpcId: vpc.id }, + to: '/vpcs/$vpcId/$action', + }); + }; + + const onCloseVPCDrawer = () => { + navigate({ to: VPC_LANDING_ROUTE }); }; const createVPC = () => { - history.push(VPC_CREATE_ROUTE); + navigate({ to: VPC_CREATE_ROUTE }); }; const isVPCCreationRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_vpcs', }); + const { data: selectedVPC, isFetching: isFetchingVPC } = useDialogData({ + enabled: !!params.vpcId, + paramKey: 'vpcId', + queryHook: useVPCQuery, + redirectToOnNotFound: '/vpcs', + }); + if (error) { return ( { pageSize={pagination.pageSize} /> setDeleteVPCDialogOpen(false)} - open={deleteVPCDialogOpen} + isFetching={isFetchingVPC} + onClose={onCloseVPCDrawer} + open={params.action === 'delete'} + vpc={selectedVPC} /> setEditVPCDrawerOpen(false)} - open={editVPCDrawerOpen} + isFetching={isFetchingVPC} + onClose={onCloseVPCDrawer} + open={params.action === 'edit'} vpc={selectedVPC} /> ); }; -export const vpcLandingLazyRoute = createLazyRoute('/')({ - component: VPCLanding, -}); - export default VPCLanding; diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx index b1de83a5e46..2514c6414e0 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx @@ -1,3 +1,4 @@ +import { useRegionsQuery } from '@linode/queries'; import * as React from 'react'; import { Hidden } from 'src/components/Hidden'; @@ -8,7 +9,6 @@ import { TableRow } from 'src/components/TableRow'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; -import { useRegionsQuery } from '@linode/queries'; import { getIsVPCLKEEnterpriseCluster } from '../utils'; diff --git a/packages/manager/src/features/VPCs/constants.ts b/packages/manager/src/features/VPCs/constants.ts index c07e3b17ffb..ce7250e5d36 100644 --- a/packages/manager/src/features/VPCs/constants.ts +++ b/packages/manager/src/features/VPCs/constants.ts @@ -70,3 +70,11 @@ export const VPC_MULTIPLE_CONFIGURATIONS_LEARN_MORE_LINK = export const ASSIGN_COMPUTE_INSTANCE_TO_VPC_LINK = 'https://techdocs.akamai.com/cloud-computing/docs/assign-a-compute-instance-to-a-vpc'; + +// constants used for tanstack routing: +export const VPC_LANDING_TABLE_PREFERENCE_KEY = 'vpcs'; +export const VPC_LANDING_ROUTE = '/vpcs'; +export const VPC_DETAILS_ROUTE = '/vpcs/$vpcId'; +export const VPC_CREATE_ROUTE = '/vpcs/create'; +export const SUBNET_ACTION_PATH = + '/vpcs/$vpcId/subnets/$subnetId/$subnetAction'; diff --git a/packages/manager/src/features/VPCs/index.tsx b/packages/manager/src/features/VPCs/index.tsx deleted file mode 100644 index f1a8ca644ad..00000000000 --- a/packages/manager/src/features/VPCs/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; -import { Route, Switch } from 'react-router-dom'; - -import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; -import { SuspenseLoader } from 'src/components/SuspenseLoader'; - -const VPCCreate = React.lazy(() => import('./VPCCreate/VPCCreate')); -const VPCDetail = React.lazy(() => import('./VPCDetail/VPCDetail')); -const VPCLanding = React.lazy(() => import('./VPCLanding/VPCLanding')); - -const VPC = () => { - return ( - }> - - - - - - - - - ); -}; - -export default VPC; diff --git a/packages/manager/src/hooks/useDialogData.ts b/packages/manager/src/hooks/useDialogData.ts index b74a0a9d1fe..08020d0e91f 100644 --- a/packages/manager/src/hooks/useDialogData.ts +++ b/packages/manager/src/hooks/useDialogData.ts @@ -12,6 +12,26 @@ import type { MigrationRouteTree } from 'src/routes'; type ExtractKeys = T extends object ? keyof T : never; type ParamsTypeKeys = ExtractKeys>; +type SingleIdQueryHook = ( + id: number | string, + enabled?: boolean +) => UseQueryResult; + +type DualIdQueryHook = ( + primaryId: number | string, + secondaryId: number | string, + enabled?: boolean +) => UseQueryResult; + +type QueryHook = DualIdQueryHook | SingleIdQueryHook; + +interface Props { + enabled?: boolean; + paramKey: ParamsTypeKeys; + queryHook: QueryHook; + redirectToOnNotFound: LinkProps['to']; + secondaryParamKey?: ParamsTypeKeys; +} interface Props { /** @@ -28,14 +48,16 @@ interface Props { /** * The query hook to fetch the entity. */ - queryHook: ( - id: number | string | undefined, - enabled?: boolean - ) => UseQueryResult; + queryHook: QueryHook; /** * The route to redirect to if the entity is not found. */ redirectToOnNotFound: LinkProps['to']; + /** + * The key of the secondary parameter in the URL that will be used to fetch the entity. + * ex: 'subnetId' for `/vpcs/$vpcId/subnets/$subnetId` + */ + secondaryParamKey?: ParamsTypeKeys; } /** @@ -45,7 +67,7 @@ interface Props { * It will return the data for the entity that the dialog is going to target, including its loading state. * It is usually used on a feature landing page, where the dialog is triggered by a route change. * - * It should be instantiated as follow: + * It should be instantiated as follow (single id): * * const { * data: {entity}, @@ -56,17 +78,54 @@ interface Props { * queryHook: useEntityQuery, // ex: useVolumeQuery * redirectToOnNotFound: '/entities', // ex: '/volumes' * }); + * + * It should be instantiated as follow (dual id): + * + * const { + * data: {entity}, + * isFetching: isFetchingEntity, + * } = useDialogRouteGuard({ + * enabled: !!params.entityId && !!params.secondaryEntityId, + * paramKey: 'entityId', // ex: 'vpcId' + * queryHook: useEntityQuery, // ex: useSubnetQuery + * redirectToOnNotFound: '/entities', // ex: '/vpcs/$vpcId/subnets' + * secondaryParamKey: 'secondaryEntityId', // ex: 'subnetId' + * }); */ export const useDialogData = ({ enabled = true, paramKey, queryHook, redirectToOnNotFound, + secondaryParamKey, }: Props) => { const params = useParams({ strict: false }); const { enqueueSnackbar } = useSnackbar(); const navigate = useNavigate(); - const query = queryHook(params[paramKey as keyof typeof params], enabled); + + const primaryId = params[paramKey as keyof typeof params]; + const secondaryId = secondaryParamKey + ? params[secondaryParamKey as keyof typeof params] + : undefined; + + // Ensure IDs are actually valid values, not just truthy + const isValidPrimaryId = + typeof primaryId === 'string' || typeof primaryId === 'number'; + const isValidSecondaryId = + typeof secondaryId === 'string' || typeof secondaryId === 'number'; + const shouldRunQuery = + enabled && isValidPrimaryId && (!secondaryParamKey || isValidSecondaryId); + + const query = secondaryParamKey + ? (queryHook as DualIdQueryHook)( + primaryId as number | string, + secondaryId as number | string, + shouldRunQuery + ) + : (queryHook as SingleIdQueryHook)( + primaryId as number | string, + shouldRunQuery + ); React.useEffect(() => { if (enabled && !query.isLoading && !query.data) { diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index 5fd7ada5b49..7e77a0ee4fd 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -96,6 +96,7 @@ export const migrationRouteTree = migrationRootRoute.addChildren([ placementGroupsRouteTree, stackScriptsRouteTree, volumesRouteTree, + vpcsRouteTree, ]); export type MigrationRouteTree = typeof migrationRouteTree; export const migrationRouter = createRouter({ diff --git a/packages/manager/src/routes/vpcs/index.ts b/packages/manager/src/routes/vpcs/index.ts index 00c9bfc5044..33ad75531ab 100644 --- a/packages/manager/src/routes/vpcs/index.ts +++ b/packages/manager/src/routes/vpcs/index.ts @@ -1,8 +1,35 @@ -import { createRoute } from '@tanstack/react-router'; +import { createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { VPCRoute } from './VPCRoute'; +import type { TableSearchParams } from '../types'; + +export interface SubnetSearchParams extends TableSearchParams { + query?: string; +} +const vpcAction = { + delete: 'delete', + edit: 'edit', +} as const; + +const subnetAction = { + assign: 'assign', + create: 'create', + delete: 'delete', + edit: 'edit', + unassign: 'unassign', +} as const; + +const subnetLinodeAction = { + 'power-action': 'power-action', + unassign: 'unassign', +} as const; + +export type VPCAction = typeof vpcAction[keyof typeof vpcAction]; +export type SubnetAction = typeof subnetAction[keyof typeof subnetAction]; +export type SubnetLinodeAction = typeof subnetLinodeAction[keyof typeof subnetLinodeAction]; + const vpcsRoute = createRoute({ component: VPCRoute, getParentRoute: () => rootRoute, @@ -12,32 +39,166 @@ const vpcsRoute = createRoute({ const vpcsLandingRoute = createRoute({ getParentRoute: () => vpcsRoute, path: '/', -}).lazy(() => - import('src/features/VPCs/VPCLanding/VPCLanding').then( - (m) => m.vpcLandingLazyRoute - ) -); +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcLandingLazyRoute)); + +type VPCActionRouteParams

    = { + action: VPCAction; + vpcId: P; +}; + +const vpcActionRouteParams = { + params: { + parse: ({ action, vpcId }: VPCActionRouteParams) => ({ + action, + vpcId: Number(vpcId), + }), + stringify: ({ action, vpcId }: VPCActionRouteParams) => ({ + action, + vpcId: String(vpcId), + }), + }, +}; + +const vpcActionRoute = createRoute({ + ...vpcActionRouteParams, + beforeLoad: async ({ params }) => { + if (!(params.action in vpcAction)) { + throw redirect({ + to: '/vpcs', + }); + } + }, + getParentRoute: () => vpcsLandingRoute, + path: '$vpcId/$action', +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcLandingLazyRoute)); const vpcsCreateRoute = createRoute({ getParentRoute: () => vpcsRoute, path: 'create', -}).lazy(() => - import('src/features/VPCs/VPCCreate/VPCCreate').then( - (m) => m.vpcCreateLazyRoute - ) -); +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcCreateLazyRoute)); const vpcsDetailRoute = createRoute({ getParentRoute: () => vpcsRoute, + parseParams: (params) => ({ + vpcId: Number(params.vpcId), + }), path: '$vpcId', -}).lazy(() => - import('src/features/VPCs/VPCDetail/VPCDetail').then( - (m) => m.vpcDetailLazyRoute - ) -); + validateSearch: (search: SubnetSearchParams) => search, +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); + +/** + * We must have different routes for the Edit and Delete modals on the VPC Landing page and the VPC Detail page, or we will get + * redirected to the Landing page whenever we try to view a modal on the VPC detail page. + * + * vpcs/$vpcId/detail/edit (detail page) <==> vpcs/$vpcId/edit (landing page) + * vpcs/$vpcId/detail/delete (detail page) <==> vpcs/$vpcId/delete (landing page) + */ +const vpcDetailActionRoute = createRoute({ + ...vpcActionRouteParams, + beforeLoad: async ({ params }) => { + if (!(params.action in vpcAction)) { + throw redirect({ + params: { + vpcId: params.vpcId, + }, + search: () => ({}), + to: `/vpcs/$vpcId`, + }); + } + }, + getParentRoute: () => vpcsDetailRoute, + path: 'detail/$action', +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); + +const subnetCreateRoute = createRoute({ + getParentRoute: () => vpcsDetailRoute, + path: 'subnets/create', + validateSearch: (search: SubnetSearchParams) => search, +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); + +const subnetDetailRoute = createRoute({ + getParentRoute: () => vpcsDetailRoute, + parseParams: (params) => ({ + subnetId: Number(params.subnetId), + }), + path: 'subnets/$subnetId', + validateSearch: (search: SubnetSearchParams) => search, +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); + +type SubnetActionRouteParams

    = { + subnetAction: SubnetAction; + subnetId: P; +}; + +const subnetActionRoute = createRoute({ + beforeLoad: async ({ params }) => { + if (!(params.subnetAction in subnetAction)) { + throw redirect({ + params: { + vpcId: params.vpcId, + }, + search: () => ({}), + to: `/vpcs/$vpcId`, + }); + } + }, + getParentRoute: () => subnetDetailRoute, + params: { + parse: ({ subnetAction }: SubnetActionRouteParams) => ({ + subnetAction, + }), + stringify: ({ subnetAction }: SubnetActionRouteParams) => ({ + subnetAction, + }), + }, + path: '$subnetAction', + validateSearch: (search: SubnetSearchParams) => search, +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); + +type SubnetLinodeActionRouteParams

    = { + linodeAction: SubnetLinodeAction; + linodeId: P; +}; + +const subnetLinodeActionRoute = createRoute({ + beforeLoad: async ({ params }) => { + if (!(params.linodeAction in subnetLinodeAction)) { + throw redirect({ + params: { + vpcId: params.vpcId, + }, + search: () => ({}), + to: `/vpcs/$vpcId`, + }); + } + }, + getParentRoute: () => subnetDetailRoute, + params: { + parse: ({ + linodeAction, + linodeId, + }: SubnetLinodeActionRouteParams) => ({ + linodeAction, + linodeId: Number(linodeId), + }), + stringify: ({ + linodeAction, + linodeId, + }: SubnetLinodeActionRouteParams) => ({ + linodeAction, + linodeId: String(linodeId), + }), + }, + path: '/linodes/$linodeId/$linodeAction', + validateSearch: (search: SubnetSearchParams) => search, +}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); export const vpcsRouteTree = vpcsRoute.addChildren([ - vpcsLandingRoute, + vpcsLandingRoute.addChildren([vpcActionRoute]), vpcsCreateRoute, - vpcsDetailRoute, + vpcsDetailRoute.addChildren([ + vpcDetailActionRoute, + subnetCreateRoute, + subnetDetailRoute.addChildren([subnetActionRoute, subnetLinodeActionRoute]), + ]), ]); diff --git a/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts b/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts new file mode 100644 index 00000000000..59b7f762898 --- /dev/null +++ b/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts @@ -0,0 +1,17 @@ +import { createLazyRoute } from '@tanstack/react-router'; + +import VPCCreate from 'src/features/VPCs/VPCCreate/VPCCreate'; +import VPCDetail from 'src/features/VPCs/VPCDetail/VPCDetail'; +import VPCLanding from 'src/features/VPCs/VPCLanding/VPCLanding'; + +export const vpcCreateLazyRoute = createLazyRoute('/vpcs/create')({ + component: VPCCreate, +}); + +export const vpcDetailLazyRoute = createLazyRoute('/vpcs/$vpcId')({ + component: VPCDetail, +}); + +export const vpcLandingLazyRoute = createLazyRoute('/')({ + component: VPCLanding, +}); diff --git a/packages/queries/src/vpcs/vpcs.ts b/packages/queries/src/vpcs/vpcs.ts index 4eac7b31a3f..233c21596ad 100644 --- a/packages/queries/src/vpcs/vpcs.ts +++ b/packages/queries/src/vpcs/vpcs.ts @@ -155,6 +155,16 @@ export const useSubnetsQuery = ( placeholderData: keepPreviousData, }); +export const useSubnetQuery = ( + vpcId: number, + subnetId: number, + enabled: boolean = true +) => + useQuery({ + ...vpcQueries.vpc(vpcId)._ctx.subnets._ctx.subnet(subnetId), + enabled, + }); + export const useCreateSubnetMutation = (vpcId: number) => { const queryClient = useQueryClient(); return useMutation({ From 3fd8df8ae79afb3ac431ac6a8c54dde455f30e20 Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:13:22 -0400 Subject: [PATCH 66/84] fix: [M3-9070] - Exclude `child_account` grant from PAT token calculations when it is hidden (#11935) * Exclude 'child_account' grant from calculations when it is hidden * Added changeset: PAT Token drawer logic when Child Account Access is hidden * Add cy spec for individually selecting all permissions --- .../pr-11935-fixed-1743110166866.md | 5 ++ .../account/personal-access-tokens.spec.ts | 66 +++++++++++++++++++ .../APITokens/CreateAPITokenDrawer.tsx | 26 ++++++-- .../src/features/Profile/APITokens/utils.ts | 18 +++-- 4 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 packages/manager/.changeset/pr-11935-fixed-1743110166866.md diff --git a/packages/manager/.changeset/pr-11935-fixed-1743110166866.md b/packages/manager/.changeset/pr-11935-fixed-1743110166866.md new file mode 100644 index 00000000000..e149756a0d0 --- /dev/null +++ b/packages/manager/.changeset/pr-11935-fixed-1743110166866.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) diff --git a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts index 81d1f357097..c133b491d55 100644 --- a/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/personal-access-tokens.spec.ts @@ -179,6 +179,72 @@ describe('Personal access tokens', () => { }); }); + it('sends scope as "*" when all permissions are set to read/write', () => { + const token = appTokenFactory.build({ + label: randomLabel(), + token: randomString(64), + }); + + mockCreatePersonalAccessToken(token).as('createToken'); + + cy.visitWithLogin('/profile/tokens'); + + // Click create button, fill out and submit PAT create form. + ui.button + .findByTitle('Create a Personal Access Token') + .should('be.visible') + .should('be.enabled') + .click(); + + ui.drawer + .findByTitle('Add Personal Access Token') + .should('be.visible') + .within(() => { + // Confirm that the “Child account access” grant is not visible in the list of permissions. + cy.findAllByText('Child Account Access').should('not.exist'); + + // Confirm submit button is disabled without specifying scopes. + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); + ui.buttonGroup.findButtonByTitle('Create Token').should('be.disabled'); + + // Select "Read/Write" for all scopes. + cy.get( + '[aria-label="Personal Access Token Permissions"] tr:gt(1)' + ).each((row) => + cy.wrap(row).within(() => { + cy.get('[type="radio"]').eq(2).click(); + }) + ); + + // Verify "Select All" radio for "Read/Write" is active + cy.get('[data-qa-perm-rw-radio]').should( + 'have.attr', + 'data-qa-radio', + 'true' + ); + + // Specify a label and submit. + cy.findByLabelText('Label').scrollIntoView(); + cy.findByLabelText('Label') + .should('be.visible') + .should('be.enabled') + .click(); + cy.findByLabelText('Label').type(token.label); + + ui.buttonGroup.findButtonByTitle('Create Token').scrollIntoView(); + ui.buttonGroup + .findButtonByTitle('Create Token') + .should('be.visible') + .should('be.enabled') + .click(); + }); + + // Confirm that new PAT's scopes are '*' + cy.wait('@createToken').then((xhr) => { + expect(xhr.request.body.scopes).to.equal('*'); + }); + }); + /* * - Uses mocked API requests to confirm UI flow when renaming and revoking tokens * - Confirms that list shows the correct label after renaming a token diff --git a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx index 225f9b6813a..82da40d92ae 100644 --- a/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx +++ b/packages/manager/src/features/Profile/APITokens/CreateAPITokenDrawer.tsx @@ -118,6 +118,10 @@ export const CreateAPITokenDrawer = (props: Props) => { globalGrantType: 'child_account_access', }); + // Visually hide the "Child Account Access" permission even though it's still part of the base perms. + const hideChildAccountAccessScope = + profile?.user_type !== 'parent' || isChildAccountAccessRestricted; + const form = useFormik<{ expiry: string; label: string; @@ -128,7 +132,10 @@ export const CreateAPITokenDrawer = (props: Props) => { const { token } = await createPersonalAccessToken({ expiry: values.expiry, label: values.label, - scopes: permTuplesToScopeString(values.scopes), + scopes: permTuplesToScopeString( + values.scopes, + hideChildAccountAccessScope ? ['child_account'] : [] + ), }); onClose(); showSecret(token ?? 'Secret not available'); @@ -186,6 +193,19 @@ export const CreateAPITokenDrawer = (props: Props) => { invalidAccessLevels: [levelMap.read_only], name: 'vpc', }, + ...(hideChildAccountAccessScope + ? [ + { + defaultAccessLevel: levelMap.hidden, + invalidAccessLevels: [ + levelMap.read_only, + levelMap.read_write, + levelMap.none, + ], + name: 'child_account', + }, + ] + : []), ]; const indexOfColumnWhereAllAreSelected = allScopesAreTheSame( @@ -202,10 +222,6 @@ export const CreateAPITokenDrawer = (props: Props) => { // Filter permissions for all users except parent user accounts. const allPermissions = form.values.scopes; - // Visually hide the "Child Account Access" permission even though it's still part of the base perms. - const hideChildAccountAccessScope = - profile?.user_type !== 'parent' || isChildAccountAccessRestricted; - return ( { if (scopeTups.length !== perms.length) { return false; } - return scopeTups.reduce( - (acc: boolean, [key, value]: Permission) => - value === levelMap.read_write && acc, - true + const excludeSet = new Set(exclude); + return scopeTups.every( + ([key, value]) => value === levelMap.read_write || excludeSet.has(key) ); }; -export const permTuplesToScopeString = (scopeTups: Permission[]): string => { - if (allMaxPerm(scopeTups, basePerms)) { +export const permTuplesToScopeString = ( + scopeTups: Permission[], + exclude: PermissionKey[] +): string => { + if (allMaxPerm(scopeTups, basePerms, exclude)) { return '*'; } const joinedTups = scopeTups.reduce((acc, [key, value]) => { From a374f99c3d7497b464e89bdbd1072beff48a2c6f Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Thu, 3 Apr 2025 08:06:33 -0700 Subject: [PATCH 67/84] fix: [M3-9689] - Fix unclearable ACL IP addresses for LKE clusters (#11947) * Allow empty array of addresses to be submitted * Show text fields even if there are no addresses in the array * Clear and disable other fields if ACL is toggled to disabled * Improve conditional * Fix submit button disabled incorrectly; update test coverage * Added changeset: Unclearable ACL IP addresses for LKE clusters * Fix test failures * Remove stray .only * Address PR feedback * Address UX feedback: restore form default values on enabled retoggle * Address feedback: reset IP fields to show textfield on toggle --- .../pr-11947-fixed-1743523539798.md | 5 ++ .../e2e/core/kubernetes/lke-update.spec.ts | 61 ++++++++++--------- .../KubeControlPaneACLDrawer.test.tsx | 35 +++++++++-- .../KubeControlPaneACLDrawer.tsx | 49 ++++++++++++--- 4 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 packages/manager/.changeset/pr-11947-fixed-1743523539798.md diff --git a/packages/manager/.changeset/pr-11947-fixed-1743523539798.md b/packages/manager/.changeset/pr-11947-fixed-1743523539798.md new file mode 100644 index 00000000000..486da047cc3 --- /dev/null +++ b/packages/manager/.changeset/pr-11947-fixed-1743523539798.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Unclearable ACL IP addresses for LKE clusters ([#11947](https://github.com/linode/manager/pull/11947)) diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts index 042570fcc64..4830646d369 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-update.spec.ts @@ -2646,28 +2646,32 @@ describe('LKE ACL updates', () => { /** * - Confirms ACL can be disabled from the summary page (for standard tier only) - * - Confirms both IPv4 and IPv6 can be updated and that drawer updates as a result */ - it('can disable ACL on a standard tier cluster and edit IPs', () => { + it('can disable ACL on a standard tier cluster', () => { const mockACLOptions = kubernetesControlPlaneACLOptionsFactory.build({ - addresses: { ipv4: undefined, ipv6: undefined }, + addresses: { + ipv4: [], + ipv6: [], + }, enabled: true, }); - const mockUpdatedACLOptions1 = kubernetesControlPlaneACLOptionsFactory.build( + + const mockDisabledACLOptions = kubernetesControlPlaneACLOptionsFactory.build( { addresses: { - ipv4: ['10.0.0.0/24'], - ipv6: ['8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e'], + ipv4: [''], + ipv6: [''], }, enabled: false, + 'revision-id': '', } ); const mockControlPaneACL = kubernetesControlPlaneACLFactory.build({ acl: mockACLOptions, }); - const mockUpdatedControlPlaneACL1 = kubernetesControlPlaneACLFactory.build( + const mockUpdatedControlPlaneACL = kubernetesControlPlaneACLFactory.build( { - acl: mockUpdatedACLOptions1, + acl: mockDisabledACLOptions, } ); @@ -2675,7 +2679,7 @@ describe('LKE ACL updates', () => { mockGetControlPlaneACL(mockCluster.id, mockControlPaneACL).as( 'getControlPlaneACL' ); - mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL1).as( + mockUpdateControlPlaneACL(mockCluster.id, mockUpdatedControlPlaneACL).as( 'updateControlPlaneACL' ); @@ -2719,27 +2723,16 @@ describe('LKE ACL updates', () => { // confirm Revision ID section cy.findByLabelText('Revision ID').should( 'have.value', - mockACLOptions['revision-id'] + mockDisabledACLOptions['revision-id'] ); - // Addresses Section: update IPv4 + // confirm IPv4 and IPv6 address sections cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') .should('be.visible') - .click(); - cy.focused().type('10.0.0.0/24'); - cy.findByText('Add IPv4 Address') - .should('be.visible') - .should('be.enabled') - .click(); - // update IPv6 + .should('have.value', mockDisabledACLOptions.addresses?.ipv4?.[0]); cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') .should('be.visible') - .click(); - cy.focused().type('8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e'); - cy.findByText('Add IPv6 Address') - .should('be.visible') - .should('be.enabled') - .click(); + .should('have.value', mockDisabledACLOptions.addresses?.ipv6?.[0]); // submit ui.button @@ -2754,7 +2747,7 @@ describe('LKE ACL updates', () => { // confirm summary panel updates cy.contains('Control Plane ACL').should('be.visible'); - cy.findByText('Enabled (O IP Addresses)').should('not.exist'); + cy.findByText('Enabled (0 IP Addresses)').should('not.exist'); ui.button .findByTitle('Enable') .should('be.visible') @@ -2772,11 +2765,19 @@ describe('LKE ACL updates', () => { .should('have.attr', 'data-qa-toggle', 'false') .should('be.visible'); - // confirm updated IP addresses display - cy.findByDisplayValue('10.0.0.0/24').should('be.visible'); - cy.findByDisplayValue( - '8e61:f9e9:8d40:6e0a:cbff:c97a:2692:827e' - ).should('be.visible'); + // confirm Revision ID section remains empty + cy.findByLabelText('Revision ID').should( + 'have.value', + mockDisabledACLOptions['revision-id'] + ); + + // confirm IPv4 and IPv6 address sections remain empty + cy.findByLabelText('IPv4 Addresses or CIDRs ip-address-0') + .should('be.visible') + .should('have.value', mockDisabledACLOptions.addresses?.ipv4?.[0]); + cy.findByLabelText('IPv6 Addresses or CIDRs ip-address-0') + .should('be.visible') + .should('have.value', mockDisabledACLOptions.addresses?.ipv6?.[0]); }); }); diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.test.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.test.tsx index d169d0cb110..42a4abfb234 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.test.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.test.tsx @@ -28,8 +28,8 @@ const queryMocks = vi.hoisted(() => ({ data: { acl: { addresses: { - ipv4: [''], - ipv6: [''], + ipv4: [], + ipv6: [], }, enabled: false, 'revision-id': '', @@ -49,9 +49,12 @@ vi.mock('src/queries/kubernetes', async () => { describe('KubeControlPaneACLDrawer', () => { it('renders the drawer as expected when the cluster is migrated', async () => { - const { getAllByText, getByText, queryByText } = renderWithTheme( - - ); + const { + getAllByTestId, + getAllByText, + getByText, + queryByText, + } = renderWithTheme(); expect(getByText('Control Plane ACL for Test')).toBeVisible(); expect(getByText(ACL_DRAWER_STANDARD_TIER_ACL_COPY)).toBeVisible(); @@ -83,6 +86,13 @@ describe('KubeControlPaneACLDrawer', () => { expect(getByText('IPv6 Addresses or CIDRs')).toBeVisible(); expect(getByText('Add IPv6 Address')).toBeVisible(); + // Confirm text input is disabled when ACL is disabled + const inputs = getAllByTestId('textfield-input'); + expect(inputs).toHaveLength(3); + inputs.forEach((input) => { + expect(input).toBeDisabled(); + }); + // Confirm notice does not display expect( queryByText( @@ -91,6 +101,21 @@ describe('KubeControlPaneACLDrawer', () => { ).not.toBeInTheDocument(); }); + it('confirms the revision ID and IP address fields are enabled when ACL is enabled', async () => { + const { getAllByTestId, getByText } = renderWithTheme( + + ); + + const toggle = getByText('Enable Control Plane ACL'); + await userEvent.click(toggle); + + const inputs = getAllByTestId('textfield-input'); + expect(inputs).toHaveLength(3); + inputs.forEach((input) => { + expect(input).toBeEnabled(); + }); + }); + it('shows a notice and hides revision ID if cluster is not migrated', () => { const { getByText, queryByText } = renderWithTheme( diff --git a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx index 875e16026b8..4e0a40985c9 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesClusterDetail/KubeControlPaneACLDrawer.tsx @@ -87,6 +87,7 @@ export const KubeControlPlaneACLDrawer = ( handleSubmit, reset, setError, + setValue, watch, } = useForm({ defaultValues: aclData, @@ -99,8 +100,12 @@ export const KubeControlPlaneACLDrawer = ( values: { acl: { addresses: { - ipv4: aclPayload?.addresses?.ipv4 ?? [''], - ipv6: aclPayload?.addresses?.ipv6 ?? [''], + ipv4: aclPayload?.addresses?.ipv4?.length + ? aclPayload?.addresses?.ipv4 + : [''], + ipv6: aclPayload?.addresses?.ipv6?.length + ? aclPayload?.addresses?.ipv6 + : [''], }, enabled: aclPayload?.enabled ?? false, 'revision-id': aclPayload?.['revision-id'] ?? '', @@ -150,12 +155,12 @@ export const KubeControlPlaneACLDrawer = ( acl: { enabled: acl.enabled, 'revision-id': acl['revision-id'], - ...((ipv4.length > 0 || ipv6.length > 0) && { + ...{ addresses: { - ...(ipv4.length > 0 && { ipv4 }), - ...(ipv6.length > 0 && { ipv6 }), + ipv4, + ipv6, }, - }), + }, }, }; @@ -233,10 +238,37 @@ export const KubeControlPlaneACLDrawer = ( checked={ isEnterpriseCluster ? true : field.value ?? false } + onChange={() => { + setValue('acl.enabled', !field.value, { + shouldDirty: true, + }); + // Disabling ACL should clear the revision-id and any addresses (see LKE-6205). + if (!acl.enabled) { + setValue('acl.revision-id', ''); + setValue('acl.addresses.ipv6', ['']); + setValue('acl.addresses.ipv4', ['']); + } else { + setValue( + 'acl.revision-id', + aclPayload?.['revision-id'] + ); + setValue( + 'acl.addresses.ipv6', + aclPayload?.addresses?.ipv6?.length + ? aclPayload?.addresses?.ipv6 + : [''] + ); + setValue( + 'acl.addresses.ipv4', + aclPayload?.addresses?.ipv4?.length + ? aclPayload?.addresses?.ipv4 + : [''] + ); + } + }} disabled={isEnterpriseCluster} name="ipacl-checkbox" onBlur={field.onBlur} - onChange={field.onChange} /> } label="Enable Control Plane ACL" @@ -260,6 +292,7 @@ export const KubeControlPlaneACLDrawer = ( ( ( ( Date: Thu, 3 Apr 2025 11:35:57 -0400 Subject: [PATCH 68/84] test [M3 9562]: remove hardcoded region id (#11948) * replace region unavailable in devcloud w/ random region * Added changeset: Fix test thats broken in devcloud --- .../manager/.changeset/pr-11948-tests-1743523964300.md | 5 +++++ .../core/linodes/create-linode-view-code-snippet.spec.ts | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 packages/manager/.changeset/pr-11948-tests-1743523964300.md diff --git a/packages/manager/.changeset/pr-11948-tests-1743523964300.md b/packages/manager/.changeset/pr-11948-tests-1743523964300.md new file mode 100644 index 00000000000..bf144dcd6c5 --- /dev/null +++ b/packages/manager/.changeset/pr-11948-tests-1743523964300.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tests +--- + +Fix test thats broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) diff --git a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts index 2ec04d6fcc7..b74046af2fc 100644 --- a/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts +++ b/packages/manager/cypress/e2e/core/linodes/create-linode-view-code-snippet.spec.ts @@ -6,6 +6,7 @@ import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { ui } from 'support/ui'; import { linodeCreatePage } from 'support/ui/pages'; import { randomLabel, randomString } from 'support/util/random'; +import { chooseRegion } from 'support/util/regions'; describe('Create Linode flow to validate code snippet modal', () => { beforeEach(() => { @@ -20,13 +21,15 @@ describe('Create Linode flow to validate code snippet modal', () => { it(`view code snippets in create linode flow`, () => { const linodeLabel = randomLabel(); const rootPass = randomString(32); - + const mockLinodeRegion = chooseRegion({ + capabilities: ['Linodes'], + }); cy.visitWithLogin('/linodes/create'); // Set Linode label, distribution, plan type, password, etc. linodeCreatePage.setLabel(linodeLabel); linodeCreatePage.selectImage('Debian 12'); - linodeCreatePage.selectRegionById('us-east'); + linodeCreatePage.selectRegionById(mockLinodeRegion.id); linodeCreatePage.selectPlan('Shared CPU', 'Nanode 1 GB'); linodeCreatePage.setRootPassword(rootPass); From 88135b4ebdadd8149ed31fce288db410a6c58471 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 3 Apr 2025 13:14:18 -0400 Subject: [PATCH 69/84] Update changelogs --- packages/api-v4/CHANGELOG.md | 2 +- packages/manager/CHANGELOG.md | 27 ++++++++++++++------------- packages/queries/CHANGELOG.md | 4 ++-- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 7d4497f5d66..ed0cf47d7ce 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -3,7 +3,7 @@ ### Added: -- DBaaS Advanced Configurations: added `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs ([#11812](https://github.com/linode/manager/pull/11812)) +- DBaaS Advanced Configurations: Add `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs ([#11812](https://github.com/linode/manager/pull/11812)) ### Upcoming Features: diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 02d56fcdc21..1dfa40ee052 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -13,27 +13,28 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Changed: -- Updated Breadcrumb component to conform to Akamai Design System specs ([#11841](https://github.com/linode/manager/pull/11841)) +- Update Breadcrumb component to conform to Akamai Design System specs ([#11841](https://github.com/linode/manager/pull/11841)) - Display interface type first in Linode Network IP Addresses table ([#11865](https://github.com/linode/manager/pull/11865)) -- Updated Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) +- Update Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) - Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) - Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) ### Fixed: -- Disable action menu for `Database` with tooltip text and enable `Delete Cluster` for `read/write` grant users ([#11890](https://github.com/linode/manager/pull/11890)) +- Database action menu incorrectly enabled with `read-only` grant and `Delete Cluster` button incorrectly disabled with `read/write` grant ([#11890](https://github.com/linode/manager/pull/11890)) - Tabs keyboard navigation on some Tanstack rerouted features ([#11894](https://github.com/linode/manager/pull/11894)) ### Removed: -- Ramda from `Utilities` ([#11861](https://github.com/linode/manager/pull/11861)) + +- Ramda from `Utilities` package ([#11861](https://github.com/linode/manager/pull/11861)) - Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) - Move `regionsData` from `manager` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) - Move `LinodeCreateType` to `utilities` package ([#11790](https://github.com/linode/manager/pull/11790)) - Move `LinodeSelect` to new `shared` package ([#11844](https://github.com/linode/manager/pull/11844)) - Legacy BetaChip component ([#11872](https://github.com/linode/manager/pull/11872)) - Move `doesRegionSupportFeature` from `manager` to `utilities` package ([#11891](https://github.com/linode/manager/pull/11891)) -- Moved Tags-related queries and dependencies to shares `queries` package ([#11897](https://github.com/linode/manager/pull/11897)) -- Moved Support-related queries and dependencies to shared `queries` package ([#11904](https://github.com/linode/manager/pull/11904)) +- Move Tags-related queries and dependencies to shares `queries` package ([#11897](https://github.com/linode/manager/pull/11897)) +- Move Support-related queries and dependencies to shared `queries` package ([#11904](https://github.com/linode/manager/pull/11904)) - Move `luxon` dependent utils from `manager` to `utilities` package ([#11905](https://github.com/linode/manager/pull/11905)) - Move ramda dependent utils ([#11913](https://github.com/linode/manager/pull/11913)) - Move `useIsGeckoEnabled` hook out of `RegionSelect` to `@linode/shared` package ([#11918](https://github.com/linode/manager/pull/11918)) @@ -51,24 +52,24 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Tests: -- html presentation for cypress test results ([#11795](https://github.com/linode/manager/pull/11795)) +- Add HTML report generation for Cypress test results ([#11795](https://github.com/linode/manager/pull/11795)) - Add `env:premiumPlans` test tag for tests which require premium plan availability ([#11886](https://github.com/linode/manager/pull/11886)) - Fix Linode create end-to-end test failures against alternative environments ([#11886](https://github.com/linode/manager/pull/11886)) - Delete redundant Linode create SSH key test ([#11886](https://github.com/linode/manager/pull/11886)) - Add test for Add Linode Interface drawer ([#11887](https://github.com/linode/manager/pull/11887)) - Prevent legacy regions from being used by Cypress tests ([#11892](https://github.com/linode/manager/pull/11892)) - Temporarily skip Firewall end-to-end tests ([#11898](https://github.com/linode/manager/pull/11898)) -- Tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) +- Add tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) ### Upcoming Features: -- DBaaS Advanced Configurations: added UI for existing engine options in the drawer ([#11812](https://github.com/linode/manager/pull/11812)) +- DBaaS Advanced Configurations: Add UI for existing engine options in the drawer ([#11812](https://github.com/linode/manager/pull/11812)) - Add Default Firewalls paper to Account Settings ([#11828](https://github.com/linode/manager/pull/11828)) - Add functionality to support the 'Assign New Roles' drawer for a single user in IAM ([#11834](https://github.com/linode/manager/pull/11834)) - Update Firewall Devices Linode landing table to account for new interface devices ([#11842](https://github.com/linode/manager/pull/11842)) -- Quotas Tab Beta Chip ([#11872](https://github.com/linode/manager/pull/11872)) -- Add AlertListNoticeMessages component for handling multiple API error messages, update AddChannelListing and MetricCriteria components to display these errors, Add handleMultipleError util method for aggregating, mapping the errors to fields. ([#11874](https://github.com/linode/manager/pull/11874)) -- Diable query to get Linode Interface when Interface Delete dialog is closed ([#11881](https://github.com/linode/manager/pull/11881)) +- Add Quotas Tab Beta Chip ([#11872](https://github.com/linode/manager/pull/11872)) +- Add AlertListNoticeMessages component for handling multiple API error messages, update AddChannelListing and MetricCriteria components to display these errors, add handleMultipleError util method for aggregating, mapping the errors to fields ([#11874](https://github.com/linode/manager/pull/11874)) +- Disable query to get Linode Interface when Interface Delete dialog is closed ([#11881](https://github.com/linode/manager/pull/11881)) - Update title for Delete Interface dialog ([#11881](https://github.com/linode/manager/pull/11881)) - Add VPC support to the Add Network Interface Drawer ([#11887](https://github.com/linode/manager/pull/11887)) - Add Interface Details drawer for Linode Interfaces ([#11888](https://github.com/linode/manager/pull/11888)) @@ -77,7 +78,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Update success message for create/edit/enable/disable alert at `CreateAlertDefinition.tsx`, `EditAlertDefinition.tsx`, and `AlertListTable.tsx` ([#11903](https://github.com/linode/manager/pull/11903)) - Update Firewall Landing table to account for Linode Interface devices and Default Firewalls ([#11920](https://github.com/linode/manager/pull/11920)) - Add Default Firewall chips to Firewall Detail page ([#11920](https://github.com/linode/manager/pull/11920)) -- remove preselected role from Change Role drawer ([#11926](https://github.com/linode/manager/pull/11926)) +- Remove preselected role from Change Role drawer ([#11926](https://github.com/linode/manager/pull/11926)) - Adjust logic for displaying encryption status on Linode Details page and encryption copy on LKE Create page ([#11930](https://github.com/linode/manager/pull/11930)) ## [2025-03-26] - v1.138.1 diff --git a/packages/queries/CHANGELOG.md b/packages/queries/CHANGELOG.md index 0197b0f782d..62935207291 100644 --- a/packages/queries/CHANGELOG.md +++ b/packages/queries/CHANGELOG.md @@ -3,8 +3,8 @@ ### Added: -- Created `tags/` directory and migrated relevant query keys and hooks ([#11897](https://github.com/linode/manager/pull/11897)) -- Created `support/` directory and migrated relevant query keys and hooks ([#11904](https://github.com/linode/manager/pull/11904)) +- `tags/` directory and migrated relevant query keys and hooks ([#11897](https://github.com/linode/manager/pull/11897)) +- `support/` directory and migrated relevant query keys and hooks ([#11904](https://github.com/linode/manager/pull/11904)) ### Upcoming Features: From 0172b47f5a557c5fe5d5adae157a4a8df66906a5 Mon Sep 17 00:00:00 2001 From: Hana Xu <115299789+hana-akamai@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:57:17 -0400 Subject: [PATCH 70/84] upcoming: [M3-9515] - Update types for VPC IPs (#11938) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description 📝 Update types for VPC IPs to support IPv6 ## How to test 🧪 ### Verification steps (How to verify changes) - [ ] Check type updates match API spec (linked in parent ticket) for `GET` `/v4/vpcs/ips` and `/v4/vpcs/{vpcId}/ips` --- .../.changeset/pr-11938-upcoming-features-1743446020892.md | 5 +++++ packages/api-v4/src/vpcs/types.ts | 5 +++++ .../.changeset/pr-11938-upcoming-features-1743446079043.md | 5 +++++ packages/manager/src/factories/vpcs.ts | 7 +++++++ 4 files changed, 22 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md create mode 100644 packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md diff --git a/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md b/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md new file mode 100644 index 00000000000..1d822cd32d9 --- /dev/null +++ b/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Upcoming Features +--- + +Add support for IPv6 to `VPCIP` ([#11938](https://github.com/linode/manager/pull/11938)) diff --git a/packages/api-v4/src/vpcs/types.ts b/packages/api-v4/src/vpcs/types.ts index 025ffd80a6b..2626c8de526 100644 --- a/packages/api-v4/src/vpcs/types.ts +++ b/packages/api-v4/src/vpcs/types.ts @@ -66,6 +66,11 @@ export interface VPCIP { active: boolean; address: string | null; address_range: string | null; + ipv6_range: string | null; + ipv6_is_public: boolean | null; + ipv6_addresses: { + slaac_address: string; + }[]; config_id: number | null; gateway: string | null; interface_id: number; diff --git a/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md b/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md new file mode 100644 index 00000000000..48f58ebeb31 --- /dev/null +++ b/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Update `vpcIPFactory` to support IPv6 ([#11938](https://github.com/linode/manager/pull/11938)) diff --git a/packages/manager/src/factories/vpcs.ts b/packages/manager/src/factories/vpcs.ts index 298a2907e84..b475274c1ce 100644 --- a/packages/manager/src/factories/vpcs.ts +++ b/packages/manager/src/factories/vpcs.ts @@ -19,6 +19,13 @@ export const vpcIPFactory = Factory.Sync.makeFactory({ config_id: Factory.each((i) => i), gateway: '192.0.2.1', interface_id: Factory.each((i) => i), + ipv6_addresses: [ + { + slaac_address: '2001:DB8::0000', + }, + ], + ipv6_is_public: null, + ipv6_range: null, linode_id: Factory.each((i) => i), nat_1_1: '192.0.2.97', prefix: 24, From 4a7a641066ce8fca4d2e453c0d1a0c7985395729 Mon Sep 17 00:00:00 2001 From: cpathipa <119517080+cpathipa@users.noreply.github.com> Date: Thu, 3 Apr 2025 13:25:13 -0500 Subject: [PATCH 71/84] feat: [M3-9632] - NewFeatureChip component (#11965) * Add NewFeatureChip component * Update styles for BetaChip component * Update BetaChip usage and component test * Added changeset: Updated BetaChip styles, its usage and updated BetaChip component tests * Added changeset: A new `NewFeatureChip` component and updated BetaChip styles * Delete packages/manager/.changeset/pr-11965-changed-1743702322230.md * Delete packages/ui/.changeset/pr-11965-added-1743702343146.md * update comments * code cleanup --- .../component/components/beta-chip.spec.tsx | 18 ++--- .../src/components/PrimaryNav/PrimaryLink.tsx | 6 +- .../src/features/Account/AccountLanding.tsx | 2 +- .../features/Account/NetworkInterfaceType.tsx | 2 +- .../Databases/DatabaseDetail/index.tsx | 4 +- .../DatabaseLanding/DatabaseLogo.tsx | 14 +--- .../components/BetaChip/BetaChip.stories.tsx | 17 +++++ .../src/components/BetaChip/BetaChip.test.tsx | 15 +--- .../ui/src/components/BetaChip/BetaChip.tsx | 72 +++++++------------ .../NewFeatureChip/NewFeatureChip.stories.tsx | 17 +++++ .../NewFeatureChip/NewFeatureChip.test.tsx | 26 +++++++ .../NewFeatureChip/NewFeatureChip.tsx | 57 +++++++++++++++ .../ui/src/components/NewFeatureChip/index.ts | 1 + packages/ui/src/components/index.ts | 1 + 14 files changed, 157 insertions(+), 95 deletions(-) create mode 100644 packages/ui/src/components/BetaChip/BetaChip.stories.tsx create mode 100644 packages/ui/src/components/NewFeatureChip/NewFeatureChip.stories.tsx create mode 100644 packages/ui/src/components/NewFeatureChip/NewFeatureChip.test.tsx create mode 100644 packages/ui/src/components/NewFeatureChip/NewFeatureChip.tsx create mode 100644 packages/ui/src/components/NewFeatureChip/index.ts diff --git a/packages/manager/cypress/component/components/beta-chip.spec.tsx b/packages/manager/cypress/component/components/beta-chip.spec.tsx index b05c7b501da..dff3a4aa0f7 100644 --- a/packages/manager/cypress/component/components/beta-chip.spec.tsx +++ b/packages/manager/cypress/component/components/beta-chip.spec.tsx @@ -5,23 +5,13 @@ import { componentTests, visualTests } from 'support/util/components'; componentTests('BetaChip', () => { visualTests((mount) => { - it('renders "BETA" text indicator with primary color', () => { - mount(); + it('renders "BETA" text indicator', () => { + mount(); cy.findByText('beta').should('be.visible'); }); - it('renders "BETA" text indicator with default color', () => { - mount(); - cy.findByText('beta').should('be.visible'); - }); - - it('passes aXe check with primary color', () => { - mount(); - checkComponentA11y(); - }); - - it('passes aXe check with default color', () => { - mount(); + it('passes aXe accessibility', () => { + mount(); checkComponentA11y(); }); }); diff --git a/packages/manager/src/components/PrimaryNav/PrimaryLink.tsx b/packages/manager/src/components/PrimaryNav/PrimaryLink.tsx index f2f50d090eb..2a85aec40ac 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryLink.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryLink.tsx @@ -61,11 +61,7 @@ const PrimaryLink = React.memo((props: PrimaryLinkProps) => { > {display} {isBeta ? ( - + ) : null} diff --git a/packages/manager/src/features/Account/AccountLanding.tsx b/packages/manager/src/features/Account/AccountLanding.tsx index a047349a024..dd2bf476489 100644 --- a/packages/manager/src/features/Account/AccountLanding.tsx +++ b/packages/manager/src/features/Account/AccountLanding.tsx @@ -90,7 +90,7 @@ const AccountLanding = () => { ...(showQuotasTab ? [ { - chip: , + chip: , routeName: '/account/quotas', title: 'Quotas', }, diff --git a/packages/manager/src/features/Account/NetworkInterfaceType.tsx b/packages/manager/src/features/Account/NetworkInterfaceType.tsx index 1010008239d..b6a472432ae 100644 --- a/packages/manager/src/features/Account/NetworkInterfaceType.tsx +++ b/packages/manager/src/features/Account/NetworkInterfaceType.tsx @@ -81,7 +81,7 @@ export const NetworkInterfaceType = () => { } + headingChip={} >
    diff --git a/packages/manager/src/features/Databases/DatabaseDetail/index.tsx b/packages/manager/src/features/Databases/DatabaseDetail/index.tsx index c07b8bc8684..2f59cb0e70c 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/index.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/index.tsx @@ -114,9 +114,7 @@ export const DatabaseDetail = () => { if (isMonitorEnabled) { tabs.splice(1, 0, { - chip: flags.dbaasV2MonitorMetrics?.beta ? ( - - ) : null, + chip: flags.dbaasV2MonitorMetrics?.beta ? : null, routeName: `/databases/${engine}/${id}/monitor`, title: 'Monitor', }); diff --git a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx index 85f20563d79..fcbd66ac17f 100644 --- a/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx +++ b/packages/manager/src/features/Databases/DatabaseLanding/DatabaseLogo.tsx @@ -23,19 +23,7 @@ export const DatabaseLogo = ({ sx }: Props) => { sx={sx ? sx : { margin: '20px' }} > - {!isDatabasesV2GA && ( - - )} + {!isDatabasesV2GA && } = { + render: (args) => , +}; + +const meta: Meta = { + args: {}, + component: BetaChip, + title: 'Foundations/Chip/BetaChip', +}; +export default meta; diff --git a/packages/ui/src/components/BetaChip/BetaChip.test.tsx b/packages/ui/src/components/BetaChip/BetaChip.test.tsx index b9300601a24..c30e2b1b68d 100644 --- a/packages/ui/src/components/BetaChip/BetaChip.test.tsx +++ b/packages/ui/src/components/BetaChip/BetaChip.test.tsx @@ -7,25 +7,16 @@ import { renderWithTheme } from '../../utilities/testHelpers'; import { BetaChip } from './BetaChip'; describe('BetaChip', () => { - it('renders with default color (primary)', () => { + it('renders with default color', () => { const { getByTestId } = renderWithTheme(); const betaChip = getByTestId('betaChip'); expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: rgba(0, 0, 0, 0.08)'); - }); - - it('renders with primary color', () => { - const { getByTestId } = renderWithTheme(); - const betaChip = getByTestId('betaChip'); - expect(betaChip).toBeInTheDocument(); - expect(betaChip).toHaveStyle('background-color: rgb(131, 131, 140)'); + expect(betaChip).toHaveStyle('background-color: rgb(105, 105, 112)'); }); it('triggers an onClick callback', () => { const onClickMock = vi.fn(); - const { getByTestId } = renderWithTheme( - - ); + const { getByTestId } = renderWithTheme(); const betaChip = getByTestId('betaChip'); fireEvent.click(betaChip); expect(onClickMock).toHaveBeenCalledTimes(1); diff --git a/packages/ui/src/components/BetaChip/BetaChip.tsx b/packages/ui/src/components/BetaChip/BetaChip.tsx index a5c870b99b1..9c7bc131a37 100644 --- a/packages/ui/src/components/BetaChip/BetaChip.tsx +++ b/packages/ui/src/components/BetaChip/BetaChip.tsx @@ -1,74 +1,54 @@ import { styled } from '@mui/material/styles'; import * as React from 'react'; +import { Global } from '@linode/design-language-system'; import { Chip } from '../Chip'; import type { ChipProps } from '@mui/material'; -export interface BetaChipProps - extends Omit< - ChipProps, - | 'avatar' - | 'clickable' - | 'deleteIcon' - | 'disabled' - | 'icon' - | 'label' - | 'onDelete' - | 'outlineColor' - | 'size' - | 'variant' - > { - /** - * The color of the chip. - * default renders a gray chip, primary renders a blue chip. - */ - color?: 'primary' | 'secondary'; -} +export type BetaChipProps = Omit< + ChipProps, + | 'avatar' + | 'clickable' + | 'deleteIcon' + | 'disabled' + | 'icon' + | 'label' + | 'onDelete' + | 'outlineColor' + | 'size' + | 'variant' +>; /** * ## Usage * - * Beta chips label features that are not yet part of Cloud Manager's core supported functionality.
    + * BetaChip is used when a feature is available to a limited number of users as part of a beta rollout.
    * **Example:** A beta chip may appear in the [primary navigation](https://github.com/linode/manager/pull/8104#issuecomment-1309334374), * breadcrumbs, [banners](/docs/components-notifications-dismissible-banners--beta-banners), tabs, and/or plain text to designate beta functionality.
    * **Visual style:** bold, capitalized text; reduced height, letter spacing, and font size; solid color background. * */ export const BetaChip = (props: BetaChipProps) => { - const { color = 'primary' } = props; - - return ( - - ); + return ; }; const StyledBetaChip = styled(Chip, { label: 'StyledBetaChip', shouldForwardProp: (prop) => prop !== 'color', -})(({ color, theme }) => ({ +})(({ theme }) => ({ '& .MuiChip-label': { padding: 0, }, - background: - color === 'primary' - ? 'lch(77.7 28.7 275 / 0.12)' - : theme.tokens.color.Neutrals[60], - color: - color === 'primary' - ? theme.tokens.color.Ultramarine[50] - : theme.tokens.color.Neutrals.White, + background: Global.Color.Neutrals[70], + color: Global.Color.Neutrals.White, - font: theme.font.bold, - fontSize: '0.625rem', + fontWeight: theme.tokens.font.FontWeight.Extrabold, + fontSize: '11px', + lineHeight: '12px', height: 16, - letterSpacing: '.25px', - marginLeft: theme.spacing(), - padding: theme.spacing(0.5), - textTransform: 'uppercase', + letterSpacing: '.22px', + marginLeft: theme.spacingFunction(8), + padding: theme.spacingFunction(4), + textTransform: theme.tokens.font.Textcase.Uppercase, })); diff --git a/packages/ui/src/components/NewFeatureChip/NewFeatureChip.stories.tsx b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.stories.tsx new file mode 100644 index 00000000000..10c6e89a73e --- /dev/null +++ b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.stories.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +import { NewFeatureChip } from './NewFeatureChip'; + +import type { NewFeatureChipProps } from './NewFeatureChip'; +import type { Meta, StoryObj } from '@storybook/react'; + +export const Default: StoryObj = { + render: (args) => , +}; + +const meta: Meta = { + args: { color: 'default' }, + component: NewFeatureChip, + title: 'Foundations/Chip/NewFeatureChip', +}; +export default meta; diff --git a/packages/ui/src/components/NewFeatureChip/NewFeatureChip.test.tsx b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.test.tsx new file mode 100644 index 00000000000..abfe861604b --- /dev/null +++ b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.test.tsx @@ -0,0 +1,26 @@ +import '@testing-library/jest-dom/vitest'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { renderWithTheme } from '../../utilities/testHelpers'; +import { NewFeatureChip } from './NewFeatureChip'; + +describe('NewFeatureChip', () => { + it('renders with default color', () => { + const { getByTestId } = renderWithTheme(); + const newFeatureChip = getByTestId('newFeatureChip'); + expect(newFeatureChip).toBeInTheDocument(); + expect(newFeatureChip).toHaveStyle('background-color: rgb(114, 89, 214)'); + }); + + it('triggers an onClick callback', () => { + const onClickMock = vi.fn(); + const { getByTestId } = renderWithTheme( + + ); + const newFeatureChip = getByTestId('newFeatureChip'); + fireEvent.click(newFeatureChip); + expect(onClickMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/ui/src/components/NewFeatureChip/NewFeatureChip.tsx b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.tsx new file mode 100644 index 00000000000..5b3da06eef9 --- /dev/null +++ b/packages/ui/src/components/NewFeatureChip/NewFeatureChip.tsx @@ -0,0 +1,57 @@ +import { styled } from '@mui/material/styles'; +import { Global } from '@linode/design-language-system'; +import * as React from 'react'; + +import { Chip } from '../Chip'; + +import type { ChipProps } from '@mui/material'; + +export type NewFeatureChipProps = Omit< + ChipProps, + | 'avatar' + | 'clickable' + | 'deleteIcon' + | 'disabled' + | 'icon' + | 'label' + | 'onDelete' + | 'outlineColor' + | 'size' + | 'variant' +>; + +/** + * ## Usage + * + * The NewFeatureChip is displayed to all users after the feature has been fully rolled out.
    + * **Example:** A NewFeatureChip chip may appear in the primary navigation, + * breadcrumbs, banners, tabs, and/or plain text to designate new functionality and improve visibility for all the users.
    + * **Visual style:** bold, capitalized text; reduced height, letter spacing, and font size; solid color background. + * + */ +export const NewFeatureChip = (props: NewFeatureChipProps) => { + return ( + + ); +}; + +const StyledNewFeatureChip = styled(Chip, { + label: 'StyledNewFeatureChip', + shouldForwardProp: (prop) => prop !== 'color', +})(({ theme }) => ({ + '& .MuiChip-label': { + padding: 0, + }, + background: Global.Color.Violet[70], + color: Global.Color.Neutrals.White, + + font: theme.font.bold, + fontSize: '11px', + fontWeight: theme.tokens.font.FontWeight.Extrabold, + lineHeight: '12px', + height: 16, + letterSpacing: '.22px', + marginLeft: theme.spacingFunction(8), + padding: theme.spacingFunction(4), + textTransform: theme.tokens.font.Textcase.Uppercase, +})); diff --git a/packages/ui/src/components/NewFeatureChip/index.ts b/packages/ui/src/components/NewFeatureChip/index.ts new file mode 100644 index 00000000000..d7c6ceaeaca --- /dev/null +++ b/packages/ui/src/components/NewFeatureChip/index.ts @@ -0,0 +1 @@ +export * from './NewFeatureChip'; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index bc0a56a62bd..375e7648336 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -34,6 +34,7 @@ export * from './Stack'; export * from './SvgIcon'; export * from './TextField'; export * from './Toggle'; +export * from './NewFeatureChip'; export * from './Tooltip'; export * from './TooltipIcon'; export * from './Typography'; From 59da96880aa020a1f928085a531b5131931bad49 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:35:57 -0400 Subject: [PATCH 72/84] Update packages/manager/CHANGELOG.md Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> --- packages/manager/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 1dfa40ee052..b8d922f2aee 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Update Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) - Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) - Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) +- Update BetaChip styles, its usage and updated BetaChip component tests ([#11965](https://github.com/linode/manager/pull/11965)) ### Fixed: From a90ccf8a0aae64d1994794d1fd96e654b69d1b3a Mon Sep 17 00:00:00 2001 From: Jaalah Ramos <125309814+jaalah-akamai@users.noreply.github.com> Date: Thu, 3 Apr 2025 14:36:12 -0400 Subject: [PATCH 73/84] Update packages/ui/CHANGELOG.md Co-authored-by: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> --- packages/ui/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 375b81500a9..979c48bdbd1 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added: - Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) +- A new `NewFeatureChip` component and updated BetaChip styles ([#11965](https://github.com/linode/manager/pull/11965)) ### Changed: From bd81b0e43e69ea2d750bd22ff87228cd213a1f9e Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 3 Apr 2025 15:18:05 -0400 Subject: [PATCH 74/84] Update changelogs --- .../pr-11885-changed-1742400890057.md | 5 ----- .../pr-11909-removed-1742830690604.md | 5 ----- ...r-11919-upcoming-features-1742907313076.md | 5 ----- ...r-11938-upcoming-features-1743446020892.md | 5 ----- .../pr-11939-fixed-1743535108177.md | 5 ----- packages/api-v4/CHANGELOG.md | 14 +++++++++++++ ...r-11885-upcoming-features-1742401242206.md | 5 ----- .../pr-11909-removed-1742830913597.md | 5 ----- ...r-11915-upcoming-features-1742915363501.md | 5 ----- ...r-11915-upcoming-features-1742915406867.md | 5 ----- ...r-11919-upcoming-features-1742907356639.md | 5 ----- .../pr-11931-removed-1743075421803.md | 5 ----- .../pr-11933-fixed-1743104278261.md | 5 ----- .../pr-11935-fixed-1743110166866.md | 5 ----- ...r-11938-upcoming-features-1743446079043.md | 5 ----- .../pr-11939-tests-1743444706872.md | 5 ----- .../pr-11945-changed-1743511226551.md | 5 ----- .../pr-11946-fixed-1743519262429.md | 5 ----- .../pr-11947-fixed-1743523539798.md | 5 ----- .../pr-11948-tests-1743523964300.md | 5 ----- .../pr-11949-removed-1743525729687.md | 5 ----- .../pr-11950-tech-stories-1743528301761.md | 5 ----- .../pr-11951-tests-1743535607155.md | 5 ----- .../pr-11952-tech-stories-1743537321600.md | 5 ----- .../pr-11954-changed-1743543232723.md | 5 ----- packages/manager/CHANGELOG.md | 21 +++++++++++++++++++ .../pr-11949-added-1743525787297.md | 5 ----- packages/queries/CHANGELOG.md | 1 + .../pr-11946-added-1743519284653.md | 5 ----- packages/ui/CHANGELOG.md | 1 + .../pr-11931-added-1743075469873.md | 5 ----- packages/utilities/CHANGELOG.md | 1 + 32 files changed, 38 insertions(+), 135 deletions(-) delete mode 100644 packages/api-v4/.changeset/pr-11885-changed-1742400890057.md delete mode 100644 packages/api-v4/.changeset/pr-11909-removed-1742830690604.md delete mode 100644 packages/api-v4/.changeset/pr-11919-upcoming-features-1742907313076.md delete mode 100644 packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md delete mode 100644 packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md delete mode 100644 packages/manager/.changeset/pr-11885-upcoming-features-1742401242206.md delete mode 100644 packages/manager/.changeset/pr-11909-removed-1742830913597.md delete mode 100644 packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md delete mode 100644 packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md delete mode 100644 packages/manager/.changeset/pr-11919-upcoming-features-1742907356639.md delete mode 100644 packages/manager/.changeset/pr-11931-removed-1743075421803.md delete mode 100644 packages/manager/.changeset/pr-11933-fixed-1743104278261.md delete mode 100644 packages/manager/.changeset/pr-11935-fixed-1743110166866.md delete mode 100644 packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md delete mode 100644 packages/manager/.changeset/pr-11939-tests-1743444706872.md delete mode 100644 packages/manager/.changeset/pr-11945-changed-1743511226551.md delete mode 100644 packages/manager/.changeset/pr-11946-fixed-1743519262429.md delete mode 100644 packages/manager/.changeset/pr-11947-fixed-1743523539798.md delete mode 100644 packages/manager/.changeset/pr-11948-tests-1743523964300.md delete mode 100644 packages/manager/.changeset/pr-11949-removed-1743525729687.md delete mode 100644 packages/manager/.changeset/pr-11950-tech-stories-1743528301761.md delete mode 100644 packages/manager/.changeset/pr-11951-tests-1743535607155.md delete mode 100644 packages/manager/.changeset/pr-11952-tech-stories-1743537321600.md delete mode 100644 packages/manager/.changeset/pr-11954-changed-1743543232723.md delete mode 100644 packages/queries/.changeset/pr-11949-added-1743525787297.md delete mode 100644 packages/ui/.changeset/pr-11946-added-1743519284653.md delete mode 100644 packages/utilities/.changeset/pr-11931-added-1743075469873.md diff --git a/packages/api-v4/.changeset/pr-11885-changed-1742400890057.md b/packages/api-v4/.changeset/pr-11885-changed-1742400890057.md deleted file mode 100644 index fa7e83838ff..00000000000 --- a/packages/api-v4/.changeset/pr-11885-changed-1742400890057.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Changed ---- - -DBaaS Advanced Configurations: remove `engine_config` from the DatabaseEngineConfig type ([#11885](https://github.com/linode/manager/pull/11885)) diff --git a/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md b/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md deleted file mode 100644 index c1183527bc4..00000000000 --- a/packages/api-v4/.changeset/pr-11909-removed-1742830690604.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Removed ---- - -DBaaS: unused functions getDatabaseType, getEngineDatabases, getDatabaseBackup ([#11909](https://github.com/linode/manager/pull/11909)) diff --git a/packages/api-v4/.changeset/pr-11919-upcoming-features-1742907313076.md b/packages/api-v4/.changeset/pr-11919-upcoming-features-1742907313076.md deleted file mode 100644 index 2131da98ab1..00000000000 --- a/packages/api-v4/.changeset/pr-11919-upcoming-features-1742907313076.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Upcoming Features ---- - -update iam apis ([#11919](https://github.com/linode/manager/pull/11919)) diff --git a/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md b/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md deleted file mode 100644 index 1d822cd32d9..00000000000 --- a/packages/api-v4/.changeset/pr-11938-upcoming-features-1743446020892.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Upcoming Features ---- - -Add support for IPv6 to `VPCIP` ([#11938](https://github.com/linode/manager/pull/11938)) diff --git a/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md b/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md deleted file mode 100644 index c74fcdc193b..00000000000 --- a/packages/api-v4/.changeset/pr-11939-fixed-1743535108177.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/api-v4": Fixed ---- - -Remove trailing slash from outgoing Linode API GET request ([#11939](https://github.com/linode/manager/pull/11939)) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index ed0cf47d7ce..85e30c3389b 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -5,10 +5,24 @@ - DBaaS Advanced Configurations: Add `getDatabaseEngineConfig` request to fetch all advanced configurations and updated types for advanced configs ([#11812](https://github.com/linode/manager/pull/11812)) +### Changed: + +- DBaaS Advanced Configurations: remove `engine_config` from the DatabaseEngineConfig type ([#11885](https://github.com/linode/manager/pull/11885)) + +### Fixed: + +- Remove trailing slash from outgoing Linode API GET request ([#11939](https://github.com/linode/manager/pull/11939)) + +### Removed: + +- DBaaS: unused functions getDatabaseType, getEngineDatabases, getDatabaseBackup ([#11909](https://github.com/linode/manager/pull/11909)) + ### Upcoming Features: - Add `/v4beta/nodebalancers` and `/v4/nodebalancers` endpoints for NB-VPC Integration ([#11832](https://github.com/linode/manager/pull/11832)) - Update `ipv6` type in `CreateSubnetPayload` and rename `createSubnetSchema` to `createSubnetSchemaIPv4` ([#11896](https://github.com/linode/manager/pull/11896)) +- Update iam apis ([#11919](https://github.com/linode/manager/pull/11919)) +- Add support for IPv6 to `VPCIP` ([#11938](https://github.com/linode/manager/pull/11938)) ## [2025-03-25] - v0.136.0 diff --git a/packages/manager/.changeset/pr-11885-upcoming-features-1742401242206.md b/packages/manager/.changeset/pr-11885-upcoming-features-1742401242206.md deleted file mode 100644 index 4c2aab83962..00000000000 --- a/packages/manager/.changeset/pr-11885-upcoming-features-1742401242206.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -DBaaS Advanced Configurations: set up Autocomplete to display categorized options, add/remove configs, and implement a dynamic validation schema for all field types ([#11885](https://github.com/linode/manager/pull/11885)) diff --git a/packages/manager/.changeset/pr-11909-removed-1742830913597.md b/packages/manager/.changeset/pr-11909-removed-1742830913597.md deleted file mode 100644 index ec908188ceb..00000000000 --- a/packages/manager/.changeset/pr-11909-removed-1742830913597.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary ([#11909](https://github.com/linode/manager/pull/11909)) diff --git a/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md b/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md deleted file mode 100644 index a8863fbd4d3..00000000000 --- a/packages/manager/.changeset/pr-11915-upcoming-features-1742915363501.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915)) diff --git a/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md b/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md deleted file mode 100644 index 3321319c8d8..00000000000 --- a/packages/manager/.changeset/pr-11915-upcoming-features-1742915406867.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915)) diff --git a/packages/manager/.changeset/pr-11919-upcoming-features-1742907356639.md b/packages/manager/.changeset/pr-11919-upcoming-features-1742907356639.md deleted file mode 100644 index 2a0862b8a53..00000000000 --- a/packages/manager/.changeset/pr-11919-upcoming-features-1742907356639.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -update according to backend responses ([#11919](https://github.com/linode/manager/pull/11919)) diff --git a/packages/manager/.changeset/pr-11931-removed-1743075421803.md b/packages/manager/.changeset/pr-11931-removed-1743075421803.md deleted file mode 100644 index f54a314fcf2..00000000000 --- a/packages/manager/.changeset/pr-11931-removed-1743075421803.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) diff --git a/packages/manager/.changeset/pr-11933-fixed-1743104278261.md b/packages/manager/.changeset/pr-11933-fixed-1743104278261.md deleted file mode 100644 index 8dc8000342d..00000000000 --- a/packages/manager/.changeset/pr-11933-fixed-1743104278261.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Console errors on create menu & Linode create flow ([#11933](https://github.com/linode/manager/pull/11933)) diff --git a/packages/manager/.changeset/pr-11935-fixed-1743110166866.md b/packages/manager/.changeset/pr-11935-fixed-1743110166866.md deleted file mode 100644 index e149756a0d0..00000000000 --- a/packages/manager/.changeset/pr-11935-fixed-1743110166866.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) diff --git a/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md b/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md deleted file mode 100644 index 48f58ebeb31..00000000000 --- a/packages/manager/.changeset/pr-11938-upcoming-features-1743446079043.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Upcoming Features ---- - -Update `vpcIPFactory` to support IPv6 ([#11938](https://github.com/linode/manager/pull/11938)) diff --git a/packages/manager/.changeset/pr-11939-tests-1743444706872.md b/packages/manager/.changeset/pr-11939-tests-1743444706872.md deleted file mode 100644 index 5ad465a6cd1..00000000000 --- a/packages/manager/.changeset/pr-11939-tests-1743444706872.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Allow Cypress Volume tests to pass against alternative environments ([#11939](https://github.com/linode/manager/pull/11939)) diff --git a/packages/manager/.changeset/pr-11945-changed-1743511226551.md b/packages/manager/.changeset/pr-11945-changed-1743511226551.md deleted file mode 100644 index 0f3f58d3711..00000000000 --- a/packages/manager/.changeset/pr-11945-changed-1743511226551.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) diff --git a/packages/manager/.changeset/pr-11946-fixed-1743519262429.md b/packages/manager/.changeset/pr-11946-fixed-1743519262429.md deleted file mode 100644 index 427d4ae3556..00000000000 --- a/packages/manager/.changeset/pr-11946-fixed-1743519262429.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Profile Menu Icon Size Inconsistency ([#11946](https://github.com/linode/manager/pull/11946)) diff --git a/packages/manager/.changeset/pr-11947-fixed-1743523539798.md b/packages/manager/.changeset/pr-11947-fixed-1743523539798.md deleted file mode 100644 index 486da047cc3..00000000000 --- a/packages/manager/.changeset/pr-11947-fixed-1743523539798.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Unclearable ACL IP addresses for LKE clusters ([#11947](https://github.com/linode/manager/pull/11947)) diff --git a/packages/manager/.changeset/pr-11948-tests-1743523964300.md b/packages/manager/.changeset/pr-11948-tests-1743523964300.md deleted file mode 100644 index bf144dcd6c5..00000000000 --- a/packages/manager/.changeset/pr-11948-tests-1743523964300.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Fix test thats broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) diff --git a/packages/manager/.changeset/pr-11949-removed-1743525729687.md b/packages/manager/.changeset/pr-11949-removed-1743525729687.md deleted file mode 100644 index c164ff0c10a..00000000000 --- a/packages/manager/.changeset/pr-11949-removed-1743525729687.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Removed ---- - -Moved stackscripts-related queries and dependencies to shared `queries` package ([#11949](https://github.com/linode/manager/pull/11949)) diff --git a/packages/manager/.changeset/pr-11950-tech-stories-1743528301761.md b/packages/manager/.changeset/pr-11950-tech-stories-1743528301761.md deleted file mode 100644 index 17dd984b63f..00000000000 --- a/packages/manager/.changeset/pr-11950-tech-stories-1743528301761.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Remove hashing on Pendo account and visitor ids ([#11950](https://github.com/linode/manager/pull/11950)) diff --git a/packages/manager/.changeset/pr-11951-tests-1743535607155.md b/packages/manager/.changeset/pr-11951-tests-1743535607155.md deleted file mode 100644 index b5f375199bc..00000000000 --- a/packages/manager/.changeset/pr-11951-tests-1743535607155.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tests ---- - -Improve stability of Linode config Cypress tests ([#11951](https://github.com/linode/manager/pull/11951)) diff --git a/packages/manager/.changeset/pr-11952-tech-stories-1743537321600.md b/packages/manager/.changeset/pr-11952-tech-stories-1743537321600.md deleted file mode 100644 index 852c513755f..00000000000 --- a/packages/manager/.changeset/pr-11952-tech-stories-1743537321600.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -Add a stray console log and a silly test change for intended revert ([#11952](https://github.com/linode/manager/pull/11952)) diff --git a/packages/manager/.changeset/pr-11954-changed-1743543232723.md b/packages/manager/.changeset/pr-11954-changed-1743543232723.md deleted file mode 100644 index c1fac0720c0..00000000000 --- a/packages/manager/.changeset/pr-11954-changed-1743543232723.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Changed ---- - -Disable form fields on Firewall Create page for restricted users ([#11954](https://github.com/linode/manager/pull/11954)) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 1dfa40ee052..8e20125037c 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -18,11 +18,19 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Update Radio Button component to conform to Akamai Design System specs ([#11878](https://github.com/linode/manager/pull/11878)) - Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) - Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) +- Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) +- Disable form fields on Firewall Create page for restricted users ([#11954](https://github.com/linode/manager/pull/11954)) ### Fixed: - Database action menu incorrectly enabled with `read-only` grant and `Delete Cluster` button incorrectly disabled with `read/write` grant ([#11890](https://github.com/linode/manager/pull/11890)) - Tabs keyboard navigation on some Tanstack rerouted features ([#11894](https://github.com/linode/manager/pull/11894)) +- Pagination for subnets in VPC Subnet table ([#11906](https://github.com/linode/manager/pull/11906)) +- IP incrementation in Subnet Create drawer ([#11906](https://github.com/linode/manager/pull/11906)) +- Console errors on create menu & Linode create flow ([#11933](https://github.com/linode/manager/pull/11933)) +- PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) +- Profile Menu Icon Size Inconsistency ([#11946](https://github.com/linode/manager/pull/11946)) +- Unclearable ACL IP addresses for LKE clusters ([#11947](https://github.com/linode/manager/pull/11947)) ### Removed: @@ -39,6 +47,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Move ramda dependent utils ([#11913](https://github.com/linode/manager/pull/11913)) - Move `useIsGeckoEnabled` hook out of `RegionSelect` to `@linode/shared` package ([#11918](https://github.com/linode/manager/pull/11918)) - Remove region selector from Edit VPC drawer since data center assignment cannot be changed. ([#11929](https://github.com/linode/manager/pull/11929)) +- DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary ([#11909](https://github.com/linode/manager/pull/11909)) +- Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) +- Moved stackscripts-related queries and dependencies to shared `queries` package ([#11949](https://github.com/linode/manager/pull/11949)) ### Tech Stories: @@ -49,6 +60,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Resolve Path Traversal Vulnerabilities detected from semgrep ([#11914](https://github.com/linode/manager/pull/11914)) - Move feature flag code out of Kubernetes queries file ([#11922](https://github.com/linode/manager/pull/11922)) - Fix incorrect secret in `publish-packages` Github Action ([#11923](https://github.com/linode/manager/pull/11923)) +- Remove hashing on Pendo account and visitor ids ([#11950](https://github.com/linode/manager/pull/11950)) +- Add a stray console log and a silly test change for intended revert ([#11952](https://github.com/linode/manager/pull/11952)) ### Tests: @@ -60,6 +73,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Prevent legacy regions from being used by Cypress tests ([#11892](https://github.com/linode/manager/pull/11892)) - Temporarily skip Firewall end-to-end tests ([#11898](https://github.com/linode/manager/pull/11898)) - Add tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) +- Allow Cypress Volume tests to pass against alternative environments ([#11939](https://github.com/linode/manager/pull/11939)) +- Fix test thats broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) +- Improve stability of Linode config Cypress tests ([#11951](https://github.com/linode/manager/pull/11951)) ### Upcoming Features: @@ -80,6 +96,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Add Default Firewall chips to Firewall Detail page ([#11920](https://github.com/linode/manager/pull/11920)) - Remove preselected role from Change Role drawer ([#11926](https://github.com/linode/manager/pull/11926)) - Adjust logic for displaying encryption status on Linode Details page and encryption copy on LKE Create page ([#11930](https://github.com/linode/manager/pull/11930)) +- DBaaS Advanced Configurations: set up Autocomplete to display categorized options, add/remove configs, and implement a dynamic validation schema for all field types ([#11885](https://github.com/linode/manager/pull/11885)) +- Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915)) +- Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915)) +- update according to backend responses ([#11919](https://github.com/linode/manager/pull/11919)) +- Update `vpcIPFactory` to support IPv6 ([#11938](https://github.com/linode/manager/pull/11938)) ## [2025-03-26] - v1.138.1 diff --git a/packages/queries/.changeset/pr-11949-added-1743525787297.md b/packages/queries/.changeset/pr-11949-added-1743525787297.md deleted file mode 100644 index f520ea806de..00000000000 --- a/packages/queries/.changeset/pr-11949-added-1743525787297.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/queries": Added ---- - -Created `stackscripts/` directory and migrated relevant query keys and hooks ([#11949](https://github.com/linode/manager/pull/11949)) diff --git a/packages/queries/CHANGELOG.md b/packages/queries/CHANGELOG.md index 62935207291..445e4096a03 100644 --- a/packages/queries/CHANGELOG.md +++ b/packages/queries/CHANGELOG.md @@ -5,6 +5,7 @@ - `tags/` directory and migrated relevant query keys and hooks ([#11897](https://github.com/linode/manager/pull/11897)) - `support/` directory and migrated relevant query keys and hooks ([#11904](https://github.com/linode/manager/pull/11904)) +- `stackscripts/` directory and migrated relevant query keys and hooks ([#11949](https://github.com/linode/manager/pull/11949)) ### Upcoming Features: diff --git a/packages/ui/.changeset/pr-11946-added-1743519284653.md b/packages/ui/.changeset/pr-11946-added-1743519284653.md deleted file mode 100644 index ac2821cc4ab..00000000000 --- a/packages/ui/.changeset/pr-11946-added-1743519284653.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/ui": Added ---- - -Chevron Up Icon ([#11946](https://github.com/linode/manager/pull/11946)) diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 375b81500a9..30245b131a6 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added: - Move `ListItemOption` from `manager` to `ui` package ([#11790](https://github.com/linode/manager/pull/11790)) +- Chevron Up Icon ([#11946](https://github.com/linode/manager/pull/11946)) ### Changed: diff --git a/packages/utilities/.changeset/pr-11931-added-1743075469873.md b/packages/utilities/.changeset/pr-11931-added-1743075469873.md deleted file mode 100644 index e1ebc390158..00000000000 --- a/packages/utilities/.changeset/pr-11931-added-1743075469873.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/utilities": Added ---- - -Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) diff --git a/packages/utilities/CHANGELOG.md b/packages/utilities/CHANGELOG.md index 7ef98d1e1a9..46208b09409 100644 --- a/packages/utilities/CHANGELOG.md +++ b/packages/utilities/CHANGELOG.md @@ -8,6 +8,7 @@ - Move `doesRegionSupportFeature` from `manager` to `utilities` package ([#11891](https://github.com/linode/manager/pull/11891)) - Add `luxon` dependency and move related utils from `manager` to `utilities` package ([#11905](https://github.com/linode/manager/pull/11905)) - Migrate ramda dependent utils to @linode/utilities package ([#11913](https://github.com/linode/manager/pull/11913)) +- Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) ### Removed: From 56f05c45c3750ed5aeaf21f31c63b4e693a031d9 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 3 Apr 2025 15:20:29 -0400 Subject: [PATCH 75/84] Revert 11906 --- .../pr-11906-fixed-1742567649683.md | 5 - .../pr-11906-fixed-1742567667084.md | 5 - .../pr-11906-tech-stories-1742567613345.md | 5 - packages/manager/.eslintrc.cjs | 1 - .../e2e/core/vpc/vpc-details-page.spec.ts | 3 - .../e2e/core/vpc/vpc-landing-page.spec.ts | 5 - .../e2e/core/vpc/vpc-linodes-update.spec.ts | 5 - .../manager/cypress/support/intercepts/vpc.ts | 21 -- packages/manager/src/MainContent.tsx | 14 +- .../Linodes/PowerActionsDialogOrDrawer.tsx | 20 +- .../FormComponents/SubnetContent.tsx | 1 - .../FormComponents/VPCTopSectionContent.tsx | 1 - .../features/VPCs/VPCCreate/SubnetNode.tsx | 2 +- .../src/features/VPCs/VPCCreate/VPCCreate.tsx | 5 + .../SubnetAssignLinodesDrawer.test.tsx | 1 - .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 21 +- .../VPCs/VPCDetail/SubnetCreateDrawer.tsx | 6 +- .../VPCDetail/SubnetDeleteDialog.test.tsx | 1 - .../VPCs/VPCDetail/SubnetDeleteDialog.tsx | 6 +- .../VPCs/VPCDetail/SubnetEditDrawer.test.tsx | 1 - .../VPCs/VPCDetail/SubnetEditDrawer.tsx | 4 +- .../VPCs/VPCDetail/SubnetLinodeRow.test.tsx | 3 - .../VPCs/VPCDetail/SubnetLinodeRow.tsx | 23 +- .../SubnetUnassignLinodesDrawer.test.tsx | 4 +- .../VPCDetail/SubnetUnassignLinodesDrawer.tsx | 11 +- .../VPCs/VPCDetail/VPCDetail.test.tsx | 89 ++---- .../src/features/VPCs/VPCDetail/VPCDetail.tsx | 60 ++--- .../VPCs/VPCDetail/VPCSubnetsTable.test.tsx | 65 ++--- .../VPCs/VPCDetail/VPCSubnetsTable.tsx | 255 ++++++------------ .../VPCs/VPCLanding/VPCDeleteDialog.test.tsx | 24 +- .../VPCs/VPCLanding/VPCDeleteDialog.tsx | 26 +- .../VPCs/VPCLanding/VPCEditDrawer.test.tsx | 1 - .../VPCs/VPCLanding/VPCEditDrawer.tsx | 8 +- .../VPCs/VPCLanding/VPCEmptyState.tsx | 6 +- .../VPCs/VPCLanding/VPCLanding.test.tsx | 32 +-- .../features/VPCs/VPCLanding/VPCLanding.tsx | 89 +++--- .../src/features/VPCs/VPCLanding/VPCRow.tsx | 2 +- .../manager/src/features/VPCs/constants.ts | 8 - packages/manager/src/features/VPCs/index.tsx | 26 ++ packages/manager/src/hooks/useDialogData.ts | 71 +---- packages/manager/src/routes/index.tsx | 1 - packages/manager/src/routes/vpcs/index.ts | 197 ++------------ .../manager/src/routes/vpcs/vpcsLazyRoutes.ts | 17 -- packages/queries/src/vpcs/vpcs.ts | 10 - 44 files changed, 308 insertions(+), 853 deletions(-) delete mode 100644 packages/manager/.changeset/pr-11906-fixed-1742567649683.md delete mode 100644 packages/manager/.changeset/pr-11906-fixed-1742567667084.md delete mode 100644 packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md create mode 100644 packages/manager/src/features/VPCs/index.tsx delete mode 100644 packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts diff --git a/packages/manager/.changeset/pr-11906-fixed-1742567649683.md b/packages/manager/.changeset/pr-11906-fixed-1742567649683.md deleted file mode 100644 index 015d42aa964..00000000000 --- a/packages/manager/.changeset/pr-11906-fixed-1742567649683.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Pagination for subnets in VPC Subnet table ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.changeset/pr-11906-fixed-1742567667084.md b/packages/manager/.changeset/pr-11906-fixed-1742567667084.md deleted file mode 100644 index 9b1877fc086..00000000000 --- a/packages/manager/.changeset/pr-11906-fixed-1742567667084.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -IP incrementation in Subnet Create drawer ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md b/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md deleted file mode 100644 index 589b14a18c9..00000000000 --- a/packages/manager/.changeset/pr-11906-tech-stories-1742567613345.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Tech Stories ---- - -VPC rerouting (TanStack) ([#11906](https://github.com/linode/manager/pull/11906)) diff --git a/packages/manager/.eslintrc.cjs b/packages/manager/.eslintrc.cjs index 44587982dd4..fd5c9ac21e0 100644 --- a/packages/manager/.eslintrc.cjs +++ b/packages/manager/.eslintrc.cjs @@ -150,7 +150,6 @@ module.exports = { 'src/features/PlacementGroups/**/*', 'src/features/StackScripts/**/*', 'src/features/Volumes/**/*', - 'src/features/VPCs/**/*', ], rules: { 'no-restricted-imports': [ diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts index 55031530cd4..dcc5323773a 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-details-page.spec.ts @@ -11,7 +11,6 @@ import { mockDeleteSubnet, mockDeleteVPC, mockEditSubnet, - mockGetSubnet, mockGetSubnets, mockGetVPC, mockGetVPCs, @@ -178,7 +177,6 @@ describe('VPC details page', () => { cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockSubnet.label).should('be.visible'); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); // edit a subnet const mockEditedSubnet = subnetFactory.build({ @@ -203,7 +201,6 @@ describe('VPC details page', () => { .should('be.visible') .click(); ui.actionMenuItem.findByTitle('Edit').should('be.visible').click(); - mockGetSubnet(mockVPC.id, mockEditedSubnet.id, mockEditedSubnet); ui.drawer .findByTitle('Edit Subnet') diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts index c2e921f6f20..d52b2d19575 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-landing-page.spec.ts @@ -3,7 +3,6 @@ import { MOCK_DELETE_VPC_ERROR, mockDeleteVPC, mockDeleteVPCError, - mockGetVPC, mockGetVPCs, mockUpdateVPC, } from 'support/intercepts/vpc'; @@ -101,7 +100,6 @@ describe('VPC landing page', () => { }; mockGetVPCs([mockVPCs[1]]).as('getVPCs'); - mockGetVPC(mockVPCs[1]).as('getVPC'); mockUpdateVPC(mockVPCs[1].id, mockUpdatedVPC).as('updateVPC'); cy.visitWithLogin('/vpcs'); @@ -167,7 +165,6 @@ describe('VPC landing page', () => { // Delete VPCs Flow mockGetVPCs(mockVPCs).as('getVPCs'); - mockGetVPC(mockVPCs[0]).as('getVPC'); mockDeleteVPC(mockVPCs[0].id).as('deleteVPC'); cy.visitWithLogin('/vpcs'); @@ -257,7 +254,6 @@ describe('VPC landing page', () => { ]; mockGetVPCs(mockVPCs).as('getVPCs'); - mockGetVPC(mockVPCs[0]).as('getVPC'); mockDeleteVPCError(mockVPCs[0].id).as('deleteVPCError'); cy.visitWithLogin('/vpcs'); @@ -306,7 +302,6 @@ describe('VPC landing page', () => { .click(); }); - mockGetVPC(mockVPCs[1]).as('getVPC'); cy.findByText(mockVPCs[1].label) .should('be.visible') .closest('tr') diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index baee7a5de3f..840b1d7f09b 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -19,7 +19,6 @@ import { import { mockGetLinodes } from 'support/intercepts/linodes'; import { mockCreateSubnet, - mockGetSubnet, mockGetSubnets, mockGetVPC, mockGetVPCs, @@ -111,8 +110,6 @@ describe('VPC assign/unassign flows', () => { cy.wait(['@createSubnet', '@getVPC', '@getSubnets', '@getLinodes']); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); - // confirm that newly created subnet should now appear on VPC's detail page cy.findByText(mockVPC.label).should('be.visible'); cy.findByText('Subnets (1)').should('be.visible'); @@ -234,8 +231,6 @@ describe('VPC assign/unassign flows', () => { cy.findByText('Subnets (1)').should('be.visible'); cy.findByText(mockSubnet.label).should('be.visible'); - mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); - // unassign a linode to the subnet ui.actionMenu .findByTitle(`Action menu for Subnet ${mockSubnet.label}`) diff --git a/packages/manager/cypress/support/intercepts/vpc.ts b/packages/manager/cypress/support/intercepts/vpc.ts index d2c179b8520..362b263076c 100644 --- a/packages/manager/cypress/support/intercepts/vpc.ts +++ b/packages/manager/cypress/support/intercepts/vpc.ts @@ -132,27 +132,6 @@ export const mockGetSubnets = ( ); }; -/** - * Intercepts GET request to get a specific subnet and mocks response. - * - * @param vpcId - ID of VPC for which to mock response. - * @param subnetId - ID of subnet for which to mock response. - * @param subnet - Subnet for which to mock response - * - * @returns Cypress chainable. - */ -export const mockGetSubnet = ( - vpcId: number, - subnetId: number, - subnet: Subnet -): Cypress.Chainable => { - return cy.intercept( - 'GET', - apiMatcher(`vpcs/${vpcId}/subnets/${subnetId}`), - subnet - ); -}; - /** * Intercepts DELETE request to delete a subnet of a VPC and mocks response * diff --git a/packages/manager/src/MainContent.tsx b/packages/manager/src/MainContent.tsx index 4dd480491df..02c79b145d4 100644 --- a/packages/manager/src/MainContent.tsx +++ b/packages/manager/src/MainContent.tsx @@ -1,9 +1,3 @@ -import { - useAccountSettings, - useMutatePreferences, - usePreferences, - useProfile, -} from '@linode/queries'; import { Box } from '@linode/ui'; import { useMediaQuery } from '@mui/material'; import Grid from '@mui/material/Grid2'; @@ -30,6 +24,12 @@ import { useNotificationContext, } from 'src/features/NotificationCenter/NotificationCenterContext'; import { TopMenu } from 'src/features/TopMenu/TopMenu'; +import { + useMutatePreferences, + usePreferences, + useAccountSettings, + useProfile, +} from '@linode/queries'; import { useIsPageScrollable } from './components/PrimaryNav/utils'; import { ENABLE_MAINTENANCE_MODE } from './constants'; @@ -157,6 +157,7 @@ const AccountActivationLanding = React.lazy( () => import('src/components/AccountActivation/AccountActivationLanding') ); const Databases = React.lazy(() => import('src/features/Databases')); +const VPC = React.lazy(() => import('src/features/VPCs')); const CloudPulseMetrics = React.lazy(() => import('src/features/CloudPulse/Dashboard/CloudPulseDashboardLanding').then( @@ -381,6 +382,7 @@ export const MainContent = () => { {isDatabasesEnabled && ( )} + {isACLPEnabled && ( configs?.length === 1 ? configs[0].id : undefined; export const PowerActionsDialog = (props: Props) => { - const { action, isFetching, isOpen, linodeId, linodeLabel, onClose } = props; + const { action, isOpen, linodeId, linodeLabel, onClose } = props; const theme = useTheme(); const { @@ -140,7 +139,6 @@ export const PowerActionsDialog = (props: Props) => { /> } error={error?.[0].reason} - isFetching={isFetching} onClose={handleOnClose} open={isOpen} title={`${action} Linode ${linodeLabel ?? ''}?`} @@ -172,15 +170,15 @@ export const PowerActionsDialog = (props: Props) => { autoHighlight disablePortal={false} errorText={configsError?.[0].reason} - helperText="If no value is selected, the last booted config will be used." label="Config" loading={configsLoading} onChange={(_, option) => setSelectConfigID(option?.value ?? null)} options={configOptions} + helperText="If no value is selected, the last booted config will be used." /> )} {props.action === 'Power Off' && ( - + Note: Powered down Linodes will still accrue charges. See the  diff --git a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx index 5e580eacd0a..64a6b3a6816 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/SubnetContent.tsx @@ -2,7 +2,6 @@ import { Notice } from '@linode/ui'; import { getQueryParamsFromQueryString } from '@linode/utilities'; import * as React from 'react'; import { useFormContext } from 'react-hook-form'; -// eslint-disable-next-line no-restricted-imports import { useLocation } from 'react-router-dom'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx index eb7745a8e74..41701390745 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/FormComponents/VPCTopSectionContent.tsx @@ -3,7 +3,6 @@ import { TextField } from '@linode/ui'; import { getQueryParamsFromQueryString } from '@linode/utilities'; import * as React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -// eslint-disable-next-line no-restricted-imports import { useLocation } from 'react-router-dom'; import { Link } from 'src/components/Link'; diff --git a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx index f6a630ed865..3804a4806b3 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/SubnetNode.tsx @@ -43,8 +43,8 @@ export const SubnetNode = (props: Props) => { ( diff --git a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx index 8a3472cee82..908c8cee520 100644 --- a/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx +++ b/packages/manager/src/features/VPCs/VPCCreate/VPCCreate.tsx @@ -1,6 +1,7 @@ import { ActionsPanel, Notice, Paper } from '@linode/ui'; import Grid from '@mui/material/Grid2'; import { styled } from '@mui/material/styles'; +import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; import { FormProvider } from 'react-hook-form'; @@ -77,6 +78,10 @@ const VPCCreate = () => { ); }; +export const vpcCreateLazyRoute = createLazyRoute('/vpcs/create')({ + component: VPCCreate, +}); + const StyledActionsPanel = styled(ActionsPanel, { label: 'StyledActionsPanel', })(({ theme }) => ({ diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx index 457fb60e178..f7ae9f77def 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.test.tsx @@ -13,7 +13,6 @@ import type { Subnet } from '@linode/api-v4'; beforeAll(() => mockMatchMedia()); const props = { - isFetching: false, onClose: vi.fn(), open: true, subnet: { diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 442aa152aa1..9ec5d22230e 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -57,7 +57,6 @@ import type { ExtendedIP } from 'src/utilities/ipUtils'; // @TODO VPC: if all subnet action menu item related components use (most of) this as their props, might be worth // putting this in a common file and naming it something like SubnetActionMenuItemProps or something interface SubnetAssignLinodesDrawerProps { - isFetching: boolean; onClose: () => void; open: boolean; subnet?: Subnet; @@ -74,7 +73,7 @@ interface LinodeAndConfigData extends Linode { export const SubnetAssignLinodesDrawer = ( props: SubnetAssignLinodesDrawerProps ) => { - const { isFetching, onClose, open, subnet, vpcId, vpcRegion } = props; + const { onClose, open, subnet, vpcId, vpcRegion } = props; const { invalidateQueries, setUnassignLinodesErrors, @@ -414,7 +413,6 @@ export const SubnetAssignLinodesDrawer = ( subnet?.ipv4 ?? subnet?.ipv6 })`} NotFoundComponent={NotFound} - isFetching={isFetching} onClose={handleOnClose} open={open} > @@ -446,17 +444,12 @@ export const SubnetAssignLinodesDrawer = ( // We only want to be able to assign linodes that were not already assigned to this subnet options={linodeOptionsToAssign} placeholder="Select Linode or type to search" - sx={(theme) => ({ marginBottom: theme.spacingFunction(8) })} + sx={{ marginBottom: '8px' }} value={values.selectedLinode?.id || null} /> {values.selectedLinode?.id && ( <> - ({ marginLeft: theme.spacingFunction(2) })} - > + ({ marginBottom: theme.spacingFunction(8) })} + sx={{ marginBottom: '8px' }} value={values.chosenIP} /> )} @@ -518,11 +511,11 @@ export const SubnetAssignLinodesDrawer = ( linodeConfigs.length === 1) && ( 1 - ? theme.spacingFunction(16) - : theme.spacingFunction(8), + ? theme.spacing(2) + : theme.spacing(), }} handleIPRangeChange={handleIPRangeChange} ipRanges={values.ipRanges} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx index 3558fb91e2a..256b17e5d2c 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetCreateDrawer.tsx @@ -61,12 +61,12 @@ export const SubnetCreateDrawer = (props: Props) => { setError, watch, } = useForm({ - mode: 'onBlur', - resolver: yupResolver(createSubnetSchemaIPv4), - values: { + defaultValues: { ipv4: recommendedIPv4, label: '', }, + mode: 'onBlur', + resolver: yupResolver(createSubnetSchemaIPv4), }); const ipv4 = watch('ipv4'); diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx index 1aac638125b..d9d93339742 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.test.tsx @@ -8,7 +8,6 @@ import { SubnetDeleteDialog } from './SubnetDeleteDialog'; import type { ManagerPreferences } from '@linode/utilities'; const props = { - isFetching: false, onClose: vi.fn(), open: true, subnet: subnetFactory.build({ label: 'some subnet' }), diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx index 68550bb14a9..0f489db9278 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetDeleteDialog.tsx @@ -1,13 +1,12 @@ -import { useDeleteSubnetMutation } from '@linode/queries'; import { useSnackbar } from 'notistack'; import * as React from 'react'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; +import { useDeleteSubnetMutation } from '@linode/queries'; import type { Subnet } from '@linode/api-v4'; interface Props { - isFetching: boolean; onClose: () => void; open: boolean; subnet?: Subnet; @@ -15,7 +14,7 @@ interface Props { } export const SubnetDeleteDialog = (props: Props) => { - const { isFetching, onClose, open, subnet, vpcId } = props; + const { onClose, open, subnet, vpcId } = props; const { enqueueSnackbar } = useSnackbar(); const { error, @@ -48,7 +47,6 @@ export const SubnetDeleteDialog = (props: Props) => { }} errors={error} expand - isFetching={isFetching} label="Subnet Label" loading={isPending} onClick={onDeleteSubnet} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.test.tsx index af6f681e3dc..fec3bb98c6f 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.test.tsx @@ -6,7 +6,6 @@ import { SubnetEditDrawer } from './SubnetEditDrawer'; describe('SubnetEditDrawer', () => { const props = { - isFetching: false, onClose: vi.fn(), open: true, vpcId: 1, diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx index ce5de18d1c1..c7152b5c1dd 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetEditDrawer.tsx @@ -14,7 +14,6 @@ import { NotFound } from 'src/components/NotFound'; import type { ModifySubnetPayload, Subnet } from '@linode/api-v4'; interface Props { - isFetching: boolean; onClose: () => void; open: boolean; subnet?: Subnet; @@ -25,7 +24,7 @@ const IP_HELPER_TEXT = 'Once a subnet is created its IP range cannot be edited.'; export const SubnetEditDrawer = (props: Props) => { - const { isFetching, onClose, open, subnet, vpcId } = props; + const { onClose, open, subnet, vpcId } = props; const { isPending, @@ -78,7 +77,6 @@ export const SubnetEditDrawer = (props: Props) => { return ( { handleUnassignLinode={handleUnassignLinode} isVPCLKEEnterpriseCluster={false} linodeId={linodeFactory1.id} - subnet={subnetFactory.build()} subnetId={1} /> ) @@ -163,7 +162,6 @@ describe('SubnetLinodeRow', () => { handleUnassignLinode={handleUnassignLinode} isVPCLKEEnterpriseCluster={false} linodeId={linodeFactory1.id} - subnet={subnetFactory.build()} subnetId={0} /> ) @@ -266,7 +264,6 @@ describe('SubnetLinodeRow', () => { handleUnassignLinode={handleUnassignLinode} isVPCLKEEnterpriseCluster={true} linodeId={linodeFactory1.id} - subnet={subnetFactory.build()} subnetId={0} /> ) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx index bf2557ccde5..3ad1b720312 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetLinodeRow.tsx @@ -1,8 +1,3 @@ -import { - useAllLinodeConfigsQuery, - useLinodeFirewallsQuery, - useLinodeQuery, -} from '@linode/queries'; import { Box, CircleProgress, TooltipIcon, Typography } from '@linode/ui'; import { capitalizeAllWords } from '@linode/utilities'; import ErrorOutline from '@mui/icons-material/ErrorOutline'; @@ -15,6 +10,11 @@ import { StatusIcon } from 'src/components/StatusIcon/StatusIcon'; import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { getLinodeIconStatus } from 'src/features/Linodes/LinodesLanding/utils'; +import { + useAllLinodeConfigsQuery, + useLinodeFirewallsQuery, + useLinodeQuery, +} from '@linode/queries'; import { determineNoneSingleOrMultipleWithChip } from 'src/utilities/noneSingleOrMultipleWithChip'; import { @@ -33,16 +33,12 @@ import type { Subnet } from '@linode/api-v4/lib/vpcs/types'; import type { Action } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; interface Props { - handlePowerActionsLinode: ( - linode: Linode, - action: Action, - subnet?: Subnet - ) => void; + handlePowerActionsLinode: (linode: Linode, action: Action) => void; handleUnassignLinode: (linode: Linode, subnet?: Subnet) => void; hover?: boolean; isVPCLKEEnterpriseCluster: boolean; linodeId: number; - subnet: Subnet; + subnet?: Subnet; subnetId: number; } @@ -209,7 +205,7 @@ export const SubnetLinodeRow = (props: Props) => { {isRebootNeeded && ( { - handlePowerActionsLinode(linode, 'Reboot', subnet); + handlePowerActionsLinode(linode, 'Reboot'); }} actionText="Reboot" disabled={isVPCLKEEnterpriseCluster} @@ -220,8 +216,7 @@ export const SubnetLinodeRow = (props: Props) => { onClick={() => { handlePowerActionsLinode( linode, - isOffline ? 'Power On' : 'Power Off', - subnet + isOffline ? 'Power On' : 'Power Off' ); }} actionText={isOffline ? 'Power On' : 'Power Off'} diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.test.tsx index 598522ab195..cd4124a20c3 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.test.tsx @@ -1,3 +1,4 @@ +import { Subnet } from '@linode/api-v4'; import * as React from 'react'; import { SUBNET_UNASSIGN_LINODES_WARNING } from 'src/features/VPCs/constants'; @@ -5,10 +6,7 @@ import { renderWithTheme } from 'src/utilities/testHelpers'; import { SubnetUnassignLinodesDrawer } from './SubnetUnassignLinodesDrawer'; -import type { Subnet } from '@linode/api-v4'; - const props = { - isFetching: false, onClose: vi.fn(), open: true, subnet: { diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx index 9acc0af2f20..8a7eb0cedc6 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx @@ -35,7 +35,6 @@ import type { } from '@linode/api-v4'; interface Props { - isFetching: boolean; onClose: () => void; open: boolean; singleLinodeToBeUnassigned?: Linode; @@ -50,14 +49,7 @@ interface ConfigInterfaceAndLinodeData extends Linode { } export const SubnetUnassignLinodesDrawer = React.memo( - ({ - isFetching, - onClose, - open, - singleLinodeToBeUnassigned, - subnet, - vpcId, - }: Props) => { + ({ onClose, open, singleLinodeToBeUnassigned, subnet, vpcId }: Props) => { const { data: profile } = useProfile(); const { data: grants } = useGrants(); const subnetId = subnet?.id; @@ -290,7 +282,6 @@ export const SubnetUnassignLinodesDrawer = React.memo( subnet?.ipv4 ?? subnet?.ipv6 })`} NotFoundComponent={NotFound} - isFetching={isFetching} onClose={handleOnClose} open={open} > diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx index 80793ef1ea0..8fd3efdca16 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.test.tsx @@ -1,63 +1,32 @@ -import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; +import { fireEvent } from '@testing-library/react'; +import { waitForElementToBeRemoved } from '@testing-library/react'; import * as React from 'react'; import { vpcFactory } from 'src/factories/vpcs'; import { HttpResponse, http, server } from 'src/mocks/testServer'; -import { - mockMatchMedia, - renderWithThemeAndRouter, -} from 'src/utilities/testHelpers'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import VPCDetail from './VPCDetail'; -const queryMocks = vi.hoisted(() => ({ - useLocation: vi.fn().mockReturnValue({}), - useNavigate: vi.fn(() => vi.fn()), - useParams: vi.fn().mockReturnValue({}), - useSearch: vi.fn().mockReturnValue({}), -})); - -vi.mock('@tanstack/react-router', async () => { - const actual = await vi.importActual('@tanstack/react-router'); - return { - ...actual, - useLocation: queryMocks.useLocation, - useNavigate: queryMocks.useNavigate, - useParams: queryMocks.useParams, - useSearch: queryMocks.useSearch, - }; -}); - beforeAll(() => mockMatchMedia()); const loadingTestId = 'circle-progress'; describe('VPC Detail Summary section', () => { - beforeEach(() => { - queryMocks.useLocation.mockReturnValue({ - pathname: '/vpcs/1', - }); - queryMocks.useParams.mockReturnValue({ - vpcId: 1, - }); - }); - it('should display number of subnets and linodes, region, id, creation and update dates', async () => { - const vpcFactory1 = vpcFactory.build({ id: 1, subnets: [] }); + const vpcFactory1 = vpcFactory.build({ id: 100 }); server.use( http.get('*/vpcs/:vpcId', () => { return HttpResponse.json(vpcFactory1); }) ); - const { getAllByText, queryByTestId } = await renderWithThemeAndRouter( - - ); + const { getAllByText, getByTestId } = renderWithTheme(); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); getAllByText('Subnets'); getAllByText('Linodes'); @@ -86,14 +55,9 @@ describe('VPC Detail Summary section', () => { }) ); - const { getByText, queryByTestId } = await renderWithThemeAndRouter( - - ); + const { getByTestId, getByText } = renderWithTheme(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); getByText('Description'); getByText(vpcFactory1.description); @@ -106,14 +70,9 @@ describe('VPC Detail Summary section', () => { }) ); - const { queryByTestId, queryByText } = await renderWithThemeAndRouter( - - ); + const { getByTestId, queryByText } = renderWithTheme(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); expect(queryByText('Description')).not.toBeInTheDocument(); }); @@ -128,14 +87,9 @@ describe('VPC Detail Summary section', () => { }) ); - const { getAllByRole, queryByTestId } = await renderWithThemeAndRouter( - - ); + const { getAllByRole, getByTestId } = renderWithTheme(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); const readMoreButton = getAllByRole('button')[2]; expect(readMoreButton.innerHTML).toBe('Read More'); @@ -155,16 +109,11 @@ describe('VPC Detail Summary section', () => { }) ); - const { - getByRole, - getByText, - queryByTestId, - } = await renderWithThemeAndRouter(); + const { getByRole, getByTestId, getByText } = renderWithTheme( + + ); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); expect( getByText( diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx index 5417dc3fa88..99b247d4302 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCDetail.tsx @@ -9,8 +9,9 @@ import { } from '@linode/ui'; import { truncate } from '@linode/utilities'; import { useTheme } from '@mui/material/styles'; -import { useNavigate, useParams } from '@tanstack/react-router'; +import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; +import { useParams } from 'react-router-dom'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; import { EntityHeader } from 'src/components/EntityHeader/EntityHeader'; @@ -33,43 +34,15 @@ import { } from './VPCDetail.styles'; import { VPCSubnetsTable } from './VPCSubnetsTable'; -import type { VPC } from '@linode/api-v4'; - const VPCDetail = () => { - const params = useParams({ strict: false }); - const { vpcId } = params; - const navigate = useNavigate(); + const { vpcId } = useParams<{ vpcId: string }>(); const theme = useTheme(); - const { - data: vpc, - error, - isFetching: isFetchingVPC, - isLoading, - } = useVPCQuery(Number(vpcId) ?? -1, Boolean(vpcId)); + const { data: vpc, error, isLoading } = useVPCQuery(+vpcId); const { data: regions } = useRegionsQuery(); - const handleEditVPC = (vpc: VPC) => { - navigate({ - params: { action: 'edit', vpcId: vpc.id }, - to: '/vpcs/$vpcId/detail/$action', - }); - }; - - const handleDeleteVPC = (vpc: VPC) => { - navigate({ - params: { action: 'delete', vpcId: vpc.id }, - to: '/vpcs/$vpcId/detail/$action', - }); - }; - - const onCloseVPCDrawer = () => { - navigate({ - params: { vpcId: vpc?.id ?? -1 }, - to: '/vpcs/$vpcId', - }); - }; - + const [editVPCDrawerOpen, setEditVPCDrawerOpen] = React.useState(false); + const [deleteVPCDialogOpen, setDeleteVPCDialogOpen] = React.useState(false); const [showFullDescription, setShowFullDescription] = React.useState(false); if (isLoading) { @@ -161,13 +134,13 @@ const VPCDetail = () => { handleEditVPC(vpc)} + onClick={() => setEditVPCDrawerOpen(true)} > Edit handleDeleteVPC(vpc)} + onClick={() => setDeleteVPCDialogOpen(true)} > Delete @@ -212,15 +185,14 @@ const VPCDetail = () => { )} setDeleteVPCDialogOpen(false)} + open={deleteVPCDialogOpen} /> setEditVPCDrawerOpen(false)} + open={editVPCDrawerOpen} vpc={vpc} /> {isVPCLKEEnterpriseCluster && ( @@ -254,4 +226,8 @@ const VPCDetail = () => { ); }; +export const vpcDetailLazyRoute = createLazyRoute('/vpcs/$vpcId')({ + component: VPCDetail, +}); + export default VPCDetail; diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx index c6c100000bc..6153d3dd3a5 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.test.tsx @@ -1,5 +1,5 @@ +import { fireEvent } from '@testing-library/react'; import { waitForElementToBeRemoved } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { @@ -8,28 +8,13 @@ import { } from 'src/factories/subnets'; import { makeResourcePage } from 'src/mocks/serverHandlers'; import { HttpResponse, http, server } from 'src/mocks/testServer'; -import { - mockMatchMedia, - renderWithThemeAndRouter, -} from 'src/utilities/testHelpers'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import { VPCSubnetsTable } from './VPCSubnetsTable'; -const loadingTestId = 'circle-progress'; - beforeAll(() => mockMatchMedia()); -const queryMocks = vi.hoisted(() => ({ - useSearch: vi.fn().mockReturnValue({ query: undefined }), -})); - -vi.mock('@tanstack/react-router', async () => { - const actual = await vi.importActual('@tanstack/react-router'); - return { - ...actual, - useSearch: queryMocks.useSearch, - }; -}); +const loadingTestId = 'circle-progress'; describe('VPC Subnets table', () => { it('should display filter input, subnet label, id, ip range, number of linodes, and action menu', async () => { @@ -50,9 +35,9 @@ describe('VPC Subnets table', () => { getAllByRole, getAllByText, getByPlaceholderText, + getByTestId, getByText, - queryByTestId, - } = await renderWithThemeAndRouter( + } = renderWithTheme( { /> ); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); getByPlaceholderText('Filter Subnets by label or id'); getByText('Subnet Label'); @@ -78,7 +60,7 @@ describe('VPC Subnets table', () => { getByText(subnet.linodes.length); const actionMenuButton = getAllByRole('button')[4]; - await userEvent.click(actionMenuButton); + fireEvent.click(actionMenuButton); getByText('Assign Linodes'); getByText('Unassign Linodes'); @@ -94,11 +76,7 @@ describe('VPC Subnets table', () => { }) ); - const { - getAllByRole, - getByText, - queryByTestId, - } = await renderWithThemeAndRouter( + const { getAllByRole, getByTestId, getByText } = renderWithTheme( { /> ); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); const expandTableButton = getAllByRole('button')[3]; - await userEvent.click(expandTableButton); + fireEvent.click(expandTableButton); getByText('No Linodes'); }); @@ -125,11 +100,7 @@ describe('VPC Subnets table', () => { return HttpResponse.json(makeResourcePage([subnet])); }) ); - const { - getAllByRole, - getByText, - queryByTestId, - } = await renderWithThemeAndRouter( + const { getAllByRole, getByTestId, getByText } = renderWithTheme( { /> ); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); const expandTableButton = getAllByRole('button')[3]; - await userEvent.click(expandTableButton); + fireEvent.click(expandTableButton); getByText('Linode Label'); getByText('Status'); @@ -152,7 +120,7 @@ describe('VPC Subnets table', () => { }); it('should disable Create Subnet button if the VPC is associated with a LKE-E cluster', async () => { - const { getByRole, queryByTestId } = await renderWithThemeAndRouter( + const { getByRole, getByTestId } = renderWithTheme( { /> ); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); const createButton = getByRole('button', { name: 'Create Subnet', diff --git a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx index aa2dded83da..471d4aa8e2a 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/VPCSubnetsTable.tsx @@ -1,17 +1,6 @@ -import { - useLinodeQuery, - useSubnetQuery, - useSubnetsQuery, -} from '@linode/queries'; +import { useSubnetsQuery } from '@linode/queries'; import { Box, Button, CircleProgress, ErrorState } from '@linode/ui'; import { useTheme } from '@mui/material/styles'; -import { - useLocation, - useNavigate, - useParams, - useSearch, -} from '@tanstack/react-router'; -import { useSnackbar } from 'notistack'; import * as React from 'react'; import { CollapsibleTable } from 'src/components/CollapsibleTable/CollapsibleTable'; @@ -27,12 +16,9 @@ import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableSortCell } from 'src/components/TableSortCell'; import { PowerActionsDialog } from 'src/features/Linodes/PowerActionsDialogOrDrawer'; import { SubnetActionMenu } from 'src/features/VPCs/VPCDetail/SubnetActionMenu'; -import { useDialogData } from 'src/hooks/useDialogData'; -import { useOrderV2 } from 'src/hooks/useOrderV2'; -import { usePaginationV2 } from 'src/hooks/usePaginationV2'; +import { useOrder } from 'src/hooks/useOrder'; +import { usePagination } from 'src/hooks/usePagination'; -import { SUBNET_ACTION_PATH } from '../constants'; -import { VPC_DETAILS_ROUTE } from '../constants'; import { SubnetAssignLinodesDrawer } from './SubnetAssignLinodesDrawer'; import { SubnetCreateDrawer } from './SubnetCreateDrawer'; import { SubnetDeleteDialog } from './SubnetDeleteDialog'; @@ -56,40 +42,47 @@ const preferenceKey = 'vpc-subnets'; export const VPCSubnetsTable = (props: Props) => { const { isVPCLKEEnterpriseCluster, vpcId, vpcRegion } = props; const theme = useTheme(); - const { enqueueSnackbar } = useSnackbar(); - - const navigate = useNavigate(); - const params = useParams({ strict: false }); - const location = useLocation(); - + const [subnetsFilterText, setSubnetsFilterText] = React.useState(''); + const [selectedSubnet, setSelectedSubnet] = React.useState< + Subnet | undefined + >(); + const [selectedLinode, setSelectedLinode] = React.useState< + Linode | undefined + >(); + const [deleteSubnetDialogOpen, setDeleteSubnetDialogOpen] = React.useState( + false + ); + const [editSubnetsDrawerOpen, setEditSubnetsDrawerOpen] = React.useState( + false + ); + const [subnetCreateDrawerOpen, setSubnetCreateDrawerOpen] = React.useState( + false + ); + const [ + subnetAssignLinodesDrawerOpen, + setSubnetAssignLinodesDrawerOpen, + ] = React.useState(false); + const [powerActionDialogOpen, setPowerActionDialogOpen] = React.useState( + false + ); const [linodePowerAction, setLinodePowerAction] = React.useState< Action | undefined >(); - const { handleOrderChange, order, orderBy } = useOrderV2({ - initialRoute: { - defaultOrder: { - order: 'asc', - orderBy: 'label', - }, - from: VPC_DETAILS_ROUTE, - }, - preferenceKey: `${preferenceKey}-order`, - }); + const [ + subnetUnassignLinodesDrawerOpen, + setSubnetUnassignLinodesDrawerOpen, + ] = React.useState(false); - const search = useSearch({ - from: VPC_DETAILS_ROUTE, - }); - const { query } = search; + const pagination = usePagination(1, preferenceKey); - const pagination = usePaginationV2({ - currentRoute: VPC_DETAILS_ROUTE, - preferenceKey, - searchParams: (prev) => ({ - ...prev, - query: search.query, - }), - }); + const { handleOrderChange, order, orderBy } = useOrder( + { + order: 'asc', + orderBy: 'label', + }, + `${preferenceKey}-order` + ); const filter = { ['+order']: order, @@ -119,129 +112,56 @@ export const VPCSubnetsTable = (props: Props) => { page: pagination.page, page_size: pagination.pageSize, }, - generateSubnetsXFilter(query ?? '') + generateSubnetsXFilter(subnetsFilterText) ); - const { data: selectedSubnet, isFetching: isFetchingSubnet } = useDialogData({ - enabled: !!params.vpcId && !!params.subnetId, - paramKey: 'vpcId', - queryHook: useSubnetQuery, - redirectToOnNotFound: '/vpcs/$vpcId', - secondaryParamKey: 'subnetId', - }); - - const { data: selectedLinode, isFetching: isFetchingLinode } = useDialogData({ - enabled: !!params.linodeId, - paramKey: 'linodeId', - queryHook: useLinodeQuery, - redirectToOnNotFound: '/vpcs/$vpcId', - }); - const handleSearch = (searchText: string) => { - navigate({ - params: { vpcId }, - search: (prev) => ({ - ...prev, - page: undefined, - query: searchText || undefined, - }), - to: VPC_DETAILS_ROUTE, - }); - }; - - const onCloseSubnetDrawer = () => { - navigate({ - params: { vpcId }, - to: VPC_DETAILS_ROUTE, - }); - }; - - const handleSubnetCreate = () => { - navigate({ params: { vpcId }, to: '/vpcs/$vpcId/subnets/create' }); + setSubnetsFilterText(searchText); + // If you're on page 2+, need to go back to page 1 to see the actual results + pagination.handlePageChange(1); }; const handleSubnetDelete = (subnet: Subnet) => { - navigate({ - params: { subnetAction: 'delete', subnetId: subnet.id, vpcId }, - to: SUBNET_ACTION_PATH, - }); + setSelectedSubnet(subnet); + setDeleteSubnetDialogOpen(true); }; - const handleSubnetEdit = (subnet: Subnet) => { - navigate({ - params: { subnetAction: 'edit', subnetId: subnet.id, vpcId }, - to: SUBNET_ACTION_PATH, - }); + const handleEditSubnet = (subnet: Subnet) => { + setSelectedSubnet(subnet); + setEditSubnetsDrawerOpen(true); }; const handleSubnetUnassignLinodes = (subnet: Subnet) => { - navigate({ - params: { subnetAction: 'unassign', subnetId: subnet.id, vpcId }, - to: SUBNET_ACTION_PATH, - }); + setSelectedSubnet(subnet); + setSubnetUnassignLinodesDrawerOpen(true); }; const handleSubnetUnassignLinode = (linode: Linode, subnet: Subnet) => { - navigate({ - params: { - linodeAction: 'unassign', - linodeId: linode.id, - subnetId: subnet.id, - vpcId, - }, - to: '/vpcs/$vpcId/subnets/$subnetId/linodes/$linodeId/$linodeAction', - }); + setSelectedSubnet(subnet); + setSelectedLinode(linode); + setSubnetUnassignLinodesDrawerOpen(true); }; const handleSubnetAssignLinodes = (subnet: Subnet) => { - navigate({ - params: { subnetAction: 'assign', subnetId: subnet.id, vpcId }, - to: SUBNET_ACTION_PATH, - }); + setSelectedSubnet(subnet); + setSubnetAssignLinodesDrawerOpen(true); }; - const handlePowerActionsLinode = ( - linode: Linode, - action: Action, - subnet: Subnet - ) => { + const handlePowerActionsLinode = (linode: Linode, action: Action) => { + setSelectedLinode(linode); + setPowerActionDialogOpen(true); setLinodePowerAction(action); - navigate({ - params: { - linodeAction: 'power-action', - linodeId: linode.id, - subnetId: subnet.id ?? selectedSubnet?.id ?? -1, - vpcId, - }, - to: '/vpcs/$vpcId/subnets/$subnetId/linodes/$linodeId/$linodeAction', - }); }; - // If the user initiates a history -/+ to complete a linode action and the linode - // is no longer assigned to the subnet, push navigation to the vpc's detail page + // Ensure that the selected subnet passed to the drawer is up to date React.useEffect(() => { - if ( - params.linodeAction && - selectedLinode && - selectedSubnet && - !selectedSubnet?.linodes.some((linode) => linode.id === selectedLinode.id) - ) { - navigate({ - params: { vpcId }, - to: VPC_DETAILS_ROUTE, - }); - enqueueSnackbar(`Linode ${selectedLinode.label} not found in subnet`, { - variant: 'error', - }); + if (subnets && selectedSubnet) { + const updatedSubnet = subnets.data.find( + (subnet) => subnet.id === selectedSubnet.id + ); + setSelectedSubnet(updatedSubnet); } - }, [ - selectedSubnet, - vpcId, - navigate, - params.linodeAction, - selectedLinode, - enqueueSnackbar, - ]); + }, [subnets, selectedSubnet]); if (isLoading) { return ; @@ -303,7 +223,7 @@ export const VPCSubnetsTable = (props: Props) => { { { label="Filter Subnets by label or id" onSearch={handleSearch} placeholder="Filter Subnets by label or id" - value={query ?? ''} + value={subnetsFilterText} /> setSubnetCreateDrawerOpen(false)} + open={subnetCreateDrawerOpen} vpcId={vpcId} /> { TableRowHead={SubnetTableRowHead} /> { + setSubnetUnassignLinodesDrawerOpen(false); + setSelectedLinode(undefined); + }} + open={subnetUnassignLinodesDrawerOpen} singleLinodeToBeUnassigned={selectedLinode} subnet={selectedSubnet} vpcId={vpcId} /> setSubnetAssignLinodesDrawerOpen(false)} + open={subnetAssignLinodesDrawerOpen} subnet={selectedSubnet} vpcId={vpcId} vpcRegion={vpcRegion} /> setDeleteSubnetDialogOpen(false)} + open={deleteSubnetDialogOpen} subnet={selectedSubnet} vpcId={vpcId} /> setEditSubnetsDrawerOpen(false)} + open={editSubnetsDrawerOpen} subnet={selectedSubnet} vpcId={vpcId} /> setPowerActionDialogOpen(false)} /> ); diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx index 693c0a86242..7bcebb30890 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.test.tsx @@ -1,23 +1,20 @@ -import userEvent from '@testing-library/user-event'; +import { fireEvent } from '@testing-library/react'; import * as React from 'react'; -import { vpcFactory } from 'src/factories'; -import { renderWithThemeAndRouter } from 'src/utilities/testHelpers'; +import { renderWithTheme } from 'src/utilities/testHelpers'; import { VPCDeleteDialog } from './VPCDeleteDialog'; describe('VPC Delete Dialog', () => { const props = { - isFetching: false, + id: 1, + label: 'vpc-1', onClose: vi.fn(), open: true, - vpc: vpcFactory.build({ label: 'vpc-1' }), }; - it('renders a VPC delete dialog correctly', async () => { - const screen = await renderWithThemeAndRouter( - - ); + it('renders a VPC delete dialog correctly', () => { + const screen = renderWithTheme(); const vpcTitle = screen.getByText('Delete VPC vpc-1'); expect(vpcTitle).toBeVisible(); @@ -27,14 +24,11 @@ describe('VPC Delete Dialog', () => { const deleteButton = screen.getByText('Delete'); expect(deleteButton).toBeVisible(); }); - - it('closes the VPC delete dialog as expected', async () => { - const screen = await renderWithThemeAndRouter( - - ); + it('closes the VPC delete dialog as expected', () => { + const screen = renderWithTheme(); const cancelButton = screen.getByText('Cancel'); expect(cancelButton).toBeVisible(); - await userEvent.click(cancelButton); + fireEvent.click(cancelButton); expect(props.onClose).toBeCalled(); }); }); diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx index 94563357774..6fe1deb4128 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCDeleteDialog.tsx @@ -1,30 +1,27 @@ -import { useDeleteVPCMutation } from '@linode/queries'; -import { useLocation, useNavigate } from '@tanstack/react-router'; import { useSnackbar } from 'notistack'; import * as React from 'react'; +import { useHistory } from 'react-router-dom'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; - -import type { VPC } from '@linode/api-v4'; +import { useDeleteVPCMutation } from '@linode/queries'; interface Props { - isFetching: boolean; + id?: number; + label?: string; onClose: () => void; open: boolean; - vpc?: VPC; } export const VPCDeleteDialog = (props: Props) => { - const { isFetching, onClose, open, vpc } = props; + const { id, label, onClose, open } = props; const { enqueueSnackbar } = useSnackbar(); const { error, isPending, mutateAsync: deleteVPC, reset, - } = useDeleteVPCMutation(vpc?.id ?? -1); - const navigate = useNavigate(); - const location = useLocation(); + } = useDeleteVPCMutation(id ?? -1); + const history = useHistory(); React.useEffect(() => { if (open) { @@ -38,8 +35,8 @@ export const VPCDeleteDialog = (props: Props) => { variant: 'success', }); onClose(); - if (location.pathname !== '/vpcs') { - navigate({ to: '/vpcs' }); + if (history.location.pathname !== '/vpcs') { + history.push('/vpcs'); } }); }; @@ -48,19 +45,18 @@ export const VPCDeleteDialog = (props: Props) => { ); }; diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx index 3e51cb9104c..e72eba7e5dc 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.test.tsx @@ -7,7 +7,6 @@ import { VPCEditDrawer } from './VPCEditDrawer'; describe('Edit VPC Drawer', () => { const props = { - isFetching: false, onClose: vi.fn(), open: true, vpc: vpcFactory.build(), diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 891ad61dc28..497c31d7846 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -10,14 +10,13 @@ import { NotFound } from 'src/components/NotFound'; import type { UpdateVPCPayload, VPC } from '@linode/api-v4'; interface Props { - isFetching: boolean; onClose: () => void; open: boolean; vpc?: VPC; } export const VPCEditDrawer = (props: Props) => { - const { isFetching, onClose, open, vpc } = props; + const { onClose, open, vpc } = props; const { data: profile } = useProfile(); const { data: grants } = useGrants(); @@ -46,8 +45,8 @@ export const VPCEditDrawer = (props: Props) => { mode: 'onBlur', resolver: yupResolver(updateVPCSchema), values: { - description: vpc?.description ?? '', - label: vpc?.label ?? '', + description: vpc?.description, + label: vpc?.label, }, }); @@ -71,7 +70,6 @@ export const VPCEditDrawer = (props: Props) => { return ( { - const navigate = useNavigate(); + const { push } = useHistory(); const isVPCCreationRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_vpcs', @@ -29,7 +29,7 @@ export const VPCEmptyState = () => { category: linkAnalyticsEvent.category, label: 'Create VPC', }); - navigate({ to: '/vpcs/create' }); + push('/vpcs/create'); }, tooltipText: getRestrictedResourceText({ action: 'create', diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx index 351e1d2d620..6ff4c148261 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.test.tsx @@ -4,18 +4,15 @@ import * as React from 'react'; import { subnetFactory } from 'src/factories'; import { vpcFactory } from 'src/factories/vpcs'; import { makeResourcePage } from 'src/mocks/serverHandlers'; -import { HttpResponse, http, server } from 'src/mocks/testServer'; -import { - mockMatchMedia, - renderWithThemeAndRouter, -} from 'src/utilities/testHelpers'; +import { http, HttpResponse, server } from 'src/mocks/testServer'; +import { mockMatchMedia, renderWithTheme } from 'src/utilities/testHelpers'; import VPCLanding from './VPCLanding'; -const loadingTestId = 'circle-progress'; - beforeAll(() => mockMatchMedia()); +const loadingTestId = 'circle-progress'; + describe('VPC Landing Table', () => { it('should render vpc landing table with items', async () => { server.use( @@ -27,14 +24,12 @@ describe('VPC Landing Table', () => { }) ); - const { getAllByText, queryByTestId } = await renderWithThemeAndRouter( - - ); + const { getAllByText, getByTestId } = renderWithTheme(); + + // Loading state should render + expect(getByTestId(loadingTestId)).toBeInTheDocument(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); // Static text and table column headers getAllByText('Label'); @@ -51,14 +46,9 @@ describe('VPC Landing Table', () => { }) ); - const { getByText, queryByTestId } = await renderWithThemeAndRouter( - - ); + const { getByTestId, getByText } = renderWithTheme(); - const loadingState = queryByTestId(loadingTestId); - if (loadingState) { - await waitForElementToBeRemoved(loadingState); - } + await waitForElementToBeRemoved(getByTestId(loadingTestId)); expect( getByText('Create a private and isolated network') diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx index efcb7d6effd..effbe147675 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCLanding.tsx @@ -1,7 +1,7 @@ -import { useVPCQuery, useVPCsQuery } from '@linode/queries'; import { CircleProgress, ErrorState } from '@linode/ui'; -import { useNavigate, useParams } from '@tanstack/react-router'; +import { createLazyRoute } from '@tanstack/react-router'; import * as React from 'react'; +import { useHistory } from 'react-router-dom'; import { Hidden } from 'src/components/Hidden'; import { LandingHeader } from 'src/components/LandingHeader'; @@ -13,16 +13,11 @@ import { TableHead } from 'src/components/TableHead'; import { TableRow } from 'src/components/TableRow'; import { TableSortCell } from 'src/components/TableSortCell'; import { getRestrictedResourceText } from 'src/features/Account/utils'; -import { - VPC_CREATE_ROUTE, - VPC_LANDING_ROUTE, - VPC_LANDING_TABLE_PREFERENCE_KEY, -} from 'src/features/VPCs/constants'; import { VPC_DOCS_LINK, VPC_LABEL } from 'src/features/VPCs/constants'; -import { useDialogData } from 'src/hooks/useDialogData'; -import { useOrderV2 } from 'src/hooks/useOrderV2'; -import { usePaginationV2 } from 'src/hooks/usePaginationV2'; +import { useOrder } from 'src/hooks/useOrder'; +import { usePagination } from 'src/hooks/usePagination'; import { useRestrictedGlobalGrantCheck } from 'src/hooks/useRestrictedGlobalGrantCheck'; +import { useVPCsQuery } from '@linode/queries'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { VPCDeleteDialog } from './VPCDeleteDialog'; @@ -32,22 +27,18 @@ import { VPCRow } from './VPCRow'; import type { VPC } from '@linode/api-v4/lib/vpcs/types'; -const VPCLanding = () => { - const pagination = usePaginationV2({ - currentRoute: VPC_LANDING_ROUTE, - preferenceKey: VPC_LANDING_TABLE_PREFERENCE_KEY, - }); +const preferenceKey = 'vpcs'; +const VPC_CREATE_ROUTE = 'vpcs/create'; - const { handleOrderChange, order, orderBy } = useOrderV2({ - initialRoute: { - defaultOrder: { - order: 'desc', - orderBy: 'label', - }, - from: VPC_LANDING_ROUTE, +const VPCLanding = () => { + const pagination = usePagination(1, preferenceKey); + const { handleOrderChange, order, orderBy } = useOrder( + { + order: 'desc', + orderBy: 'label', }, - preferenceKey: `${VPC_LANDING_TABLE_PREFERENCE_KEY}-order`, - }); + `${preferenceKey}-order` + ); const filter = { ['+order']: order, @@ -62,42 +53,31 @@ const VPCLanding = () => { filter ); - const navigate = useNavigate(); - const params = useParams({ strict: false }); + const history = useHistory(); + + const [selectedVPC, setSelectedVPC] = React.useState(); + + const [editVPCDrawerOpen, setEditVPCDrawerOpen] = React.useState(false); + const [deleteVPCDialogOpen, setDeleteVPCDialogOpen] = React.useState(false); const handleEditVPC = (vpc: VPC) => { - navigate({ - params: { action: 'edit', vpcId: vpc.id }, - to: '/vpcs/$vpcId/$action', - }); + setSelectedVPC(vpc); + setEditVPCDrawerOpen(true); }; const handleDeleteVPC = (vpc: VPC) => { - navigate({ - params: { action: 'delete', vpcId: vpc.id }, - to: '/vpcs/$vpcId/$action', - }); - }; - - const onCloseVPCDrawer = () => { - navigate({ to: VPC_LANDING_ROUTE }); + setSelectedVPC(vpc); + setDeleteVPCDialogOpen(true); }; const createVPC = () => { - navigate({ to: VPC_CREATE_ROUTE }); + history.push(VPC_CREATE_ROUTE); }; const isVPCCreationRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'add_vpcs', }); - const { data: selectedVPC, isFetching: isFetchingVPC } = useDialogData({ - enabled: !!params.vpcId, - paramKey: 'vpcId', - queryHook: useVPCQuery, - redirectToOnNotFound: '/vpcs', - }); - if (error) { return ( { pageSize={pagination.pageSize} /> setDeleteVPCDialogOpen(false)} + open={deleteVPCDialogOpen} /> setEditVPCDrawerOpen(false)} + open={editVPCDrawerOpen} vpc={selectedVPC} /> ); }; +export const vpcLandingLazyRoute = createLazyRoute('/')({ + component: VPCLanding, +}); + export default VPCLanding; diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx index 2514c6414e0..b1de83a5e46 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCRow.tsx @@ -1,4 +1,3 @@ -import { useRegionsQuery } from '@linode/queries'; import * as React from 'react'; import { Hidden } from 'src/components/Hidden'; @@ -9,6 +8,7 @@ import { TableRow } from 'src/components/TableRow'; import { getRestrictedResourceText } from 'src/features/Account/utils'; import { LKE_ENTERPRISE_VPC_WARNING } from 'src/features/Kubernetes/constants'; import { useIsResourceRestricted } from 'src/hooks/useIsResourceRestricted'; +import { useRegionsQuery } from '@linode/queries'; import { getIsVPCLKEEnterpriseCluster } from '../utils'; diff --git a/packages/manager/src/features/VPCs/constants.ts b/packages/manager/src/features/VPCs/constants.ts index ce7250e5d36..c07e3b17ffb 100644 --- a/packages/manager/src/features/VPCs/constants.ts +++ b/packages/manager/src/features/VPCs/constants.ts @@ -70,11 +70,3 @@ export const VPC_MULTIPLE_CONFIGURATIONS_LEARN_MORE_LINK = export const ASSIGN_COMPUTE_INSTANCE_TO_VPC_LINK = 'https://techdocs.akamai.com/cloud-computing/docs/assign-a-compute-instance-to-a-vpc'; - -// constants used for tanstack routing: -export const VPC_LANDING_TABLE_PREFERENCE_KEY = 'vpcs'; -export const VPC_LANDING_ROUTE = '/vpcs'; -export const VPC_DETAILS_ROUTE = '/vpcs/$vpcId'; -export const VPC_CREATE_ROUTE = '/vpcs/create'; -export const SUBNET_ACTION_PATH = - '/vpcs/$vpcId/subnets/$subnetId/$subnetAction'; diff --git a/packages/manager/src/features/VPCs/index.tsx b/packages/manager/src/features/VPCs/index.tsx new file mode 100644 index 00000000000..f1a8ca644ad --- /dev/null +++ b/packages/manager/src/features/VPCs/index.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Route, Switch } from 'react-router-dom'; + +import { DocumentTitleSegment } from 'src/components/DocumentTitle'; +import { ProductInformationBanner } from 'src/components/ProductInformationBanner/ProductInformationBanner'; +import { SuspenseLoader } from 'src/components/SuspenseLoader'; + +const VPCCreate = React.lazy(() => import('./VPCCreate/VPCCreate')); +const VPCDetail = React.lazy(() => import('./VPCDetail/VPCDetail')); +const VPCLanding = React.lazy(() => import('./VPCLanding/VPCLanding')); + +const VPC = () => { + return ( + }> + + + + + + + + + ); +}; + +export default VPC; diff --git a/packages/manager/src/hooks/useDialogData.ts b/packages/manager/src/hooks/useDialogData.ts index 08020d0e91f..b74a0a9d1fe 100644 --- a/packages/manager/src/hooks/useDialogData.ts +++ b/packages/manager/src/hooks/useDialogData.ts @@ -12,26 +12,6 @@ import type { MigrationRouteTree } from 'src/routes'; type ExtractKeys = T extends object ? keyof T : never; type ParamsTypeKeys = ExtractKeys>; -type SingleIdQueryHook = ( - id: number | string, - enabled?: boolean -) => UseQueryResult; - -type DualIdQueryHook = ( - primaryId: number | string, - secondaryId: number | string, - enabled?: boolean -) => UseQueryResult; - -type QueryHook = DualIdQueryHook | SingleIdQueryHook; - -interface Props { - enabled?: boolean; - paramKey: ParamsTypeKeys; - queryHook: QueryHook; - redirectToOnNotFound: LinkProps['to']; - secondaryParamKey?: ParamsTypeKeys; -} interface Props { /** @@ -48,16 +28,14 @@ interface Props { /** * The query hook to fetch the entity. */ - queryHook: QueryHook; + queryHook: ( + id: number | string | undefined, + enabled?: boolean + ) => UseQueryResult; /** * The route to redirect to if the entity is not found. */ redirectToOnNotFound: LinkProps['to']; - /** - * The key of the secondary parameter in the URL that will be used to fetch the entity. - * ex: 'subnetId' for `/vpcs/$vpcId/subnets/$subnetId` - */ - secondaryParamKey?: ParamsTypeKeys; } /** @@ -67,7 +45,7 @@ interface Props { * It will return the data for the entity that the dialog is going to target, including its loading state. * It is usually used on a feature landing page, where the dialog is triggered by a route change. * - * It should be instantiated as follow (single id): + * It should be instantiated as follow: * * const { * data: {entity}, @@ -78,54 +56,17 @@ interface Props { * queryHook: useEntityQuery, // ex: useVolumeQuery * redirectToOnNotFound: '/entities', // ex: '/volumes' * }); - * - * It should be instantiated as follow (dual id): - * - * const { - * data: {entity}, - * isFetching: isFetchingEntity, - * } = useDialogRouteGuard({ - * enabled: !!params.entityId && !!params.secondaryEntityId, - * paramKey: 'entityId', // ex: 'vpcId' - * queryHook: useEntityQuery, // ex: useSubnetQuery - * redirectToOnNotFound: '/entities', // ex: '/vpcs/$vpcId/subnets' - * secondaryParamKey: 'secondaryEntityId', // ex: 'subnetId' - * }); */ export const useDialogData = ({ enabled = true, paramKey, queryHook, redirectToOnNotFound, - secondaryParamKey, }: Props) => { const params = useParams({ strict: false }); const { enqueueSnackbar } = useSnackbar(); const navigate = useNavigate(); - - const primaryId = params[paramKey as keyof typeof params]; - const secondaryId = secondaryParamKey - ? params[secondaryParamKey as keyof typeof params] - : undefined; - - // Ensure IDs are actually valid values, not just truthy - const isValidPrimaryId = - typeof primaryId === 'string' || typeof primaryId === 'number'; - const isValidSecondaryId = - typeof secondaryId === 'string' || typeof secondaryId === 'number'; - const shouldRunQuery = - enabled && isValidPrimaryId && (!secondaryParamKey || isValidSecondaryId); - - const query = secondaryParamKey - ? (queryHook as DualIdQueryHook)( - primaryId as number | string, - secondaryId as number | string, - shouldRunQuery - ) - : (queryHook as SingleIdQueryHook)( - primaryId as number | string, - shouldRunQuery - ); + const query = queryHook(params[paramKey as keyof typeof params], enabled); React.useEffect(() => { if (enabled && !query.isLoading && !query.data) { diff --git a/packages/manager/src/routes/index.tsx b/packages/manager/src/routes/index.tsx index 7e77a0ee4fd..5fd7ada5b49 100644 --- a/packages/manager/src/routes/index.tsx +++ b/packages/manager/src/routes/index.tsx @@ -96,7 +96,6 @@ export const migrationRouteTree = migrationRootRoute.addChildren([ placementGroupsRouteTree, stackScriptsRouteTree, volumesRouteTree, - vpcsRouteTree, ]); export type MigrationRouteTree = typeof migrationRouteTree; export const migrationRouter = createRouter({ diff --git a/packages/manager/src/routes/vpcs/index.ts b/packages/manager/src/routes/vpcs/index.ts index 33ad75531ab..00c9bfc5044 100644 --- a/packages/manager/src/routes/vpcs/index.ts +++ b/packages/manager/src/routes/vpcs/index.ts @@ -1,35 +1,8 @@ -import { createRoute, redirect } from '@tanstack/react-router'; +import { createRoute } from '@tanstack/react-router'; import { rootRoute } from '../root'; import { VPCRoute } from './VPCRoute'; -import type { TableSearchParams } from '../types'; - -export interface SubnetSearchParams extends TableSearchParams { - query?: string; -} -const vpcAction = { - delete: 'delete', - edit: 'edit', -} as const; - -const subnetAction = { - assign: 'assign', - create: 'create', - delete: 'delete', - edit: 'edit', - unassign: 'unassign', -} as const; - -const subnetLinodeAction = { - 'power-action': 'power-action', - unassign: 'unassign', -} as const; - -export type VPCAction = typeof vpcAction[keyof typeof vpcAction]; -export type SubnetAction = typeof subnetAction[keyof typeof subnetAction]; -export type SubnetLinodeAction = typeof subnetLinodeAction[keyof typeof subnetLinodeAction]; - const vpcsRoute = createRoute({ component: VPCRoute, getParentRoute: () => rootRoute, @@ -39,166 +12,32 @@ const vpcsRoute = createRoute({ const vpcsLandingRoute = createRoute({ getParentRoute: () => vpcsRoute, path: '/', -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcLandingLazyRoute)); - -type VPCActionRouteParams

    = { - action: VPCAction; - vpcId: P; -}; - -const vpcActionRouteParams = { - params: { - parse: ({ action, vpcId }: VPCActionRouteParams) => ({ - action, - vpcId: Number(vpcId), - }), - stringify: ({ action, vpcId }: VPCActionRouteParams) => ({ - action, - vpcId: String(vpcId), - }), - }, -}; - -const vpcActionRoute = createRoute({ - ...vpcActionRouteParams, - beforeLoad: async ({ params }) => { - if (!(params.action in vpcAction)) { - throw redirect({ - to: '/vpcs', - }); - } - }, - getParentRoute: () => vpcsLandingRoute, - path: '$vpcId/$action', -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcLandingLazyRoute)); +}).lazy(() => + import('src/features/VPCs/VPCLanding/VPCLanding').then( + (m) => m.vpcLandingLazyRoute + ) +); const vpcsCreateRoute = createRoute({ getParentRoute: () => vpcsRoute, path: 'create', -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcCreateLazyRoute)); +}).lazy(() => + import('src/features/VPCs/VPCCreate/VPCCreate').then( + (m) => m.vpcCreateLazyRoute + ) +); const vpcsDetailRoute = createRoute({ getParentRoute: () => vpcsRoute, - parseParams: (params) => ({ - vpcId: Number(params.vpcId), - }), path: '$vpcId', - validateSearch: (search: SubnetSearchParams) => search, -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); - -/** - * We must have different routes for the Edit and Delete modals on the VPC Landing page and the VPC Detail page, or we will get - * redirected to the Landing page whenever we try to view a modal on the VPC detail page. - * - * vpcs/$vpcId/detail/edit (detail page) <==> vpcs/$vpcId/edit (landing page) - * vpcs/$vpcId/detail/delete (detail page) <==> vpcs/$vpcId/delete (landing page) - */ -const vpcDetailActionRoute = createRoute({ - ...vpcActionRouteParams, - beforeLoad: async ({ params }) => { - if (!(params.action in vpcAction)) { - throw redirect({ - params: { - vpcId: params.vpcId, - }, - search: () => ({}), - to: `/vpcs/$vpcId`, - }); - } - }, - getParentRoute: () => vpcsDetailRoute, - path: 'detail/$action', -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); - -const subnetCreateRoute = createRoute({ - getParentRoute: () => vpcsDetailRoute, - path: 'subnets/create', - validateSearch: (search: SubnetSearchParams) => search, -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); - -const subnetDetailRoute = createRoute({ - getParentRoute: () => vpcsDetailRoute, - parseParams: (params) => ({ - subnetId: Number(params.subnetId), - }), - path: 'subnets/$subnetId', - validateSearch: (search: SubnetSearchParams) => search, -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); - -type SubnetActionRouteParams

    = { - subnetAction: SubnetAction; - subnetId: P; -}; - -const subnetActionRoute = createRoute({ - beforeLoad: async ({ params }) => { - if (!(params.subnetAction in subnetAction)) { - throw redirect({ - params: { - vpcId: params.vpcId, - }, - search: () => ({}), - to: `/vpcs/$vpcId`, - }); - } - }, - getParentRoute: () => subnetDetailRoute, - params: { - parse: ({ subnetAction }: SubnetActionRouteParams) => ({ - subnetAction, - }), - stringify: ({ subnetAction }: SubnetActionRouteParams) => ({ - subnetAction, - }), - }, - path: '$subnetAction', - validateSearch: (search: SubnetSearchParams) => search, -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); - -type SubnetLinodeActionRouteParams

    = { - linodeAction: SubnetLinodeAction; - linodeId: P; -}; - -const subnetLinodeActionRoute = createRoute({ - beforeLoad: async ({ params }) => { - if (!(params.linodeAction in subnetLinodeAction)) { - throw redirect({ - params: { - vpcId: params.vpcId, - }, - search: () => ({}), - to: `/vpcs/$vpcId`, - }); - } - }, - getParentRoute: () => subnetDetailRoute, - params: { - parse: ({ - linodeAction, - linodeId, - }: SubnetLinodeActionRouteParams) => ({ - linodeAction, - linodeId: Number(linodeId), - }), - stringify: ({ - linodeAction, - linodeId, - }: SubnetLinodeActionRouteParams) => ({ - linodeAction, - linodeId: String(linodeId), - }), - }, - path: '/linodes/$linodeId/$linodeAction', - validateSearch: (search: SubnetSearchParams) => search, -}).lazy(() => import('./vpcsLazyRoutes').then((m) => m.vpcDetailLazyRoute)); +}).lazy(() => + import('src/features/VPCs/VPCDetail/VPCDetail').then( + (m) => m.vpcDetailLazyRoute + ) +); export const vpcsRouteTree = vpcsRoute.addChildren([ - vpcsLandingRoute.addChildren([vpcActionRoute]), + vpcsLandingRoute, vpcsCreateRoute, - vpcsDetailRoute.addChildren([ - vpcDetailActionRoute, - subnetCreateRoute, - subnetDetailRoute.addChildren([subnetActionRoute, subnetLinodeActionRoute]), - ]), + vpcsDetailRoute, ]); diff --git a/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts b/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts deleted file mode 100644 index 59b7f762898..00000000000 --- a/packages/manager/src/routes/vpcs/vpcsLazyRoutes.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createLazyRoute } from '@tanstack/react-router'; - -import VPCCreate from 'src/features/VPCs/VPCCreate/VPCCreate'; -import VPCDetail from 'src/features/VPCs/VPCDetail/VPCDetail'; -import VPCLanding from 'src/features/VPCs/VPCLanding/VPCLanding'; - -export const vpcCreateLazyRoute = createLazyRoute('/vpcs/create')({ - component: VPCCreate, -}); - -export const vpcDetailLazyRoute = createLazyRoute('/vpcs/$vpcId')({ - component: VPCDetail, -}); - -export const vpcLandingLazyRoute = createLazyRoute('/')({ - component: VPCLanding, -}); diff --git a/packages/queries/src/vpcs/vpcs.ts b/packages/queries/src/vpcs/vpcs.ts index 233c21596ad..4eac7b31a3f 100644 --- a/packages/queries/src/vpcs/vpcs.ts +++ b/packages/queries/src/vpcs/vpcs.ts @@ -155,16 +155,6 @@ export const useSubnetsQuery = ( placeholderData: keepPreviousData, }); -export const useSubnetQuery = ( - vpcId: number, - subnetId: number, - enabled: boolean = true -) => - useQuery({ - ...vpcQueries.vpc(vpcId)._ctx.subnets._ctx.subnet(subnetId), - enabled, - }); - export const useCreateSubnetMutation = (vpcId: number) => { const queryClient = useQueryClient(); return useMutation({ From 9a4b96494e60d8ef78b9f166ef439b5009cf1d63 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 3 Apr 2025 15:24:38 -0400 Subject: [PATCH 76/84] Revert 11952 --- packages/manager/CHANGELOG.md | 1 - .../manager/cypress/e2e/core/kubernetes/lke-create.spec.ts | 3 +-- .../src/features/Longview/LongviewLanding/LongviewList.tsx | 4 ---- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 8e20125037c..6a5cc462129 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -61,7 +61,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Move feature flag code out of Kubernetes queries file ([#11922](https://github.com/linode/manager/pull/11922)) - Fix incorrect secret in `publish-packages` Github Action ([#11923](https://github.com/linode/manager/pull/11923)) - Remove hashing on Pendo account and visitor ids ([#11950](https://github.com/linode/manager/pull/11950)) -- Add a stray console log and a silly test change for intended revert ([#11952](https://github.com/linode/manager/pull/11952)) ### Tests: diff --git a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts index 24f063b9406..ebe1e5a933a 100644 --- a/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts +++ b/packages/manager/cypress/e2e/core/kubernetes/lke-create.spec.ts @@ -1290,8 +1290,7 @@ describe('LKE Cluster Creation with LKE-E', () => { cy.url().should('endWith', '/kubernetes/create'); - // TODO: revert me before we release on 4/8! - cy.contains('Tier').should('not.exist'); + cy.contains('Cluster Tier').should('not.exist'); }); describe('shows the LKE-E flow with the feature flag on', () => { diff --git a/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx b/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx index 053fd9a8a22..b478889a05b 100644 --- a/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx +++ b/packages/manager/src/features/Longview/LongviewLanding/LongviewList.tsx @@ -16,10 +16,6 @@ import { LongviewListRows } from './LongviewListRows'; import type { LongviewClient } from '@linode/api-v4/lib/longview/types'; import type { Props as LVProps } from 'src/containers/longview.container'; -// TODO: revert me before we release on 4/8! -// eslint-disable-next-line no-console -console.log('Delete me!'); - type LongviewProps = Omit< LVProps, | 'createLongviewClient' From 377d0c2eed718edf4029f4ed9ac0acca31d9807c Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 3 Apr 2025 15:56:37 -0400 Subject: [PATCH 77/84] Edits to changelog --- packages/manager/CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index d8149da6cab..8c990aa31c3 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -19,7 +19,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Change `GlobalFilters.tsx` and `Zoomer.tsx` to add color on hover of icon ([#11883](https://github.com/linode/manager/pull/11883)) - Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) - Update BetaChip styles, its usage and updated BetaChip component tests ([#11965](https://github.com/linode/manager/pull/11965)) -- Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) - Disable form fields on Firewall Create page for restricted users ([#11954](https://github.com/linode/manager/pull/11954)) ### Fixed: @@ -50,7 +49,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Remove region selector from Edit VPC drawer since data center assignment cannot be changed. ([#11929](https://github.com/linode/manager/pull/11929)) - DBaaS: deprecated types, outdated and unused code in DatabaseCreate and DatabaseSummary ([#11909](https://github.com/linode/manager/pull/11909)) - Move `useFormattedDate` from `manager` to `utilities` package ([#11931](https://github.com/linode/manager/pull/11931)) -- Moved stackscripts-related queries and dependencies to shared `queries` package ([#11949](https://github.com/linode/manager/pull/11949)) +- Move stackscripts-related queries and dependencies to shared `queries` package ([#11949](https://github.com/linode/manager/pull/11949)) ### Tech Stories: @@ -74,7 +73,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Temporarily skip Firewall end-to-end tests ([#11898](https://github.com/linode/manager/pull/11898)) - Add tests for restricted user on database page ([#11912](https://github.com/linode/manager/pull/11912)) - Allow Cypress Volume tests to pass against alternative environments ([#11939](https://github.com/linode/manager/pull/11939)) -- Fix test thats broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) +- Fix create-linode-view-code-snippet.spec.ts test broken in devcloud ([#11948](https://github.com/linode/manager/pull/11948)) - Improve stability of Linode config Cypress tests ([#11951](https://github.com/linode/manager/pull/11951)) ### Upcoming Features: @@ -99,8 +98,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - DBaaS Advanced Configurations: set up Autocomplete to display categorized options, add/remove configs, and implement a dynamic validation schema for all field types ([#11885](https://github.com/linode/manager/pull/11885)) - Support more VPC features when using Linode Interfaces on the Linode Create page ([#11915](https://github.com/linode/manager/pull/11915)) - Pre-select default firewalls on the Linode Create flow ([#11915](https://github.com/linode/manager/pull/11915)) -- update according to backend responses ([#11919](https://github.com/linode/manager/pull/11919)) +- Update mock data and tests according to IAM backend response updates ([#11919](https://github.com/linode/manager/pull/11919)) - Update `vpcIPFactory` to support IPv6 ([#11938](https://github.com/linode/manager/pull/11938)) +- Add a 2-minute refetch interval in alerts.ts, add isLoading and remove isFetching in AlertDetail.tsx. ([#11945](https://github.com/linode/manager/pull/11945)) ## [2025-03-26] - v1.138.1 From da7c016958043af387aa2cd3ce45c75a4c31fa89 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Fri, 4 Apr 2025 11:43:21 -0400 Subject: [PATCH 78/84] Remove 11906 VPC from changelog --- packages/manager/CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 8c990aa31c3..662641ae589 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -25,8 +25,6 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Database action menu incorrectly enabled with `read-only` grant and `Delete Cluster` button incorrectly disabled with `read/write` grant ([#11890](https://github.com/linode/manager/pull/11890)) - Tabs keyboard navigation on some Tanstack rerouted features ([#11894](https://github.com/linode/manager/pull/11894)) -- Pagination for subnets in VPC Subnet table ([#11906](https://github.com/linode/manager/pull/11906)) -- IP incrementation in Subnet Create drawer ([#11906](https://github.com/linode/manager/pull/11906)) - Console errors on create menu & Linode create flow ([#11933](https://github.com/linode/manager/pull/11933)) - PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) - Profile Menu Icon Size Inconsistency ([#11946](https://github.com/linode/manager/pull/11946)) From 5ba3d84be18c1faba87455888bc273cad76cad11 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Fri, 4 Apr 2025 11:52:24 -0400 Subject: [PATCH 79/84] Update changelog for 11970 --- packages/manager/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 662641ae589..5ce97db0236 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Update styles to CDS for profile menu ([#11884](https://github.com/linode/manager/pull/11884)) - Update BetaChip styles, its usage and updated BetaChip component tests ([#11965](https://github.com/linode/manager/pull/11965)) - Disable form fields on Firewall Create page for restricted users ([#11954](https://github.com/linode/manager/pull/11954)) +- Update 'Learn more' docs link for Accelerated Compute plans ([#11970](https://github.com/linode/manager/pull/11970)) ### Fixed: From 1645ae5b0067b817448946d6da37b14900f097e0 Mon Sep 17 00:00:00 2001 From: Connie Liu <139280159+coliu-akamai@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:52:51 -0400 Subject: [PATCH 80/84] docs link (#11970) --- .../manager/src/features/components/PlansPanel/constants.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/manager/src/features/components/PlansPanel/constants.ts b/packages/manager/src/features/components/PlansPanel/constants.ts index ebd4b1ee140..6b52c75c2dc 100644 --- a/packages/manager/src/features/components/PlansPanel/constants.ts +++ b/packages/manager/src/features/components/PlansPanel/constants.ts @@ -29,9 +29,8 @@ export const GPU_COMPUTE_INSTANCES_LINK = 'https://techdocs.akamai.com/cloud-computing/docs/gpu-compute-instances'; export const TRANSFER_COSTS_LINK = 'https://techdocs.akamai.com/cloud-computing/docs/network-transfer-usage-and-costs'; -// TODO: accelerated plans - update to GA link (when GA launches) export const ACCELERATED_COMPUTE_INSTANCES_LINK = - 'https://techdocs.akamai.com/cloud-computing/docs/accelerated-compute-instances-beta'; + 'https://techdocs.akamai.com/cloud-computing/docs/accelerated-compute-instances'; export const DEDICATED_512_GB_PLAN: ExtendedType = { accelerated_devices: 0, From b92d61b97d9e3a641477e195b0123f64ddf2920a Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Mon, 7 Apr 2025 11:13:32 +0200 Subject: [PATCH 81/84] fix: [UIE-8447] - refresh drawer after config add/update --- packages/api-v4/src/databases/types.ts | 2 +- packages/manager/src/factories/databases.ts | 22 +++++++++---------- .../DatabaseAdvancedConfigurationDrawer.tsx | 18 ++++++++------- .../DatabaseConfigurationItem.tsx | 7 ++++-- .../src/features/Databases/utilities.test.ts | 10 ++++----- .../src/features/Databases/utilities.ts | 2 +- 6 files changed, 33 insertions(+), 28 deletions(-) diff --git a/packages/api-v4/src/databases/types.ts b/packages/api-v4/src/databases/types.ts index 07048a3fb77..444b391a18b 100644 --- a/packages/api-v4/src/databases/types.ts +++ b/packages/api-v4/src/databases/types.ts @@ -60,7 +60,7 @@ export interface ConfigurationItem { pattern?: string; type?: string | [string, null] | string[]; enum?: string[]; - restart_cluster?: boolean; + requires_restart?: boolean; } export type ConfigValue = number | string | boolean; diff --git a/packages/manager/src/factories/databases.ts b/packages/manager/src/factories/databases.ts index 2dabc3119b8..0677cd00e05 100644 --- a/packages/manager/src/factories/databases.ts +++ b/packages/manager/src/factories/databases.ts @@ -281,7 +281,7 @@ export const databaseEngineConfigFactory = Factory.Sync.makeFactory { const configurations = convertEngineConfigToOptions(databaseConfig); - const existingConfigsArray = convertExistingConfigsToArray( - existingConfigurations, - databaseConfig + const existingConfigsArray = useMemo( + () => convertExistingConfigsToArray(existingConfigurations, databaseConfig), + [existingConfigurations, databaseConfig] ); const { @@ -81,6 +81,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { watch, } = useForm({ defaultValues: { configs: existingConfigsArray }, + mode: 'onBlur', resolver: yupResolver( createDynamicAdvancedConfigSchema( configurations @@ -96,10 +97,10 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { const configs = watch('configs'); useEffect(() => { - if (databaseConfig) { + if (databaseConfig && open) { reset({ configs: existingConfigsArray }); } - }, [databaseConfig]); + }, [databaseConfig, open, reset, existingConfigsArray]); const usedConfigs = useMemo( () => new Set(fields.map((config) => config.label)), @@ -109,7 +110,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { (config) => !usedConfigs.has(config.label) ); - const hasRestartCluster = fields.some((item) => item.restart_cluster); + const hasRestartCluster = fields.some((item) => item.requires_restart); const handleAddConfiguration = (config: ConfigurationOption | null) => { if (!config || usedConfigs.has(config.label)) { @@ -140,7 +141,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { engine_config: formatConfigPayload(formData.configs, configurations), }; await updateDatabase(payload).then(() => { - onClose(); + handleClose(); enqueueSnackbar('Advanced Configuration settings saved', { variant: 'success', }); @@ -150,7 +151,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { return ( @@ -208,6 +209,7 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { configItem={config} engine={engine} errorText={fieldState.error?.message} + onBlur={field.onBlur} onChange={field.onChange} onRemove={() => handleRemoveConfig(index)} /> diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx index acd1a3c046d..78eb0ca831e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseConfigurationItem.tsx @@ -27,12 +27,13 @@ interface Props { configItem?: ConfigurationOption; engine: string; errorText: string | undefined; + onBlur?: () => void; onChange: (config: ConfigValue) => void; onRemove?: (label: string) => void; } export const DatabaseConfigurationItem = (props: Props) => { - const { configItem, engine, errorText, onChange, onRemove } = props; + const { configItem, engine, errorText, onBlur, onChange, onRemove } = props; const configLabel = configItem?.label || ''; const renderInputField = () => { @@ -82,6 +83,7 @@ export const DatabaseConfigurationItem = (props: Props) => { fullWidth label="" name={configLabel} + onBlur={onBlur} onChange={(e) => onChange(Number(e.target.value))} type="number" value={Number(configItem.value)} @@ -101,6 +103,7 @@ export const DatabaseConfigurationItem = (props: Props) => { fullWidth label="" name={configLabel} + onBlur={onBlur} onChange={(e) => onChange(e.target.value)} placeholder={String(configItem.example)} type="text" @@ -126,7 +129,7 @@ export const DatabaseConfigurationItem = (props: Props) => { > {`${engine}.${configLabel}`} - {configItem?.restart_cluster && ( + {configItem?.requires_restart && ( )} {configItem?.description && ( diff --git a/packages/manager/src/features/Databases/utilities.test.ts b/packages/manager/src/features/Databases/utilities.test.ts index 0834e92f814..4ffcf30709c 100644 --- a/packages/manager/src/features/Databases/utilities.test.ts +++ b/packages/manager/src/features/Databases/utilities.test.ts @@ -604,7 +604,7 @@ describe('findConfigItem', () => { example: 600, maximum: 86400, minimum: 600, - restart_cluster: false, + requires_restart: false, type: 'integer', }; @@ -614,7 +614,7 @@ describe('findConfigItem', () => { example: 10, maximum: 3600, minimum: 2, - restart_cluster: false, + requires_restart: false, type: 'integer', }; it('should return the correct ConfigurationItem for a given targetKey', () => { @@ -653,7 +653,7 @@ describe('convertExistingConfigsToArray', () => { label: 'connect_timeout', maximum: 3600, minimum: 2, - restart_cluster: false, + requires_restart: false, type: 'integer', value: 10, }, @@ -666,7 +666,7 @@ describe('convertExistingConfigsToArray', () => { maxLength: 100, minLength: 2, pattern: '^([-+][\\d:]*|[\\w/]*)$', - restart_cluster: false, + requires_restart: false, type: 'string', value: '+03:00', }, @@ -678,7 +678,7 @@ describe('convertExistingConfigsToArray', () => { label: 'binlog_retention_period', maximum: 86400, minimum: 600, - restart_cluster: false, + requires_restart: false, type: 'integer', value: 600, }, diff --git a/packages/manager/src/features/Databases/utilities.ts b/packages/manager/src/features/Databases/utilities.ts index 0ef3190b4ba..42effa10ac5 100644 --- a/packages/manager/src/features/Databases/utilities.ts +++ b/packages/manager/src/features/Databases/utilities.ts @@ -456,6 +456,6 @@ export const getDefaultConfigValue = (config: ConfigurationOption) => { : isConfigStringWithEnum(config) ? config.enum?.[0] ?? '' : config?.type === 'number' || config?.type === 'integer' - ? 0 + ? config.minimum ?? 0 : ''; }; From dc1c05c5a0a928ff312e3acf982d1ed99b074354 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Mon, 7 Apr 2025 16:04:53 +0200 Subject: [PATCH 82/84] fix: [UIE-8447] - update changelog --- packages/api-v4/CHANGELOG.md | 1 + packages/manager/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 85e30c3389b..8514e69fac1 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -8,6 +8,7 @@ ### Changed: - DBaaS Advanced Configurations: remove `engine_config` from the DatabaseEngineConfig type ([#11885](https://github.com/linode/manager/pull/11885)) +- DBaaS Advanced Configurations: rename `restart_cluster` to `requires_restart` to align with the API response ([#11979](https://github.com/linode/manager/pull/11979)) ### Fixed: diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 5ce97db0236..601edf78d3e 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - PAT Token drawer logic when Child Account Access is hidden ([#11935](https://github.com/linode/manager/pull/11935)) - Profile Menu Icon Size Inconsistency ([#11946](https://github.com/linode/manager/pull/11946)) - Unclearable ACL IP addresses for LKE clusters ([#11947](https://github.com/linode/manager/pull/11947)) +- DBaaS Advanced Configuration: drawer shows outdated config values after save and reopen ([#11979](https://github.com/linode/manager/pull/11979)) ### Removed: From ba9482c7ca97851aef7e56c226bef8428224fa13 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Mon, 7 Apr 2025 16:45:22 +0200 Subject: [PATCH 83/84] fix: [UIE-8447] - fix loading state --- .../DatabaseAdvancedConfigurationDrawer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx index 9b3382dc962..28ecf672a6e 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx @@ -97,10 +97,10 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { const configs = watch('configs'); useEffect(() => { - if (databaseConfig && open) { + if (existingConfigsArray && open) { reset({ configs: existingConfigsArray }); } - }, [databaseConfig, open, reset, existingConfigsArray]); + }, [open, existingConfigsArray]); const usedConfigs = useMemo( () => new Set(fields.map((config) => config.label)), From b169b6b870bf12ec87df446a6e7aac2b9ac54f6f Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Mon, 7 Apr 2025 17:22:10 +0200 Subject: [PATCH 84/84] fix: [UIE-8447] - fix delay in drawer --- .../DatabaseAdvancedConfigurationDrawer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx index 28ecf672a6e..a435c9fd20d 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseAdvancedConfiguration/DatabaseAdvancedConfigurationDrawer.tsx @@ -97,10 +97,10 @@ export const DatabaseAdvancedConfigurationDrawer = (props: Props) => { const configs = watch('configs'); useEffect(() => { - if (existingConfigsArray && open) { + if (existingConfigsArray) { reset({ configs: existingConfigsArray }); } - }, [open, existingConfigsArray]); + }, [existingConfigsArray]); const usedConfigs = useMemo( () => new Set(fields.map((config) => config.label)),