From 16b3d79563b0ca8b5db4bcd3968ec77abba2277b Mon Sep 17 00:00:00 2001 From: Hussain Khalil <122488130+hkhalil-akamai@users.noreply.github.com> Date: Wed, 28 Jun 2023 08:15:25 -0400 Subject: [PATCH 01/12] hotfix: Notification Menu crash due to null value (#9331) --- packages/api-v4/CHANGELOG.md | 7 +++++++ packages/api-v4/package.json | 2 +- packages/api-v4/src/account/types.ts | 2 +- packages/manager/CHANGELOG.md | 7 +++++++ packages/manager/package.json | 2 +- packages/manager/src/eventMessageGenerator.test.ts | 13 +++++++++++++ packages/manager/src/eventMessageGenerator.ts | 12 ++++++------ 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/api-v4/CHANGELOG.md b/packages/api-v4/CHANGELOG.md index 4722e941992..17811bae3c5 100644 --- a/packages/api-v4/CHANGELOG.md +++ b/packages/api-v4/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2023-06-27] - v0.95.1 + + +### Fixed: + +- Updated Entity interface to reflect the possibility of a null label ([#9331](https://github.com/linode/manager/pull/9331)) + ## [2023-06-26] - v0.95.0 ### Added: diff --git a/packages/api-v4/package.json b/packages/api-v4/package.json index 4fdc0fff985..4920f6ba031 100644 --- a/packages/api-v4/package.json +++ b/packages/api-v4/package.json @@ -1,6 +1,6 @@ { "name": "@linode/api-v4", - "version": "0.95.0", + "version": "0.95.1", "homepage": "https://github.com/linode/manager/tree/develop/packages/api-v4", "bugs": { "url": "https://github.com/linode/manager/issues" diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index 2b71fb9a8e1..c0e7d854f2b 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -223,7 +223,7 @@ export interface Notification { export interface Entity { id: number; - label: string; + label: string | null; type: string; url: string; } diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 7d2356fdceb..28a1647bd1b 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2023-06-27] - v1.96.1 + + +### Fixed: + +- Crash when viewing notifications due to `null` label in event entity ([#9331](https://github.com/linode/manager/pull/9331)) + ## [2023-06-26] - v1.96.0 ### Added: diff --git a/packages/manager/package.json b/packages/manager/package.json index 3544425f279..81fd42b037a 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.96.0", + "version": "1.96.1", "private": true, "bugs": { "url": "https://github.com/Linode/manager/issues" diff --git a/packages/manager/src/eventMessageGenerator.test.ts b/packages/manager/src/eventMessageGenerator.test.ts index 015ed63eb6f..7dd2873096c 100644 --- a/packages/manager/src/eventMessageGenerator.test.ts +++ b/packages/manager/src/eventMessageGenerator.test.ts @@ -141,5 +141,18 @@ describe('Event message generation', () => { 'created entity Weird label with special characters.(?) ' ); }); + + it('should work when label is null', () => { + const mockEvent = eventFactory.build({ + entity: entityFactory.build({ + id: 10, + label: null, + }), + }); + const message = 'created entity Null label'; + const result = applyLinking(mockEvent, message); + + expect(result).toEqual('created entity Null label'); + }); }); }); diff --git a/packages/manager/src/eventMessageGenerator.ts b/packages/manager/src/eventMessageGenerator.ts index f5055b167ec..718144a6b87 100644 --- a/packages/manager/src/eventMessageGenerator.ts +++ b/packages/manager/src/eventMessageGenerator.ts @@ -54,19 +54,19 @@ export const eventMessageCreators: { [index: string]: CreatorsForStatus } = { }, community_question_reply: { notification: (e) => - e.entity + e.entity?.label ? `There has been a reply to your thread "${e.entity.label}".` : `There has been a reply to your thread.`, }, community_like: { notification: (e) => - e.entity + e.entity?.label ? `A post on "${e.entity.label}" has been liked.` : `There has been a like on your community post.`, }, community_mention: { notification: (e) => - e.entity + e.entity?.label ? `You have been mentioned in a Community post: ${e.entity.label}.` : `You have been mentioned in a Community post.`, }, @@ -790,7 +790,7 @@ export default (e: Event): string => { /** finally return some default fallback text */ return e.message ? formatEventWithAPIMessage(e) - : `${e.action}${e.entity ? ` on ${e.entity.label}` : ''}`; + : `${e.action}${e.entity?.label ? ` on ${e.entity.label}` : ''}`; } let message = ''; @@ -872,7 +872,7 @@ export function applyLinking(event: Event, message: string) { let newMessage = message; - if (event.entity && entityLinkTarget) { + if (event.entity?.label && entityLinkTarget) { const label = event.entity.label; const nonTickedLabels = new RegExp(`(?${event.secondary_entity.label}` From c335cb183948ab67cf6d1b2eb0fa1fd4de3ced20 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 15:54:35 -0400 Subject: [PATCH 02/12] Fix: [M3-6800] improve FW custom ports validation --- .../Rules/FirewallRuleDrawer.utils.ts | 4 +- packages/validation/src/firewalls.schema.ts | 90 +++++++++++++++++-- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts index fad2eb3658b..5351b3fe68b 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts @@ -12,8 +12,8 @@ import { predefinedFirewallFromRule, } from 'src/features/Firewalls/shared'; import { - CUSTOM_PORTS_VALIDATION_REGEX, CUSTOM_PORTS_ERROR_MESSAGE, + runCustomPortsValidation, } from '@linode/validation'; import type { FirewallRuleProtocol, @@ -332,7 +332,7 @@ export const validateForm = ({ if ((protocol === 'ICMP' || protocol === 'IPENCAP') && ports) { errors.ports = `Ports are not allowed for ${protocol} protocols.`; - } else if (ports && !ports.match(CUSTOM_PORTS_VALIDATION_REGEX)) { + } else if (ports && !runCustomPortsValidation(ports)) { errors.ports = CUSTOM_PORTS_ERROR_MESSAGE; } diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index b9aa0d36946..38754ca85e1 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -5,8 +5,8 @@ import { array, mixed, number, object, string } from 'yup'; export const IP_ERROR_MESSAGE = 'Must be a valid IPv4 or IPv6 address or range.'; -export const CUSTOM_PORTS_ERROR_MESSAGE = - 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; +// export const CUSTOM_PORTS_ERROR_MESSAGE = +// 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; export const CUSTOM_PORTS_VALIDATION_REGEX = /^(?:\d+|\d+-\d+|(?:\d+,\s*)*\d+)$/; export const validateIP = (ipAddress?: string | null): boolean => { @@ -42,10 +42,88 @@ export const ipAddress = string().test({ test: validateIP, }); -export const validateFirewallPorts = string().matches( - CUSTOM_PORTS_VALIDATION_REGEX, - CUSTOM_PORTS_ERROR_MESSAGE -); +export let CUSTOM_PORTS_ERROR_MESSAGE = + 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; + +const validatePort = (port: string): boolean => { + if (!port) { + CUSTOM_PORTS_ERROR_MESSAGE = 'Must be 1-65535'; + return false; + } + + const convertedPort = parseInt(port, 10); + if (!(1 <= convertedPort && convertedPort <= 65535)) { + CUSTOM_PORTS_ERROR_MESSAGE = 'Must be 1-65535'; + return false; + } + + if (String(convertedPort) !== port) { + CUSTOM_PORTS_ERROR_MESSAGE = 'Port must not have leading zeroes'; + return false; + } + + return true; +}; + +export const runCustomPortsValidation = (value: string): boolean => { + const portList = value?.split(',') || []; + let portLimitCount = 0; + + for (const port of portList) { + const cleanedPort = port.trim(); + + if (cleanedPort.includes('-')) { + const portRange = cleanedPort.split('-'); + + if (!validatePort(portRange[0]) || !validatePort(portRange[1])) { + return false; + } + + if (portRange.length !== 2) { + CUSTOM_PORTS_ERROR_MESSAGE = 'Ranges must have 2 values'; + return false; + } + + if (parseInt(portRange[0], 10) >= parseInt(portRange[1], 10)) { + CUSTOM_PORTS_ERROR_MESSAGE = + 'Range must start with a smaller number and end with a larger number'; + return false; + } + + portLimitCount += 2; + } else { + if (!validatePort(cleanedPort)) { + return false; + } + portLimitCount++; + } + } + + if (portLimitCount > 15) { + CUSTOM_PORTS_ERROR_MESSAGE = + 'Number of ports or port range endpoints exceeded. Max allowed is 15'; + return false; + } + + return true; +}; + +const validateFirewallPorts = string().test({ + name: 'firewall-ports', + message: CUSTOM_PORTS_ERROR_MESSAGE, + test: (value) => { + if (!value) { + return false; + } + + try { + runCustomPortsValidation(value); + } catch (err) { + return false; + } + return true; + }, +}); const validFirewallRuleProtocol = ['ALL', 'TCP', 'UDP', 'ICMP', 'IPENCAP']; export const FirewallRuleTypeSchema = object().shape({ From ffd6a142089b0465d6f1397e5033ce3d7775a768 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 16:05:56 -0400 Subject: [PATCH 03/12] Fix: [M3-6800] increased test coverage --- .../Rules/FirewallRuleDrawer.test.tsx | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.test.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.test.tsx index 124d37fddb7..2715d6b6de8 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.test.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.test.tsx @@ -121,7 +121,8 @@ describe('utilities', () => { label: 'Firewalllabel', addresses: 'All IPv4', }; - expect(validateForm({ protocol: 'TCP', ports: '1', ...rest })).toEqual( + // SUCCESS CASES + expect(validateForm({ protocol: 'TCP', ports: '1234', ...rest })).toEqual( {} ); expect( @@ -134,28 +135,46 @@ describe('utilities', () => { {} ); expect( - validateForm({ protocol: 'TCP', ports: 'abc', ...rest }) - ).toHaveProperty( - 'ports', - 'Ports must be an integer, range of integers, or a comma-separated list of integers.' - ); - expect( - validateForm({ protocol: 'TCP', ports: '1--20', ...rest }) - ).toHaveProperty( - 'ports', - 'Ports must be an integer, range of integers, or a comma-separated list of integers.' - ); + validateForm({ + protocol: 'TCP', + ports: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15', + ...rest, + }) + ).toEqual({}); expect( validateForm({ protocol: 'TCP', ports: '1-2,3-4', ...rest }) + ).toEqual({}); + expect( + validateForm({ protocol: 'TCP', ports: '1,5-12', ...rest }) + ).toEqual({}); + // FAILURE CASES + expect( + validateForm({ protocol: 'TCP', ports: '1,21-12', ...rest }) ).toHaveProperty( 'ports', - 'Ports must be an integer, range of integers, or a comma-separated list of integers.' + 'Range must start with a smaller number and end with a larger number' ); + expect( + validateForm({ protocol: 'TCP', ports: '1-21-45', ...rest }) + ).toHaveProperty('ports', 'Ranges must have 2 values'); + expect( + validateForm({ protocol: 'TCP', ports: 'abc', ...rest }) + ).toHaveProperty('ports', 'Must be 1-65535'); + expect( + validateForm({ protocol: 'TCP', ports: '1--20', ...rest }) + ).toHaveProperty('ports', 'Must be 1-65535'); expect( validateForm({ protocol: 'TCP', ports: '-20', ...rest }) + ).toHaveProperty('ports', 'Must be 1-65535'); + expect( + validateForm({ + protocol: 'TCP', + ports: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16', + ...rest, + }) ).toHaveProperty( 'ports', - 'Ports must be an integer, range of integers, or a comma-separated list of integers.' + 'Number of ports or port range endpoints exceeded. Max allowed is 15' ); }); it('validates label', () => { From 55e61665ca4ee4fd628cd5a95264c0f7a4b3d496 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 16:09:55 -0400 Subject: [PATCH 04/12] Added changeset: Firewall custom ports validation --- packages/manager/.changeset/pr-9336-fixed-1687982995866.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-9336-fixed-1687982995866.md diff --git a/packages/manager/.changeset/pr-9336-fixed-1687982995866.md b/packages/manager/.changeset/pr-9336-fixed-1687982995866.md new file mode 100644 index 00000000000..af3816b78e1 --- /dev/null +++ b/packages/manager/.changeset/pr-9336-fixed-1687982995866.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Fixed +--- + +Firewall custom ports validation ([#9336](https://github.com/linode/manager/pull/9336)) From c45465f69e2c454ca9826a2b50c5f28a51e710ad Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 16:10:41 -0400 Subject: [PATCH 05/12] Added changeset: Firewall custom port validation --- .../validation/.changeset/pr-9336-fixed-1687983041491.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/validation/.changeset/pr-9336-fixed-1687983041491.md diff --git a/packages/validation/.changeset/pr-9336-fixed-1687983041491.md b/packages/validation/.changeset/pr-9336-fixed-1687983041491.md new file mode 100644 index 00000000000..69266a068bb --- /dev/null +++ b/packages/validation/.changeset/pr-9336-fixed-1687983041491.md @@ -0,0 +1,5 @@ +--- +"@linode/validation": Fixed +--- + +Firewall custom port validation ([#9336](https://github.com/linode/manager/pull/9336)) From fd75dc58051b7dd9fd1b2da0b8fa9d1e706b59cd Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 16:11:21 -0400 Subject: [PATCH 06/12] Fix: [M3-6800] cleanup --- packages/validation/src/firewalls.schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index 38754ca85e1..9c73e59639a 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -5,8 +5,6 @@ import { array, mixed, number, object, string } from 'yup'; export const IP_ERROR_MESSAGE = 'Must be a valid IPv4 or IPv6 address or range.'; -// export const CUSTOM_PORTS_ERROR_MESSAGE = -// 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; export const CUSTOM_PORTS_VALIDATION_REGEX = /^(?:\d+|\d+-\d+|(?:\d+,\s*)*\d+)$/; export const validateIP = (ipAddress?: string | null): boolean => { From 559663aa42585ae3209edd7d2c4a06f6652cef7a Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 16:15:47 -0400 Subject: [PATCH 07/12] Fix: [M3-6800] moaaar cleanup --- packages/validation/src/firewalls.schema.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index 9c73e59639a..f5cc6718a8c 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -5,7 +5,6 @@ import { array, mixed, number, object, string } from 'yup'; export const IP_ERROR_MESSAGE = 'Must be a valid IPv4 or IPv6 address or range.'; -export const CUSTOM_PORTS_VALIDATION_REGEX = /^(?:\d+|\d+-\d+|(?:\d+,\s*)*\d+)$/; export const validateIP = (ipAddress?: string | null): boolean => { if (!ipAddress) { From d1bed3fd4e38c7553bf53c41a3dda9aa8fe0313d Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Wed, 28 Jun 2023 17:15:45 -0400 Subject: [PATCH 08/12] Fix: [M3-6800] better naming conventions and JSDoc --- .../Rules/FirewallRuleDrawer.utils.ts | 4 ++-- packages/validation/src/firewalls.schema.ts | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts index 5351b3fe68b..e19971e62a5 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts @@ -13,7 +13,7 @@ import { } from 'src/features/Firewalls/shared'; import { CUSTOM_PORTS_ERROR_MESSAGE, - runCustomPortsValidation, + isCustomPortsValid, } from '@linode/validation'; import type { FirewallRuleProtocol, @@ -332,7 +332,7 @@ export const validateForm = ({ if ((protocol === 'ICMP' || protocol === 'IPENCAP') && ports) { errors.ports = `Ports are not allowed for ${protocol} protocols.`; - } else if (ports && !runCustomPortsValidation(ports)) { + } else if (ports && !isCustomPortsValid(ports)) { errors.ports = CUSTOM_PORTS_ERROR_MESSAGE; } diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index f5cc6718a8c..df25c4d2a32 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -42,6 +42,11 @@ export const ipAddress = string().test({ export let CUSTOM_PORTS_ERROR_MESSAGE = 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; +/** + * @param port + * @returns boolean + * @description Validates a single port or port range and sets the error message + */ const validatePort = (port: string): boolean => { if (!port) { CUSTOM_PORTS_ERROR_MESSAGE = 'Must be 1-65535'; @@ -62,8 +67,13 @@ const validatePort = (port: string): boolean => { return true; }; -export const runCustomPortsValidation = (value: string): boolean => { - const portList = value?.split(',') || []; +/** + * @param ports + * @returns boolean + * @description Validates a comma-separated list of ports and port ranges and sets the error message + */ +export const isCustomPortsValid = (ports: string): boolean => { + const portList = ports?.split(',') || []; let portLimitCount = 0; for (const port of portList) { @@ -114,7 +124,7 @@ const validateFirewallPorts = string().test({ } try { - runCustomPortsValidation(value); + isCustomPortsValid(value); } catch (err) { return false; } From 17ae8a69abeee330e017933b68eb49d3625b078e Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 29 Jun 2023 12:16:51 -0400 Subject: [PATCH 09/12] Fix: [M3-6800] Improved feedback --- packages/validation/src/firewalls.schema.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/validation/src/firewalls.schema.ts b/packages/validation/src/firewalls.schema.ts index df25c4d2a32..e1a2eeb6e4f 100644 --- a/packages/validation/src/firewalls.schema.ts +++ b/packages/validation/src/firewalls.schema.ts @@ -39,8 +39,7 @@ export const ipAddress = string().test({ test: validateIP, }); -export let CUSTOM_PORTS_ERROR_MESSAGE = - 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; +export let CUSTOM_PORTS_ERROR_MESSAGE = ''; /** * @param port @@ -48,6 +47,9 @@ export let CUSTOM_PORTS_ERROR_MESSAGE = * @description Validates a single port or port range and sets the error message */ const validatePort = (port: string): boolean => { + CUSTOM_PORTS_ERROR_MESSAGE = + 'Ports must be an integer, range of integers, or a comma-separated list of integers.'; + if (!port) { CUSTOM_PORTS_ERROR_MESSAGE = 'Must be 1-65535'; return false; @@ -59,11 +61,15 @@ const validatePort = (port: string): boolean => { return false; } - if (String(convertedPort) !== port) { + if (port.startsWith('0')) { CUSTOM_PORTS_ERROR_MESSAGE = 'Port must not have leading zeroes'; return false; } + if (String(convertedPort) !== port) { + return false; + } + return true; }; From 1e16579ae70beb9f704220008299734553c32201 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 29 Jun 2023 15:15:38 -0400 Subject: [PATCH 10/12] remove unecessary changesets --- packages/manager/.changeset/pr-9336-fixed-1687982995866.md | 5 ----- .../validation/.changeset/pr-9336-fixed-1687983041491.md | 5 ----- 2 files changed, 10 deletions(-) delete mode 100644 packages/manager/.changeset/pr-9336-fixed-1687982995866.md delete mode 100644 packages/validation/.changeset/pr-9336-fixed-1687983041491.md diff --git a/packages/manager/.changeset/pr-9336-fixed-1687982995866.md b/packages/manager/.changeset/pr-9336-fixed-1687982995866.md deleted file mode 100644 index af3816b78e1..00000000000 --- a/packages/manager/.changeset/pr-9336-fixed-1687982995866.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/manager": Fixed ---- - -Firewall custom ports validation ([#9336](https://github.com/linode/manager/pull/9336)) diff --git a/packages/validation/.changeset/pr-9336-fixed-1687983041491.md b/packages/validation/.changeset/pr-9336-fixed-1687983041491.md deleted file mode 100644 index 69266a068bb..00000000000 --- a/packages/validation/.changeset/pr-9336-fixed-1687983041491.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@linode/validation": Fixed ---- - -Firewall custom port validation ([#9336](https://github.com/linode/manager/pull/9336)) From 622fb260cd5561597db365887729f34c863daabd Mon Sep 17 00:00:00 2001 From: Banks Nussman <115251059+bnussman-akamai@users.noreply.github.com> Date: Wed, 28 Jun 2023 12:59:24 -0400 Subject: [PATCH 11/12] Cherry Picked 309337e - Linodes Clone Fix --- packages/manager/CHANGELOG.md | 11 +- packages/manager/package.json | 2 +- packages/manager/src/App.tsx | 16 -- .../src/containers/withLinodes.container.ts | 121 ++++-------- .../src/features/Account/EnableManaged.tsx | 26 +-- .../src/features/Account/GlobalSettings.tsx | 41 ++-- .../src/features/Images/ImageSelect.test.tsx | 4 - .../src/features/Images/ImagesDrawer.tsx | 9 +- .../NodePoolsDisplay/NodeTable.tsx | 15 +- .../Linodes/CloneLanding/CloneLanding.tsx | 134 ++++--------- .../features/Linodes/CloneLanding/Details.tsx | 8 +- .../Linodes/LinodeSelect/LinodeSelect.tsx | 38 +--- .../features/Linodes/LinodeSelect/index.tsx | 2 - .../Linodes/LinodesCreate/LinodeCreate.tsx | 2 +- .../LinodesCreate/LinodeCreateContainer.tsx | 25 +-- .../features/Linodes/LinodesCreate/types.ts | 6 - .../LinodesDetail/HostMaintenanceError.tsx | 4 +- .../LinodeActivity/LinodeActivity.test.tsx | 18 -- .../LinodeActivity/LinodeActivity.tsx | 27 +-- .../LinodesDetail/LinodeActivity/index.ts | 2 - .../LinodeBackup/BackupsPlaceholder.tsx | 2 +- .../LinodeBackup/LinodeBackups.tsx | 2 +- .../LinodeDetailErrorBoundary.tsx | 31 --- .../LinodeNetworking/LinodeNetworking.tsx | 2 +- .../LinodesDetail/LinodePermissionsError.tsx | 4 +- .../LinodeRebuild/LinodeRebuildDialog.tsx | 4 +- .../LinodeRescue/StandardRescueDialog.tsx | 2 +- .../LinodeResize/LinodeResize.tsx | 4 +- .../LinodeSettings/ImageAndPassword.test.tsx | 2 +- .../LinodeSettings/ImageAndPassword.tsx | 33 +--- .../LinodeSettings/LinodeSettings.tsx | 2 +- .../LinodeStorage/CreateDiskDrawer.tsx | 3 +- .../LinodesDetail/LinodesDetail.container.tsx | 71 ------- .../Linodes/LinodesDetail/LinodesDetail.tsx | 72 +++---- .../LinodesDetailHeader/Notifications.tsx | 7 +- .../features/Linodes/LinodesDetail/README.md | 159 ---------------- .../features/Linodes/LinodesDetail/index.tsx | 2 - .../LinodesDetail/linodeDetailContext.tsx | 155 --------------- .../manager/src/features/Linodes/index.tsx | 2 +- .../NodeBalancers/ConfigNodeIPSelect.tsx | 2 +- .../NotificationData/RenderProgressEvent.tsx | 13 +- .../features/Search/SearchLanding.test.tsx | 4 - .../src/features/Search/SearchLanding.tsx | 30 ++- .../SupportTickets/SupportTicketDrawer.tsx | 179 ++++++------------ .../features/TopMenu/SearchBar/SearchBar.tsx | 41 ++-- .../Volumes/DestructiveVolumeDialog.tsx | 9 +- .../Volumes/VolumeAttachmentDrawer.tsx | 2 +- .../Volumes/VolumeCreate/CreateVolumeForm.tsx | 2 +- packages/manager/src/hooks/useEntities.ts | 47 ----- .../manager/src/hooks/useExtendedLinode.ts | 57 ------ .../manager/src/hooks/useLinodeActions.ts | 43 ----- packages/manager/src/hooks/useLinodes.ts | 19 -- .../manager/src/hooks/useReduxLoad.test.ts | 25 --- packages/manager/src/hooks/useReduxLoad.ts | 121 ------------ .../manager/src/queries/linodes/linodes.ts | 11 ++ .../src/store/linodes/linode.containers.ts | 33 ---- .../manager/src/utilities/testHelpersStore.ts | 3 - packages/validation/CHANGELOG.md | 5 + packages/validation/package.json | 2 +- 59 files changed, 304 insertions(+), 1414 deletions(-) delete mode 100644 packages/manager/src/features/Linodes/LinodeSelect/index.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/LinodeActivity.test.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeActivity/index.ts delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodeDetailErrorBoundary.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/LinodesDetail.container.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/README.md delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/index.tsx delete mode 100644 packages/manager/src/features/Linodes/LinodesDetail/linodeDetailContext.tsx delete mode 100644 packages/manager/src/hooks/useEntities.ts delete mode 100644 packages/manager/src/hooks/useExtendedLinode.ts delete mode 100644 packages/manager/src/hooks/useLinodeActions.ts delete mode 100644 packages/manager/src/hooks/useLinodes.ts delete mode 100644 packages/manager/src/hooks/useReduxLoad.test.ts delete mode 100644 packages/manager/src/hooks/useReduxLoad.ts delete mode 100644 packages/manager/src/store/linodes/linode.containers.ts delete mode 100644 packages/manager/src/utilities/testHelpersStore.ts diff --git a/packages/manager/CHANGELOG.md b/packages/manager/CHANGELOG.md index 28a1647bd1b..ae479346b6f 100644 --- a/packages/manager/CHANGELOG.md +++ b/packages/manager/CHANGELOG.md @@ -4,8 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [2023-06-27] - v1.96.1 +## [2023-06-29] - v1.96.2 + +### Fixed: +- Issue where Cloud Manager was not displaying all linodes capable of being "cloned" ([#9294](https://github.com/linode/manager/pull/9294)) +- Firewall custom ports validation w/ unit tests ([#9336](https://github.com/linode/manager/pull/9336)) +### Tech Stories: + +- React Query - Linodes - General Refactors ([#9294](https://github.com/linode/manager/pull/9294)) + +## [2023-06-27] - v1.96.1 ### Fixed: diff --git a/packages/manager/package.json b/packages/manager/package.json index 81fd42b037a..ffe433c5228 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "1.96.1", + "version": "1.96.2", "private": true, "bugs": { "url": "https://github.com/Linode/manager/issues" diff --git a/packages/manager/src/App.tsx b/packages/manager/src/App.tsx index 61ce13db465..aeee3c078f4 100644 --- a/packages/manager/src/App.tsx +++ b/packages/manager/src/App.tsx @@ -20,7 +20,6 @@ import { ADOBE_ANALYTICS_URL, NUM_ADOBE_SCRIPTS } from './constants'; import { reportException } from './exceptionReporting'; import { useAuthentication } from './hooks/useAuthentication'; import useFeatureFlagsLoad from './hooks/useFeatureFlagLoad'; -import useLinodes from './hooks/useLinodes'; import { loadScript } from './hooks/useScript'; import { oauthClientsEventHandler } from './queries/accountOAuth'; import { databaseEventsHandler } from './queries/databases'; @@ -59,12 +58,6 @@ const BaseApp = withFeatureFlagProvider( const { enqueueSnackbar } = useSnackbar(); - const { - linodes: { - error: { read: linodesError }, - }, - } = useLinodes(); - const [goToOpen, setGoToOpen] = React.useState(false); const theme = preferences?.theme; @@ -263,15 +256,6 @@ const BaseApp = withFeatureFlagProvider( }; }, [handleMigrationEvent]); - /** - * in the event that we encounter an "invalid OAuth token" error from the API, - * we can simply refrain from rendering any content since the user will - * imminently be redirected to the login page. - */ - if (hasOauthError(linodesError)) { - return null; - } - return ( }> {/** Accessibility helper */} diff --git a/packages/manager/src/containers/withLinodes.container.ts b/packages/manager/src/containers/withLinodes.container.ts index 7655fc204b8..cf3fc95186d 100644 --- a/packages/manager/src/containers/withLinodes.container.ts +++ b/packages/manager/src/containers/withLinodes.container.ts @@ -1,91 +1,40 @@ -import { Linode } from '@linode/api-v4/lib/linodes'; -import { APIError, Filter, Params } from '@linode/api-v4/lib/types'; -import { path } from 'ramda'; -import { connect, InferableComponentEnhancerWithProps } from 'react-redux'; -import { ApplicationState } from 'src/store'; -import { requestLinodes } from 'src/store/linodes/linode.requests'; -import { State } from 'src/store/linodes/linodes.reducer'; -import { LinodeWithMaintenanceAndDisplayStatus } from 'src/store/linodes/types'; -import { ThunkDispatch } from 'src/store/types'; -import { GetAllData } from 'src/utilities/getAll'; - -export interface DispatchProps { - getLinodes: ( - params?: Params, - filters?: Filter - ) => Promise>; +import React from 'react'; +import { CreateLinodeRequest, Linode } from '@linode/api-v4/lib/linodes'; +import { APIError } from '@linode/api-v4/lib/types'; +import { + useAllLinodesQuery, + useCreateLinodeMutation, +} from 'src/queries/linodes/linodes'; + +interface Actions { + createLinode: (data: CreateLinodeRequest) => Promise; } -/* tslint:disable-next-line */ -export interface StateProps { - linodesError?: APIError[]; - linodesLoading: State['loading']; - linodesData: LinodeWithMaintenanceAndDisplayStatus[]; - linodesLastUpdated: State['lastUpdated']; - linodesResults: State['results']; +export interface WithLinodesProps { + linodesError: APIError[] | null; + linodesLoading: boolean; + linodesData: Linode[] | undefined; + linodeActions: Actions; } -type MapProps = ( - ownProps: OwnProps, - linodes: Linode[], - loading: boolean, - error?: APIError[] -) => ReduxStateProps & Partial; - -export type Props = DispatchProps & StateProps; - -interface Connected { - ( - mapStateToProps: MapProps - ): InferableComponentEnhancerWithProps< - ReduxStateProps & Partial & DispatchProps & OwnProps, - OwnProps - >; - (): InferableComponentEnhancerWithProps< - ReduxStateProps & DispatchProps & OwnProps, - OwnProps - >; -} - -const connected: Connected = ( - mapStateToProps?: MapProps -) => - connect< - (ReduxState & Partial) | StateProps, - DispatchProps, - OwnProps, - ApplicationState - >( - (state, ownProps) => { - const { - loading, - error, - itemsById, - lastUpdated, - results, - } = state.__resources.linodes; - const linodes = Object.values(itemsById); - if (mapStateToProps) { - return mapStateToProps( - ownProps, - linodes, - loading, - path(['read'], error) - ); - } - - return { - linodesError: path(['read'], error), - linodesLoading: loading, - linodesData: linodes, - linodesResults: results, - linodesLastUpdated: lastUpdated, - }; +export const withLinodes = ( + Component: React.ComponentType +) => (props: Props) => { + const { + data: linodesData, + isLoading: linodesLoading, + error: linodesError, + } = useAllLinodesQuery(); + + const { mutateAsync: createLinode } = useCreateLinodeMutation(); + + return React.createElement(Component, { + ...props, + linodesData, + linodesLoading, + linodesError, + linodeActions: { + createLinode, }, - (dispatch: ThunkDispatch) => ({ - getLinodes: (params, filter) => - dispatch(requestLinodes({ params, filter })), - }) - ); - -export default connected; + }); +}; diff --git a/packages/manager/src/features/Account/EnableManaged.tsx b/packages/manager/src/features/Account/EnableManaged.tsx index 84e2a859676..486c9682966 100644 --- a/packages/manager/src/features/Account/EnableManaged.tsx +++ b/packages/manager/src/features/Account/EnableManaged.tsx @@ -9,27 +9,20 @@ import Typography from 'src/components/core/Typography'; import ExternalLink from 'src/components/ExternalLink'; import Grid from '@mui/material/Unstable_Grid2'; import { SupportLink } from 'src/components/SupportLink'; -import withLinodes, { - DispatchProps, -} from 'src/containers/withLinodes.container'; import { pluralize } from 'src/utilities/pluralize'; import { updateAccountSettingsData } from 'src/queries/accountSettings'; import { useQueryClient } from 'react-query'; +import { useLinodesQuery } from 'src/queries/linodes/linodes'; interface Props { isManaged: boolean; } -interface StateProps { - linodeCount: number; -} - -type CombinedProps = Props & StateProps & DispatchProps; - interface ContentProps { isManaged: boolean; openConfirmationModal: () => void; } + export const ManagedContent = (props: ContentProps) => { const { isManaged, openConfirmationModal } = props; @@ -66,13 +59,16 @@ export const ManagedContent = (props: ContentProps) => { ); }; -export const EnableManaged = (props: CombinedProps) => { - const { isManaged, linodeCount } = props; +export const EnableManaged = (props: Props) => { + const { isManaged } = props; const queryClient = useQueryClient(); + const { data: linodes } = useLinodesQuery(); const [isOpen, setOpen] = React.useState(false); const [error, setError] = React.useState(); const [isLoading, setLoading] = React.useState(false); + const linodeCount = linodes?.results ?? 0; + const handleClose = () => { setOpen(false); setError(undefined); @@ -94,7 +90,7 @@ export const EnableManaged = (props: CombinedProps) => { .catch(handleError); }; - const actions = () => ( + const actions = ( - ) -}; - -const enhanced = withLinodeDetailContext(({ linode, updateLinode }) => ({ - linode, - updateLinode, -})); - -export default enhanced(myComponent); -``` - -### LinodeDetailContextConsumer. - -The `LinodeDetailContextConsumer` is a render function as children component which allows you to subscribe the wrapped component to prop changes. - -```jsx -// src/features/Linodes/LinodesDetail/MyComponent.tsx -/** Hooks used for berevity, they are not used in this PR. */ -import { LinodeDetailContextConsumer } from './context' - -const MyComponent = (props) => { - const [newLabel, setNewLabel] = useState(linode.label); - - return ( - - {({ linode, updateLinode }) => ( -

Rename {linode.label}

- setLabel(e.target.value)} value={label} /> - - )} -
- ) -}; - -export default myComponent; -``` - -### useLinodeDetailContext (NYI) - -**N**ot **Y**et **I**mplemented - This is an example of what the Linode Detail Context usage could look like with hooks. This is possible, with all of 1 line of code, but you should wait for Enzyme to be updated for Hooks or agree on a [new testing strategy](https://github.com/kentcdodds/react-testing-library). - -```jsx -// src/features/Linodes/LinodesDetail/MyComponent.tsx -import { LinodeDetailContextConsumer } from './context'; - -const MyComponent = props => { - const [newLabel, setNewLabel] = useState(linode.label); - const { linode, updateLinode } = useLinodeDetailContext(); - - return ( - <> -

Rename {linode.label}

- setLabel(e.target.value)} value={label} /> - - - ); -}; - -export default myComponent; -``` - -# LinodesDetail Container - -_Note: This could probably be better named._ - -LinodesDetail.container will create a data object or rendering a error/loading/empty state component based on the state of Redux. - -The LinodesDetail.container file exists to build an extended Linode object, or render a loading, error, or empty components given the state of Redux. - -The LinodesDetail.container file exists to build an extended Linode object, or render a -loading, error, or empty components given the state of that object. - -1. Request all the configs for the Linode. If the linodeId changes we make the request again. No additional props are injected. - -2. Request all the disks for the Linode. If the linodeId changes we make the request again. No additional props are injected. - -3. Combine error states of relevant requests. Render the ErrorState component if any error is defined. This is an early return. No additional props are injected. - -4. Combine loading states of relevant requests. Render the CircleProgress if any are true. This is an early return. No additional props are injected. - -5. Finally if the Linode object is defined, we merge the secondary data onto the Linode and pass it as a prop `linode` to the wrapped component. Render the NotFound component if the Linode object is undefined, as it was not found in Redux. - -The maybeRenderError, maybeRenderLoading, and maybeWithExtendedLinode all use Recompose's [branch](https://github.com/acdlite/recompose/blob/master/docs/API.md#branch) function. This is a great way to early return and prevent unnecessary computation. - -# A Working Work in Progress - -Not all nested components have been updated to make use of this new pattern. The LinodeConfigSelectionDrawer could probably benefit from some work. Any nested components that are directly requesting the Linode, configs, or disks (or really any data at this point) should probably be going through Redux, and thus the context. - -Should we add `_backups`? Probably. -Should we add `_networking`? Probably. - -Have a look at disks and configs state management. Notice a very obvious pattern? Would could extract that to some sort of factory pattern. But should we? The indirection comes at cost. - -# Todo - -The following requests are not on the Linode Context Detail. Each should be reviewed for their effect on the state of the Linode and maybe added as preconfigured handlers. The more information you pump through the Store the more reactive you can make the application. - -- [ ] getLinode -- [ ] deleteLinode -- [ ] getBackups -- [ ] createSnapshot -- [ ] cancelBackups -- [ ] enableBackups -- [ ] getBackup -- [ ] restoreBackup -- [ ] bootLinode -- [ ] cloneLinode -- [ ] resetDiskRootPassword -- [ ] getNetworkingInformation -- [ ] allocateIPv4Address -- [ ] getIPAddress -- [ ] updateIPAddress -- [ ] deleteIPAddress -- [ ] initiatePendingMigration -- [ ] updateLinode -- [ ] rebootLinode -- [ ] rebuildLinode -- [ ] rescueLinode -- [ ] resizeLinode -- [ ] shutdownLinode -- [ ] viewLinodeStats -- [ ] viewLinodeYYMMStatis diff --git a/packages/manager/src/features/Linodes/LinodesDetail/index.tsx b/packages/manager/src/features/Linodes/LinodesDetail/index.tsx deleted file mode 100644 index e40bf3a6f23..00000000000 --- a/packages/manager/src/features/Linodes/LinodesDetail/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import LinodesDetail from './LinodesDetail.container'; -export default LinodesDetail; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/linodeDetailContext.tsx b/packages/manager/src/features/Linodes/LinodesDetail/linodeDetailContext.tsx deleted file mode 100644 index 4e4a12db159..00000000000 --- a/packages/manager/src/features/Linodes/LinodesDetail/linodeDetailContext.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Linode } from '@linode/api-v4/lib/linodes'; -import * as React from 'react'; -import { createHOCForConsumer } from 'src/requestableContext'; -import { - CreateLinodeConfigResponse, - DeleteLinodeConfigResponse, - GetLinodeConfigResponse, - GetLinodeConfigsResponse, - LinodeConfigCreateFields, - LinodeConfigUpdateFields, - UpdateLinodeConfigResponse, -} from 'src/store/linodes/config/config.actions'; -import { - createLinodeConfig as _createLinodeConfig, - deleteLinodeConfig as _deleteLinodeConfig, - getLinodeConfig as _getLinodeConfig, - getLinodeConfigs as _getLinodeConfigs, - updateLinodeConfig as _updateLinodeConfig, -} from 'src/store/linodes/config/config.requests'; -import { - CreateLinodeDiskResponse, - DeleteLinodeDiskResponse, - GetLinodeDiskResponse, - GetLinodeDisksResponse, - LinodeDiskCreateFields, - LinodeDiskUpdateFields, - ResizeLinodeDiskResponse, - UpdateLinodeDiskResponse, -} from 'src/store/linodes/disk/disk.actions'; -import { - createLinodeDisk as _createLinodeDisk, - deleteLinodeDisk as _deleteLinodeDisk, - getLinodeDisk as _getLinodeDisk, - getLinodeDisks as _getLinodeDisks, - resizeLinodeDisk as _resizeLinodeDisk, - updateLinodeDisk as _updateLinodeDisk, -} from 'src/store/linodes/disk/disk.requests'; -import { updateLinode as _updateLinode } from 'src/store/linodes/linode.requests'; -import { ThunkDispatch } from 'src/store/types'; -import { ExtendedLinode } from './types'; - -export type CreateLinodeConfig = ( - data: LinodeConfigCreateFields -) => CreateLinodeConfigResponse; - -export type DeleteLinodeConfig = ( - configId: number -) => DeleteLinodeConfigResponse; - -export type GetLinodeConfig = (configId: number) => GetLinodeConfigResponse; - -export type GetLinodeConfigs = () => GetLinodeConfigsResponse; - -export type UpdateLinodeConfig = ( - configId: number, - data: LinodeConfigUpdateFields -) => UpdateLinodeConfigResponse; - -export type CreateLinodeDisk = ( - data: LinodeDiskCreateFields -) => CreateLinodeDiskResponse; - -export type DeleteLinodeDisk = (diskId: number) => DeleteLinodeDiskResponse; - -export type ResizeLinodeDisk = ( - diskId: number, - size: number -) => ResizeLinodeDiskResponse; - -export type GetLinodeDisk = (diskId: number) => GetLinodeDiskResponse; - -export type GetLinodeDisks = () => GetLinodeDisksResponse; - -export type UpdateLinodeDisk = ( - diskId: number, - data: LinodeDiskUpdateFields -) => UpdateLinodeDiskResponse; - -export type UpdateLinode = (data: Partial) => Promise; - -export interface LinodeDetailContext { - linode: ExtendedLinode; - - /** Linode Actions */ - updateLinode: (data: Partial) => Promise; - - /** Linode Config actions */ - createLinodeConfig: CreateLinodeConfig; - deleteLinodeConfig: DeleteLinodeConfig; - getLinodeConfig: GetLinodeConfig; - getLinodeConfigs: GetLinodeConfigs; - updateLinodeConfig: UpdateLinodeConfig; - - /** Linode Disk actions */ - createLinodeDisk: CreateLinodeDisk; - deleteLinodeDisk: DeleteLinodeDisk; - getLinodeDisk: GetLinodeDisk; - getLinodeDisks: GetLinodeDisks; - updateLinodeDisk: UpdateLinodeDisk; - resizeLinodeDisk: ResizeLinodeDisk; -} - -/** - * Create the Linode Detail Context including handlers pre-configured with the - * required Linode ID. - */ -export const linodeDetailContextFactory = ( - linode: ExtendedLinode, - dispatch: ThunkDispatch -): LinodeDetailContext => { - const { id: linodeId } = linode; - - return { - updateLinode: (data: Partial) => - dispatch(_updateLinode({ linodeId, ...data })), - - /** Linode Config actions */ - getLinodeConfig: (configId) => - dispatch(_getLinodeConfig({ linodeId, configId })), - getLinodeConfigs: () => - dispatch(_getLinodeConfigs({ linodeId })).then(({ data }) => data), - updateLinodeConfig: (configId, data) => - dispatch(_updateLinodeConfig({ linodeId, configId, ...data })), - createLinodeConfig: (data) => - dispatch(_createLinodeConfig({ linodeId, ...data })), - deleteLinodeConfig: (configId) => - dispatch(_deleteLinodeConfig({ linodeId, configId })), - - /** Linode Disk actions */ - getLinodeDisk: (diskId) => dispatch(_getLinodeDisk({ linodeId, diskId })), - getLinodeDisks: () => dispatch(_getLinodeDisks({ linodeId })), - updateLinodeDisk: (diskId, data) => - dispatch(_updateLinodeDisk({ linodeId, diskId, ...data })), - createLinodeDisk: (data) => - dispatch(_createLinodeDisk({ linodeId, ...data })), - deleteLinodeDisk: (diskId) => - dispatch(_deleteLinodeDisk({ linodeId, diskId })), - resizeLinodeDisk: (diskId, size) => - dispatch(_resizeLinodeDisk({ linodeId, diskId, size })), - linode, - }; -}; - -const linodeContext = React.createContext(null as any); - -export default linodeContext; - -export const LinodeDetailContextProvider = linodeContext.Provider; - -export const LinodeDetailContextConsumer = linodeContext.Consumer; - -export const withLinodeDetailContext = createHOCForConsumer( - linodeContext.Consumer, - 'withLinodeDetailContext' -); diff --git a/packages/manager/src/features/Linodes/index.tsx b/packages/manager/src/features/Linodes/index.tsx index 85bdc7b16b6..6a7769e0cb3 100644 --- a/packages/manager/src/features/Linodes/index.tsx +++ b/packages/manager/src/features/Linodes/index.tsx @@ -8,7 +8,7 @@ import { addMaintenanceToLinodes } from 'src/store/linodes/linodes.helpers'; const LinodesLanding = React.lazy( () => import('./LinodesLanding/LinodesLanding') ); -const LinodesDetail = React.lazy(() => import('./LinodesDetail')); +const LinodesDetail = React.lazy(() => import('./LinodesDetail/LinodesDetail')); const LinodesCreate = React.lazy( () => import('./LinodesCreate/LinodeCreateContainer') ); diff --git a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx index 0aed6f1563c..04a9750024f 100644 --- a/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx +++ b/packages/manager/src/features/NodeBalancers/ConfigNodeIPSelect.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import LinodeSelect from 'src/features/Linodes/LinodeSelect'; +import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { Linode } from '@linode/api-v4/lib/linodes'; import { privateIPRegex } from 'src/utilities/ipUtils'; import type { Props as TextFieldProps } from 'src/components/TextField'; diff --git a/packages/manager/src/features/NotificationCenter/NotificationData/RenderProgressEvent.tsx b/packages/manager/src/features/NotificationCenter/NotificationData/RenderProgressEvent.tsx index 00ea13f3bd6..dfccfeb5868 100644 --- a/packages/manager/src/features/NotificationCenter/NotificationData/RenderProgressEvent.tsx +++ b/packages/manager/src/features/NotificationCenter/NotificationData/RenderProgressEvent.tsx @@ -3,7 +3,6 @@ import BarPercent from 'src/components/BarPercent'; import Box from 'src/components/core/Box'; import Divider from 'src/components/core/Divider'; import Typography from 'src/components/core/Typography'; -import useLinodes from 'src/hooks/useLinodes'; import { Duration } from 'luxon'; import { Event } from '@linode/api-v4/lib/account/types'; import { extendTypesQueryResult } from 'src/utilities/extendType'; @@ -18,24 +17,22 @@ import { RenderEventStyledBox, useRenderEventStyles, } from './RenderEvent.styles'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; interface Props { event: Event; onClose: () => void; } -export type CombinedProps = Props; - -export const RenderProgressEvent: React.FC = (props) => { +export const RenderProgressEvent = (props: Props) => { const { classes } = useRenderEventStyles(); const { event } = props; - const { linodes } = useLinodes(); - const _linodes = Object.values(linodes.itemsById); + const { data: linodes } = useAllLinodesQuery(); const typesQuery = useSpecificTypes( - _linodes.map((linode) => linode.type).filter(isNotNullOrUndefined) + (linodes ?? []).map((linode) => linode.type).filter(isNotNullOrUndefined) ); const types = extendTypesQueryResult(typesQuery); - const message = eventMessageGenerator(event, _linodes, types); + const message = eventMessageGenerator(event, linodes, types); if (message === null) { return null; diff --git a/packages/manager/src/features/Search/SearchLanding.test.tsx b/packages/manager/src/features/Search/SearchLanding.test.tsx index 8eefb2dc1d0..d220189f017 100644 --- a/packages/manager/src/features/Search/SearchLanding.test.tsx +++ b/packages/manager/src/features/Search/SearchLanding.test.tsx @@ -33,10 +33,6 @@ const propsWithResults: Props = { const queryClient = new QueryClient(); -jest.mock('src/hooks/useReduxLoad', () => ({ - useReduxLoad: () => jest.fn().mockReturnValue({ _loading: false }), -})); - describe('Component', () => { server.use( rest.get('*/domains', (req, res, ctx) => { diff --git a/packages/manager/src/features/Search/SearchLanding.tsx b/packages/manager/src/features/Search/SearchLanding.tsx index 2d25824a120..a78dccae174 100644 --- a/packages/manager/src/features/Search/SearchLanding.tsx +++ b/packages/manager/src/features/Search/SearchLanding.tsx @@ -1,6 +1,5 @@ import { equals } from 'ramda'; import * as React from 'react'; -import { useSelector } from 'react-redux'; import { RouteComponentProps } from 'react-router-dom'; import { compose } from 'recompose'; import Error from 'src/assets/icons/error.svg'; @@ -11,10 +10,8 @@ import Typography from 'src/components/core/Typography'; import Grid from '@mui/material/Unstable_Grid2'; import { H1Header } from 'src/components/H1Header/H1Header'; import { Notice } from 'src/components/Notice/Notice'; -import { REFRESH_INTERVAL } from 'src/constants'; import useAPISearch from 'src/features/Search/useAPISearch'; import useAccountManagement from 'src/hooks/useAccountManagement'; -import { useReduxLoad } from 'src/hooks/useReduxLoad'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllImagesQuery } from 'src/queries/images'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; @@ -25,7 +22,6 @@ import { import { useSpecificTypes } from 'src/queries/types'; import { useRegionsQuery } from 'src/queries/regions'; import { useAllVolumesQuery } from 'src/queries/volumes'; -import { ApplicationState } from 'src/store'; import { ErrorObject } from 'src/store/selectors/entitiesErrors'; import { formatLinode } from 'src/store/selectors/getSearchEntities'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -40,6 +36,7 @@ import { extendTypesQueryResult } from 'src/utilities/extendType'; import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; import { getImageLabelForLinode } from '../Images/utils'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; const useStyles = makeStyles((theme: Theme) => ({ root: { @@ -152,18 +149,20 @@ export const SearchLanding: React.FC = (props) => { !_isLargeAccount ); - const { data: regions } = useRegionsQuery(); + const { + data: linodes, + isLoading: areLinodesLoading, + error: linodesError, + } = useAllLinodesQuery({}, {}, !_isLargeAccount); - const linodes = useSelector((state: ApplicationState) => - Object.values(state.__resources.linodes.itemsById) - ); + const { data: regions } = useRegionsQuery(); const typesQuery = useSpecificTypes( - linodes.map((linode) => linode.type).filter(isNotNullOrUndefined) + (linodes ?? []).map((linode) => linode.type).filter(isNotNullOrUndefined) ); const types = extendTypesQueryResult(typesQuery); - const searchableLinodes = linodes.map((linode) => { + const searchableLinodes = (linodes ?? []).map((linode) => { const imageLabel = getImageLabelForLinode(linode, publicImages ?? []); return formatLinode(linode, types, imageLabel); }); @@ -180,12 +179,6 @@ export const SearchLanding: React.FC = (props) => { queryError = true; } - const { _loading: reduxLoading } = useReduxLoad( - ['linodes'], - REFRESH_INTERVAL, - !_isLargeAccount - ); - const { searchAPI } = useAPISearch(!isNilOrEmpty(query)); const _searchAPI = React.useRef( @@ -236,11 +229,12 @@ export const SearchLanding: React.FC = (props) => { _privateImages, regions, nodebalancers, + searchableLinodes, ]); const getErrorMessage = (errors: ErrorObject): string => { const errorString: string[] = []; - if (errors.linodes) { + if (linodesError) { errorString.push('Linodes'); } if (domainsError) { @@ -277,7 +271,7 @@ export const SearchLanding: React.FC = (props) => { const resultsEmpty = equals(finalResults, emptyResults); const loading = - reduxLoading || + areLinodesLoading || areBucketsLoading || areClustersLoading || areDomainsLoading || diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx index d7a9fa6f8d5..cadb5f98934 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketDrawer.tsx @@ -20,7 +20,6 @@ import { Notice } from 'src/components/Notice/Notice'; import SectionErrorBoundary from 'src/components/SectionErrorBoundary'; import { EntityForTicketDetails } from 'src/components/SupportLink/SupportLink'; import TextField from 'src/components/TextField'; -import useEntities, { Entity } from 'src/hooks/useEntities'; import { useAccount } from 'src/queries/account'; import { useAllDatabasesQuery } from 'src/queries/databases'; import { useAllDomainsQuery } from 'src/queries/domains'; @@ -46,6 +45,7 @@ import SupportTicketSMTPFields, { smtpHelperText, } from './SupportTicketSMTPFields'; import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; const useStyles = makeStyles((theme: Theme) => ({ expPanelSummary: { @@ -144,7 +144,7 @@ const entityMap: Record = { Firewalls: 'firewall_id', }; -const entityIdToNameMap: Partial> = { +const entityIdToNameMap: Record = { linode_id: 'Linode', volume_id: 'Volume', domain_id: 'Domain', @@ -152,18 +152,8 @@ const entityIdToNameMap: Partial> = { lkecluster_id: 'Kubernetes Cluster', database_id: 'Database Cluster', firewall_id: 'Firewall', -}; - -const entityIdToTypeMap: Record = { - linode_id: 'linodes', - volume_id: 'volumes', - domain_id: 'domains', - nodebalancer_id: 'nodeBalancers', - lkecluster_id: 'kubernetesClusters', - database_id: 'databases', - firewall_id: 'firewalls', - none: 'linodes', - general: 'linodes', + none: '', + general: '', }; export const entitiesToItems = (type: string, entities: any) => { @@ -221,55 +211,60 @@ export const SupportTicketDrawer: React.FC = (props) => { publicInfo: '', }); - // Entities for populating dropdown - const [data, setData] = React.useState[]>([]); - const [entitiesLoading, setLoading] = React.useState(false); - const [files, setFiles] = React.useState([]); const [errors, setErrors] = React.useState(); const [submitting, setSubmitting] = React.useState(false); - const entities = useEntities(); - const classes = useStyles(); React.useEffect(() => { if (!open) { resetDrawer(); } - if (prefilledEntity) { - loadSelectedEntities(prefilledEntity.type); - } }, [open]); // React Query entities - const { data: databases, isLoading: databasesLoading } = useAllDatabasesQuery( - entityType === 'database_id' - ); + const { + data: databases, + isLoading: databasesLoading, + error: databasesError, + } = useAllDatabasesQuery(entityType === 'database_id'); - const { data: firewalls, isLoading: firewallsLoading } = useAllFirewallsQuery( - entityType === 'firewall_id' - ); + const { + data: firewalls, + isLoading: firewallsLoading, + error: firewallsError, + } = useAllFirewallsQuery(entityType === 'firewall_id'); - const { data: domains, isLoading: domainsLoading } = useAllDomainsQuery( - entityType === 'domain_id' - ); + const { + data: domains, + isLoading: domainsLoading, + error: domainsError, + } = useAllDomainsQuery(entityType === 'domain_id'); const { data: nodebalancers, isLoading: nodebalancersLoading, + error: nodebalancersError, } = useAllNodeBalancersQuery(entityType === 'nodebalancer_id'); const { data: clusters, isLoading: clustersLoading, + error: clustersError, } = useAllKubernetesClustersQuery(entityType === 'lkecluster_id'); - const { data: volumes, isLoading: volumesLoading } = useAllVolumesQuery( - {}, - {}, - entityType === 'volume_id' - ); + const { + data: linodes, + isLoading: linodesLoading, + error: linodesError, + } = useAllLinodesQuery({}, {}, entityType === 'linode_id'); + + const { + data: volumes, + isLoading: volumesLoading, + error: volumesError, + } = useAllVolumesQuery({}, {}, entityType === 'volume_id'); const saveText = (_title: string, _description: string) => { storage.supportText.set({ title: _title, description: _description }); @@ -283,56 +278,6 @@ export const SupportTicketDrawer: React.FC = (props) => { debouncedSave(summary, description); }, [summary, description]); - const handleSetOrRequestEntities = ( - _entity: Entity, - _entityType: string - ) => { - if (_entity.lastUpdated === 0) { - setLoading(true); - setErrors(undefined); - _entity - .request() - .then((response) => { - setLoading(false); - setData(entitiesToItems(_entityType, response)); - }) - .catch((_) => setLoading(false)); // Errors through Redux - } else { - setData(entitiesToItems(_entityType, _entity.data)); - } - }; - - /** - * When a new entity type is selected, - * 1. check to see if we have data for that type. - * 2. If we don't, request it and assign the result to the selectedEntities state - * 3. If we do, directly assign the data from Redux to the selectedEntities state - * - * NOTE: Using a switch here rather than the entities[entityIdToTypeMap] logic - * used for error handling below; it's more explicit and safer. - */ - const loadSelectedEntities = (_entityType: EntityType) => { - switch (_entityType) { - case 'linode_id': { - handleSetOrRequestEntities(entities.linodes, _entityType); - return; - } - case 'nodebalancer_id': - case 'lkecluster_id': - case 'volume_id': - case 'firewall_id': - case 'domain_id': - case 'database_id': { - // intentionally do nothing for React Query entities - return; - } - default: { - setData([]); - return; - } - } - }; - const resetTicket = (clearValues: boolean = false) => { /** * Clear the drawer completely if clearValues is passed (as in when closing the drawer) @@ -352,7 +297,6 @@ export const SupportTicketDrawer: React.FC = (props) => { }; const resetDrawer = (clearValues: boolean = false) => { - setData([]); resetTicket(clearValues); setFiles([]); @@ -377,10 +321,6 @@ export const SupportTicketDrawer: React.FC = (props) => { } setEntityType(e.value as EntityType); setEntityID(''); - setErrors(undefined); - setData([]); - - loadSelectedEntities(e.value as EntityType); }; const handleEntityIDChange = (selected: Item | null) => { @@ -551,10 +491,6 @@ export const SupportTicketDrawer: React.FC = (props) => { const generalError = hasErrorFor.none; const inputError = hasErrorFor.input; - const entityError = Boolean(entities[entityIdToTypeMap[entityType]]?.error) - ? `Error loading ${entityIdToNameMap[entityType]}s` - : undefined; - const topicOptions = [ { label: 'General/Account/Billing', value: 'general' }, ...renderEntityTypes(), @@ -572,11 +508,11 @@ export const SupportTicketDrawer: React.FC = (props) => { volume_id: volumes, lkecluster_id: clusters, nodebalancer_id: nodebalancers, + linode_id: linodes, }; if (!reactQueryEntityDataMap[entityType]) { - // We are dealing with an entity found in Redux. Return the data from state. - return data; + return []; } // domain's don't have a label so we map the domain as the label @@ -599,30 +535,35 @@ export const SupportTicketDrawer: React.FC = (props) => { ); }; - const getAreEntitiesLoading = () => { - if (entityType === 'database_id') { - return databasesLoading; - } - if (entityType === 'firewall_id') { - return firewallsLoading; - } - if (entityType === 'domain_id') { - return domainsLoading; - } - if (entityType === 'volume_id') { - return volumesLoading; - } - if (entityType === 'lkecluster_id') { - return clustersLoading; - } - if (entityType === 'nodebalancer_id') { - return nodebalancersLoading; - } - return entitiesLoading; + const loadingMap: Record = { + database_id: databasesLoading, + firewall_id: firewallsLoading, + domain_id: domainsLoading, + volume_id: volumesLoading, + lkecluster_id: clustersLoading, + nodebalancer_id: nodebalancersLoading, + linode_id: linodesLoading, + none: false, + general: false, + }; + + const errorMap: Record = { + database_id: databasesError, + firewall_id: firewallsError, + domain_id: domainsError, + volume_id: volumesError, + lkecluster_id: clustersError, + nodebalancer_id: nodebalancersError, + linode_id: linodesError, + none: null, + general: null, }; const entityOptions = getEntityOptions(); - const areEntitiesLoading = getAreEntitiesLoading(); + const areEntitiesLoading = loadingMap[entityType]; + const entityError = Boolean(errorMap[entityType]) + ? `Error loading ${entityIdToNameMap[entityType]}s` + : undefined; const selectedEntity = entityOptions.find((thisEntity) => String(thisEntity.value) === entityID) || diff --git a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx index 6bf40cb58c1..1a547d71fe1 100644 --- a/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx +++ b/packages/manager/src/features/TopMenu/SearchBar/SearchBar.tsx @@ -2,18 +2,16 @@ import Close from '@mui/icons-material/Close'; import Search from '@mui/icons-material/Search'; import { take } from 'ramda'; import * as React from 'react'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; import { components } from 'react-select'; import { compose } from 'recompose'; import { IconButton } from 'src/components/IconButton'; import EnhancedSelect, { Item } from 'src/components/EnhancedSelect/Select'; -import { REFRESH_INTERVAL } from 'src/constants'; import useAPISearch from 'src/features/Search/useAPISearch'; import withStoreSearch, { SearchProps, } from 'src/features/Search/withStoreSearch'; import useAccountManagement from 'src/hooks/useAccountManagement'; -import { ReduxEntity, useReduxLoad } from 'src/hooks/useReduxLoad'; import { useAllDomainsQuery } from 'src/queries/domains'; import { useAllImagesQuery } from 'src/queries/images'; import { @@ -26,8 +24,6 @@ import { isNilOrEmpty } from 'src/utilities/isNilOrEmpty'; import { debounce } from 'throttle-debounce'; import styled, { StyleProps } from './SearchBar.styles'; import SearchSuggestion from './SearchSuggestion'; -import { useSelector } from 'react-redux'; -import { ApplicationState } from 'src/store'; import { formatLinode } from 'src/store/selectors/getSearchEntities'; import { useAllKubernetesClustersQuery } from 'src/queries/kubernetes'; import { useSpecificTypes } from 'src/queries/types'; @@ -36,8 +32,9 @@ import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; import { useRegionsQuery } from 'src/queries/regions'; import { useAllNodeBalancersQuery } from 'src/queries/nodebalancers'; import { getImageLabelForLinode } from 'src/features/Images/utils'; +import { useAllLinodesQuery } from 'src/queries/linodes/linodes'; -type CombinedProps = SearchProps & StyleProps & RouteComponentProps; +type CombinedProps = SearchProps & StyleProps; const Control = (props: any) => ; @@ -77,8 +74,6 @@ export const selectStyles = { menu: (base: any) => ({ ...base, maxWidth: '100% !important' }), }; -const searchDeps: ReduxEntity[] = ['linodes']; - export const SearchBar = (props: CombinedProps) => { const { classes, combinedResults, entitiesLoading, search } = props; @@ -90,6 +85,8 @@ export const SearchBar = (props: CombinedProps) => { const [apiError, setAPIError] = React.useState(null); const [apiSearchLoading, setAPILoading] = React.useState(false); + const history = useHistory(); + const { _isLargeAccount } = useAccountManagement(); // Only request things if the search bar is open/active. @@ -124,24 +121,22 @@ export const SearchBar = (props: CombinedProps) => { searchActive ); - const { data: regions } = useRegionsQuery(); - - const { _loading } = useReduxLoad( - searchDeps, - REFRESH_INTERVAL, + const { data: linodes, isLoading: linodesLoading } = useAllLinodesQuery( + {}, + {}, shouldMakeRequests ); - const linodes = useSelector((state: ApplicationState) => - Object.values(state.__resources.linodes.itemsById) - ); + const { data: regions } = useRegionsQuery(); + const typesQuery = useSpecificTypes( - linodes.map((linode) => linode.type).filter(isNotNullOrUndefined), + (linodes ?? []).map((linode) => linode.type).filter(isNotNullOrUndefined), shouldMakeRequests ); + const types = extendTypesQueryResult(typesQuery); - const searchableLinodes = linodes.map((linode) => { + const searchableLinodes = (linodes ?? []).map((linode) => { const imageLabel = getImageLabelForLinode(linode, publicImages ?? []); return formatLinode(linode, types, imageLabel); }); @@ -188,7 +183,6 @@ export const SearchBar = (props: CombinedProps) => { ); } }, [ - _loading, imagesLoading, search, searchText, @@ -235,13 +229,13 @@ export const SearchBar = (props: CombinedProps) => { const text = item?.data?.searchText ?? ''; if (item.value === 'redirect') { - props.history.push({ + history.push({ pathname: `/search`, search: `?query=${encodeURIComponent(text)}`, }); return; } - props.history.push(item.data.path); + history.push(item.data.path); }; const onKeyDown = (e: any) => { @@ -250,7 +244,7 @@ export const SearchBar = (props: CombinedProps) => { searchText !== '' && (!combinedResults || combinedResults.length < 1) ) { - props.history.push({ + history.push({ pathname: `/search`, search: `?query=${encodeURIComponent(searchText)}`, }); @@ -282,7 +276,7 @@ export const SearchBar = (props: CombinedProps) => { const finalOptions = createFinalOptions( _isLargeAccount ? apiResults : combinedResults, searchText, - _loading || imagesLoading || apiSearchLoading, + apiSearchLoading || linodesLoading || imagesLoading, // Ignore "Unauthorized" errors, since these will always happen on LKE // endpoints for restricted users. It's not really an "error" in this case. // We still want these users to be able to use the search feature. @@ -351,7 +345,6 @@ export const SearchBar = (props: CombinedProps) => { }; export default compose( - withRouter, withStoreSearch(), styled )(SearchBar) as React.ComponentType<{}>; diff --git a/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx b/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx index 6a31a4028cd..f66e7e5c374 100644 --- a/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx +++ b/packages/manager/src/features/Volumes/DestructiveVolumeDialog.tsx @@ -6,12 +6,12 @@ import Typography from 'src/components/core/Typography'; import { Notice } from 'src/components/Notice/Notice'; import { TypeToConfirmDialog } from 'src/components/TypeToConfirmDialog/TypeToConfirmDialog'; import { resetEventsPolling } from 'src/eventsPolling'; -import useLinodes from 'src/hooks/useLinodes'; import { useDeleteVolumeMutation, useDetachVolumeMutation, } from 'src/queries/volumes'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; +import { useLinodeQuery } from 'src/queries/linodes/linodes'; const useStyles = makeStyles((theme: Theme) => ({ warningCopy: { @@ -36,10 +36,11 @@ export const DestructiveVolumeDialog = (props: Props) => { const { volumeLabel: label, volumeId, linodeId, mode, open, onClose } = props; const { enqueueSnackbar } = useSnackbar(); - const linodes = useLinodes(); - const linode = - linodeId !== undefined ? linodes.linodes.itemsById[linodeId] : undefined; + const { data: linode } = useLinodeQuery( + linodeId ?? -1, + linodeId !== undefined + ); const { mutateAsync: detachVolume, diff --git a/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx b/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx index 65f8b7a2aac..455e632748e 100644 --- a/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx +++ b/packages/manager/src/features/Volumes/VolumeAttachmentDrawer.tsx @@ -9,7 +9,7 @@ import Drawer from 'src/components/Drawer'; import Select, { Item } from 'src/components/EnhancedSelect'; import { Notice } from 'src/components/Notice/Notice'; import { resetEventsPolling } from 'src/eventsPolling'; -import LinodeSelect from 'src/features/Linodes/LinodeSelect'; +import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { useAllLinodeConfigsQuery } from 'src/queries/linodes/linodes'; import { useGrants, useProfile } from 'src/queries/profile'; import { useAttachVolumeMutation } from 'src/queries/volumes'; diff --git a/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx b/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx index 41f1fbfe77d..9ecccd8e203 100644 --- a/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx +++ b/packages/manager/src/features/Volumes/VolumeCreate/CreateVolumeForm.tsx @@ -18,7 +18,7 @@ import { TooltipIcon } from 'src/components/TooltipIcon/TooltipIcon'; import { Notice } from 'src/components/Notice/Notice'; import { MAX_VOLUME_SIZE } from 'src/constants'; import EUAgreementCheckbox from 'src/features/Account/Agreements/EUAgreementCheckbox'; -import LinodeSelect from 'src/features/Linodes/LinodeSelect'; +import { LinodeSelect } from 'src/features/Linodes/LinodeSelect/LinodeSelect'; import { reportAgreementSigningError, useAccountAgreements, diff --git a/packages/manager/src/hooks/useEntities.ts b/packages/manager/src/hooks/useEntities.ts deleted file mode 100644 index 1fcc5088887..00000000000 --- a/packages/manager/src/hooks/useEntities.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { APIError } from '@linode/api-v4/lib/types'; -import useLinodeActions from './useLinodeActions'; -import useLinodes from './useLinodes'; - -export interface Entity { - data: T[]; - request: () => Promise; - lastUpdated: number; - error?: APIError[]; -} - -/** - * Returns data for each entity type in array format, - * along with the request thunk for each of the entity types. - * - * @example - * - * const { linodes, volumes } = useEntities(); - * linodes.map(thisLinode => thisLinode.label); - * if (linodes.lastUpdated === 0) { linodes.request(); } - */ -export const useEntities = () => { - const { linodes: _linodes } = useLinodes(); - const { requestLinodes } = useLinodeActions(); - - /** Our Redux store is currently inconsistent about - * the data shape for different entity types. - * The purpose of this meta-container is to expose - * a single, consistent interface so that consumers - * can map through different entity types without - * worrying about whether they should use data.entities - * or Object.value(data.itemsById). - */ - - const linodes = Object.values(_linodes.itemsById); - - return { - linodes: { - data: linodes, - request: requestLinodes, - lastUpdated: _linodes.lastUpdated, - error: _linodes.error?.read, - }, - }; -}; - -export default useEntities; diff --git a/packages/manager/src/hooks/useExtendedLinode.ts b/packages/manager/src/hooks/useExtendedLinode.ts deleted file mode 100644 index 6642ece5025..00000000000 --- a/packages/manager/src/hooks/useExtendedLinode.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Event, GrantLevel } from '@linode/api-v4/lib/account'; -import { Config, Disk } from '@linode/api-v4/lib/linodes'; -import { useSelector } from 'react-redux'; -import { useGrants } from 'src/queries/profile'; -import { useSpecificTypes } from 'src/queries/types'; -import { ApplicationState } from 'src/store'; -import { eventsForLinode } from 'src/store/events/event.selectors'; -import { getLinodeConfigsForLinode } from 'src/store/linodes/config/config.selectors'; -import { getLinodeDisksForLinode } from 'src/store/linodes/disk/disk.selectors'; -import { LinodeWithMaintenance } from 'src/store/linodes/linodes.helpers'; -import { getPermissionsForLinode } from 'src/store/linodes/permissions/permissions.selector'; -import { ExtendedType, extendTypesQueryResult } from 'src/utilities/extendType'; -import { isNotNullOrUndefined } from 'src/utilities/nullOrUndefined'; -import useLinodes from './useLinodes'; - -export interface ExtendedLinode extends LinodeWithMaintenance { - _configs: Config[]; - _disks: Disk[]; - _events: Event[]; - _type?: null | ExtendedType; - _permissions: GrantLevel; -} - -export const useExtendedLinodes = (linodeIds?: number[]): ExtendedLinode[] => { - const { data: grants } = useGrants(); - const { linodes: linodesMap } = useLinodes(); - const linodes = (linodeIds ?? Object.keys(linodesMap.itemsById)) - .map((linodeId) => linodesMap.itemsById[linodeId]) - .filter(isNotNullOrUndefined); - const typesQuery = useSpecificTypes( - linodes.map((linode) => linode.type).filter(isNotNullOrUndefined) - ); - const types = extendTypesQueryResult(typesQuery); - - const typeMap = new Map( - types.map((type) => [type.id, type]) - ); - - return useSelector((state: ApplicationState) => { - const { events, __resources } = state; - const { linodeConfigs, linodeDisks } = __resources; - - return linodes.map((linode) => ({ - ...linode, - _type: linode.type ? typeMap.get(linode.type) : null, - _events: eventsForLinode(events, linode.id), - _configs: getLinodeConfigsForLinode(linodeConfigs, linode.id), - _disks: getLinodeDisksForLinode(linodeDisks, linode.id), - _permissions: getPermissionsForLinode(grants ?? null, linode.id), - })); - }); -}; - -export const useExtendedLinode = (linodeId: number): ExtendedLinode | null => - useExtendedLinodes([linodeId])[0] ?? null; - -export default useExtendedLinode; diff --git a/packages/manager/src/hooks/useLinodeActions.ts b/packages/manager/src/hooks/useLinodeActions.ts deleted file mode 100644 index 222bb2024d5..00000000000 --- a/packages/manager/src/hooks/useLinodeActions.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Linode } from '@linode/api-v4/lib/linodes/types'; -import { useQueryClient } from 'react-query'; -import { useDispatch } from 'react-redux'; -import { - deleteLinode as _deleteLinode, - getLinode as _getLinode, - requestLinodes as _requestLinodes, - updateLinode as _updateLinode, -} from 'src/store/linodes/linode.requests'; -import { UpdateLinodeParams } from 'src/store/linodes/linodes.actions'; -import { Dispatch } from './types'; - -export interface LinodesProps { - requestLinodes: () => Promise; - getLinode: (linodeId: number) => Promise; - deleteLinode: (linodeId: number) => Promise<{}>; - updateLinode: (params: UpdateLinodeParams) => Promise; -} - -export const useLinodeActions = (): LinodesProps => { - const dispatch: Dispatch = useDispatch(); - const queryClient = useQueryClient(); - - const requestLinodes = () => - dispatch(_requestLinodes({})).then((response) => response.data); - - const getLinode = (linodeId: number) => dispatch(_getLinode({ linodeId })); - - const deleteLinode = (linodeId: number) => - dispatch(_deleteLinode({ linodeId, queryClient })); - - const updateLinode = (params: UpdateLinodeParams) => - dispatch(_updateLinode(params)); - - return { - requestLinodes, - getLinode, - deleteLinode, - updateLinode, - }; -}; - -export default useLinodeActions; diff --git a/packages/manager/src/hooks/useLinodes.ts b/packages/manager/src/hooks/useLinodes.ts deleted file mode 100644 index d9deb4c9732..00000000000 --- a/packages/manager/src/hooks/useLinodes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useSelector } from 'react-redux'; -import { ApplicationState } from 'src/store'; -import { State } from 'src/store/linodes/linodes.reducer'; - -export interface LinodesProps { - linodes: State; -} - -export const useLinodes = (): LinodesProps => { - const linodes = useSelector( - (state: ApplicationState) => state.__resources.linodes - ); - - return { - linodes, - }; -}; - -export default useLinodes; diff --git a/packages/manager/src/hooks/useReduxLoad.test.ts b/packages/manager/src/hooks/useReduxLoad.test.ts deleted file mode 100644 index 43e52ff7586..00000000000 --- a/packages/manager/src/hooks/useReduxLoad.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { hasError } from './useReduxLoad'; - -const errorMessage = 'An error occurred.'; - -describe('hasError utility function', () => { - it('EntityError: returns `true` only if a "read" error is present', () => { - expect(hasError({ read: errorMessage })).toBe(true); - expect(hasError({})).toBe(false); - }); - - it('APIError[]: returns `true` if an error is present', () => { - expect(hasError([{ reason: errorMessage }])).toBe(true); - expect(hasError([])).toBe(false); - }); - - it('handles any input you throw at it', () => { - expect(hasError(undefined)).toBe(false); - expect(hasError(null)).toBe(false); - expect(hasError(1234)).toBe(false); - expect(hasError('1234')).toBe(false); - expect(hasError(true)).toBe(false); - expect(hasError(false)).toBe(false); - expect(hasError([])).toBe(false); - }); -}); diff --git a/packages/manager/src/hooks/useReduxLoad.ts b/packages/manager/src/hooks/useReduxLoad.ts deleted file mode 100644 index dfd49f4613b..00000000000 --- a/packages/manager/src/hooks/useReduxLoad.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import { usePageVisibility } from 'react-page-visibility'; -import { useDispatch } from 'react-redux'; -import { Dispatch } from 'redux'; -import { REFRESH_INTERVAL } from 'src/constants'; -import { ApplicationState, useApplicationStore } from 'src/store'; -import { getEvents } from 'src/store/events/event.request'; -import { requestLinodes } from 'src/store/linodes/linode.requests'; -import { getAllLongviewClients } from 'src/store/longview/longview.requests'; - -interface UseReduxPreload { - _loading: boolean; -} - -export type ReduxEntity = 'linodes' | 'events' | 'longview'; - -type RequestMap = Record; - -const requestMap: RequestMap = { - linodes: () => requestLinodes({}), - events: getEvents, - longview: getAllLongviewClients, -}; - -export const useReduxLoad = ( - deps: ReduxEntity[] = [], - refreshInterval: number = REFRESH_INTERVAL, - predicate: boolean = true -): UseReduxPreload => { - const [_loading, setLoading] = useState(false); - const dispatch = useDispatch(); - const store = useApplicationStore(); - const isVisible = usePageVisibility(); - /** - * Restricted users get a 403 from /lke/clusters, - * which gums up the works. We want to prevent that particular - * request for a restricted user. - */ - const mountedRef = useRef(true); - - const _setLoading = (val: boolean) => { - if (mountedRef.current) { - setLoading(val); - } - }; - - useEffect(() => { - if (isVisible && predicate && mountedRef.current) { - requestDeps( - store.getState(), - dispatch, - deps, - refreshInterval, - _setLoading - ); - } - }, [predicate, refreshInterval, deps, dispatch, store, isVisible]); - - useEffect(() => { - return () => { - mountedRef.current = false; - }; - }, []); - - return { _loading }; -}; - -export const requestDeps = ( - state: ApplicationState, - dispatch: Dispatch, - deps: ReduxEntity[], - refreshInterval: number = 60000, - loadingCb: (l: boolean) => void = (_) => null -) => { - let i = 0; - let needsToLoad = false; - const requests = []; - for (i; i < deps.length; i++) { - const currentResource = state.__resources[deps[i]] || state[deps[i]]; - - if (currentResource) { - const currentResourceHasError = hasError(currentResource?.error); - if ( - currentResource.lastUpdated === 0 && - !currentResource.loading && - !currentResourceHasError - ) { - needsToLoad = true; - requests.push(requestMap[deps[i]]); - } else if ( - Date.now() - currentResource.lastUpdated > refreshInterval && - !currentResource.loading && - !currentResourceHasError - ) { - requests.push(requestMap[deps[i]]); - } - } - } - - if (requests.length === 0) { - return; - } - - if (needsToLoad) { - loadingCb(true); - } - - return Promise.all(requests.map((thisRequest) => dispatch(thisRequest()))) - .then((_) => loadingCb(false)) - .catch((_) => loadingCb(false)); -}; - -export default useReduxLoad; - -export const hasError = (resourceError: any) => { - if (Array.isArray(resourceError) && resourceError.length > 0) { - return true; - } - - return resourceError?.read !== undefined; -}; diff --git a/packages/manager/src/queries/linodes/linodes.ts b/packages/manager/src/queries/linodes/linodes.ts index 5e42a8c90b0..84d5d454b29 100644 --- a/packages/manager/src/queries/linodes/linodes.ts +++ b/packages/manager/src/queries/linodes/linodes.ts @@ -32,6 +32,8 @@ import { getLinodeKernel, resizeLinode, ResizeLinodePayload, + createLinode, + CreateLinodeRequest, } from '@linode/api-v4/lib/linodes'; import { queryKey as accountQueryKey } from '../account'; @@ -167,6 +169,15 @@ export const useDeleteLinodeMutation = (id: number) => { }); }; +export const useCreateLinodeMutation = () => { + const queryClient = useQueryClient(); + return useMutation(createLinode, { + onSuccess() { + queryClient.invalidateQueries([queryKey]); + }, + }); +}; + export const useBootLinodeMutation = (id: number) => { const queryClient = useQueryClient(); return useMutation<{}, APIError[], { config_id?: number }>( diff --git a/packages/manager/src/store/linodes/linode.containers.ts b/packages/manager/src/store/linodes/linode.containers.ts deleted file mode 100644 index 0cd4f0705d5..00000000000 --- a/packages/manager/src/store/linodes/linode.containers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Linode } from '@linode/api-v4/lib/linodes'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { - createLinode, - deleteLinode, - rebootLinode, - updateLinode, -} from './linode.requests'; -import { - CreateLinodeParams, - DeleteLinodeParams, - RebootLinodeParams, - UpdateLinodeParams, -} from './linodes.actions'; - -export interface Actions { - createLinode: (params: CreateLinodeParams) => Promise; - deleteLinode: (params: DeleteLinodeParams) => Promise; - updateLinode: (params: UpdateLinodeParams) => Promise; - rebootLinode: (params: RebootLinodeParams) => Promise; -} - -export interface LinodeActionsProps { - linodeActions: Actions; -} - -export const withLinodeActions = connect(undefined, (dispatch) => ({ - linodeActions: bindActionCreators( - { createLinode, deleteLinode, updateLinode, rebootLinode }, - dispatch - ), -})); diff --git a/packages/manager/src/utilities/testHelpersStore.ts b/packages/manager/src/utilities/testHelpersStore.ts deleted file mode 100644 index f5c47c1e07e..00000000000 --- a/packages/manager/src/utilities/testHelpersStore.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const withLinodesLoaded = { - __resources: { linodes: { loading: false, lastUpdated: 1 } }, -}; diff --git a/packages/validation/CHANGELOG.md b/packages/validation/CHANGELOG.md index f6d06f5a338..d76d23c7074 100644 --- a/packages/validation/CHANGELOG.md +++ b/packages/validation/CHANGELOG.md @@ -1,3 +1,8 @@ +## [2023-06-29] - v0.24.1 + +### Changed: +- Firewall custom ports validation ([#9336](https://github.com/linode/manager/pull/9336)) + ## [2023-06-12] - v0.24.0 ### Changed: diff --git a/packages/validation/package.json b/packages/validation/package.json index b76fc86ff12..49592dc710e 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@linode/validation", - "version": "0.24.0", + "version": "0.24.1", "description": "Yup validation schemas for use with the Linode APIv4", "type": "module", "main": "lib/index.cjs", From 46025b0315718148a1f857c81f73e2cae88e0761 Mon Sep 17 00:00:00 2001 From: Jaalah Ramos Date: Thu, 29 Jun 2023 16:00:38 -0400 Subject: [PATCH 12/12] Update validation package to even version 0.25.0 --- packages/validation/CHANGELOG.md | 4 ++-- packages/validation/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/validation/CHANGELOG.md b/packages/validation/CHANGELOG.md index d76d23c7074..ff310bb7f49 100644 --- a/packages/validation/CHANGELOG.md +++ b/packages/validation/CHANGELOG.md @@ -1,6 +1,6 @@ -## [2023-06-29] - v0.24.1 +## [2023-06-29] - v0.25.0 -### Changed: +### Fixed: - Firewall custom ports validation ([#9336](https://github.com/linode/manager/pull/9336)) ## [2023-06-12] - v0.24.0 diff --git a/packages/validation/package.json b/packages/validation/package.json index 49592dc710e..a27b8acb573 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@linode/validation", - "version": "0.24.1", + "version": "0.25.0", "description": "Yup validation schemas for use with the Linode APIv4", "type": "module", "main": "lib/index.cjs",