Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Invalidate VPC-related queries when deleting a Linode ([#9814](https://github.com/linode/manager/pull/9814))
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { useQueryClient } from 'react-query';

import { Notice } from 'src/components/Notice/Notice';
import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog';
Expand All @@ -9,6 +10,14 @@ import {
useLinodeQuery,
} from 'src/queries/linodes/linodes';

import { useAllLinodeConfigsQuery } from 'src/queries/linodes/configs';
import { vpcQueryKey, subnetQueryKey } from 'src/queries/vpcs';
import { useFlags } from 'src/hooks/useFlags';
import { useAccount } from 'src/queries/account';
import { isFeatureEnabled } from 'src/utilities/accountCapabilities';

import { getVPCsFromLinodeConfigs } from './utils';

interface Props {
linodeId: number | undefined;
onClose: () => void;
Expand All @@ -17,13 +26,28 @@ interface Props {
}

export const DeleteLinodeDialog = (props: Props) => {
const queryClient = useQueryClient();
const flags = useFlags();
const { data: account } = useAccount();

const enableVPCActions = isFeatureEnabled(
'VPCs',
Boolean(flags.vpc),
account?.capabilities ?? []
);
Comment on lines +30 to +37
Copy link
Contributor Author

@coliu-akamai coliu-akamai Oct 18, 2023

Choose a reason for hiding this comment

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

Adding this check to determine when to fire the useAllLinodeConfigsQuery (line 49) -- feature flagging the query, basically


const { linodeId, onClose, onSuccess, open } = props;

const { data: linode } = useLinodeQuery(
linodeId ?? -1,
linodeId !== undefined && open
);

const { data: configs } = useAllLinodeConfigsQuery(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The PR to include a VPC column in the LInodes Landing page table caused a bunch of 429s due to the useAllLinodeConfigsQuery being fired for every single linode. (see prs #9485 and #9625 for context + their respective tickets)

I think that would be less of an issue here bc we only fire this query when we are deleting the linode (and also only if the user has VPC capabilities), not for every single linode, but please lmk if I'm mistaken!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is reasonable πŸ‘

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, you are correct! This is good!

linodeId ?? -1,
linodeId !== undefined && open && enableVPCActions
);

const { error, isLoading, mutateAsync, reset } = useDeleteLinodeMutation(
linodeId ?? -1
);
Expand All @@ -36,6 +60,24 @@ export const DeleteLinodeDialog = (props: Props) => {

const onDelete = async () => {
await mutateAsync();
const vpcIds = enableVPCActions
? getVPCsFromLinodeConfigs(configs ?? [])
: [];
// @TODO VPC: potentially revisit using the linodeEventsHandler in linode/events.ts to invalidate queries rather than here
// See PR #9814 for more details
if (vpcIds.length > 0) {
queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
// invalidate data for specific vpcs this linode is assigned to
vpcIds.forEach((vpcId) => {
queryClient.invalidateQueries([vpcQueryKey, 'vpc', vpcId]);
queryClient.invalidateQueries([
vpcQueryKey,
'vpc',
vpcId,
subnetQueryKey,
]);
});
}
onClose();
resetEventsPolling();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { parseMaintenanceStartTime } from './utils';
import { parseMaintenanceStartTime, getVPCsFromLinodeConfigs } from './utils';
import {
configFactory,
LinodeConfigInterfaceFactory,
LinodeConfigInterfaceFactoryWithVPC,
} from 'src/factories';

describe('Linode Landing Utilites', () => {
it('should return "Maintenance Window Unknown" for invalid dates', () => {
Expand Down Expand Up @@ -26,4 +31,39 @@ describe('Linode Landing Utilites', () => {
expect(parseMaintenanceStartTime(undefined)).toBe('No Maintenance Needed');
expect(parseMaintenanceStartTime(null)).toBe('No Maintenance Needed');
});

describe('getVPCsFromLinodeConfigs', () => {
const vpcInterfaceList = LinodeConfigInterfaceFactoryWithVPC.buildList(2);

it('returns an empty list if there are no vpc interfaces in the configs', () => {
const configs = configFactory.buildList(3);
const vpcIds = getVPCsFromLinodeConfigs(configs);
expect(vpcIds).toEqual([]);
});

it('returns the IDs of vpc-related interfaces', () => {
const config = configFactory.build({
interfaces: [
...vpcInterfaceList,
...LinodeConfigInterfaceFactory.buildList(4),
],
});
const vpcIds = getVPCsFromLinodeConfigs([
...configFactory.buildList(3),
config,
]);
expect(vpcIds).toEqual([2, 3]);
});

it('returns unique vpc ids (no duplicates)', () => {
const vpcInterface = LinodeConfigInterfaceFactoryWithVPC.build({
vpc_id: 2,
});
const config = configFactory.build({
interfaces: [...vpcInterfaceList, vpcInterface],
});
const vpcIds = getVPCsFromLinodeConfigs([config]);
expect(vpcIds).toEqual([2, 3]);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

πŸ‘

});
16 changes: 15 additions & 1 deletion packages/manager/src/features/Linodes/LinodesLanding/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LinodeStatus } from '@linode/api-v4/lib/linodes';
import { Config, LinodeStatus } from '@linode/api-v4/lib/linodes';

import { reportException } from 'src/exceptionReporting';

Expand Down Expand Up @@ -51,3 +51,17 @@ export const getLinodeIconStatus = (status: LinodeStatus) => {
}
return 'other';
};

// Return all (unique) vpc IDs that a linode is assigned to
export const getVPCsFromLinodeConfigs = (configs: Config[]): number[] => {
const vpcIds = new Set<number>();
for (const config of configs) {
for (const linodeInterface of config.interfaces) {
if (linodeInterface.purpose === 'vpc' && linodeInterface.vpc_id) {
vpcIds.add(linodeInterface.vpc_id);
}
}
}

return Array.from(vpcIds);
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface Props {
subnet: SubnetFieldState;
}

// TODO: VPC - currently only supports IPv4, must update when/if IPv6 is also supported
// @TODO VPC: currently only supports IPv4, must update when/if IPv6 is also supported
export const SubnetNode = (props: Props) => {
const { disabled, idx, isRemovable, onChange, subnet } = props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const SubnetCreateDrawer = (props: Props) => {
const { dirty, handleSubmit, resetForm, setValues, values } = useFormik({
enableReinitialize: true,
initialValues: {
// TODO VPC - add IPv6 when that is supported
// @TODO VPC: add IPv6 when that is supported
label: '',
ip: {
ipv4: DEFAULT_SUBNET_IPV4_VALUE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ const VPCDetail = () => {
{numLinodes > 0 && (
<DismissibleBanner
preferenceKey={`reboot-linodes-warning-banner-${vpc.id}`}
sx={{ marginBottom: theme.spacing(2) }}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

see PR description for reasoning behind this additional margin

variant="warning"
>
<Typography variant="body1">
Expand Down