From 1754b9a149f83a8485b10776f68c0dd2d946373a Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 10:39:57 -0400 Subject: [PATCH 01/19] save changes #1 --- .../manager/src/utilities/pricing/dynamicPricing.ts | 13 ++++++++----- packages/manager/src/utilities/regions.ts | 8 ++++---- packages/manager/src/utilities/storage.ts | 2 +- packages/manager/src/utilities/subnets.ts | 6 ++++-- packages/manager/src/utilities/theme.ts | 2 +- packages/manager/src/utilities/unitConversions.ts | 6 +++--- packages/manager/tsconfig.json | 4 ---- 7 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.ts b/packages/manager/src/utilities/pricing/dynamicPricing.ts index b6e17da0b60..85fe244ed59 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.ts @@ -70,13 +70,16 @@ export const getDCSpecificPrice = ({ return undefined; } - const increaseFactor = priceIncreaseMap[regionId] as number | undefined; + if (regionId in priceIncreaseMap) { + const increaseFactor = + priceIncreaseMap[regionId as keyof typeof priceIncreaseMap]; - if (increaseFactor !== undefined) { - // If increaseFactor is defined, it means the region has a price increase and we should apply it. - const increase = basePrice * increaseFactor; + if (increaseFactor !== undefined) { + // If increaseFactor is defined, it means the region has a price increase and we should apply it. + const increase = basePrice * increaseFactor; - return (basePrice + increase).toFixed(2); + return (basePrice + increase).toFixed(2); + } } return basePrice.toFixed(2); diff --git a/packages/manager/src/utilities/regions.ts b/packages/manager/src/utilities/regions.ts index 91f7f62ac3b..b49ac8aeabf 100644 --- a/packages/manager/src/utilities/regions.ts +++ b/packages/manager/src/utilities/regions.ts @@ -1,4 +1,4 @@ -import { Region } from '@linode/api-v4/lib/regions'; +import type { Region } from '@linode/api-v4'; /** * This utility function takes an array of regions data and transforms it into a lookup object. @@ -7,11 +7,11 @@ import { Region } from '@linode/api-v4/lib/regions'; * * @returns {Object} A lookup object where each key is a region ID and its value is the corresponding region object. */ -export const getRegionsByRegionId = (regionsData: Region[] | undefined) => { - if (!Array.isArray(regionsData)) { +export const getRegionsByRegionId = (regions: Region[] | undefined) => { + if (!regions) { return {}; } - return regionsData.reduce((lookup, region) => { + return regions.reduce>((lookup, region) => { lookup[region.id] = region; return lookup; }, {}); diff --git a/packages/manager/src/utilities/storage.ts b/packages/manager/src/utilities/storage.ts index 6d1450f3b2b..1dbf77ccc6b 100644 --- a/packages/manager/src/utilities/storage.ts +++ b/packages/manager/src/utilities/storage.ts @@ -3,7 +3,7 @@ import { shouldEnableDevTools } from 'src/dev-tools/load'; import type { StackScriptPayload } from '@linode/api-v4/lib/stackscripts/types'; import type { SupportTicketFormFields } from 'src/features/Support/SupportTickets/SupportTicketDialog'; -const localStorageCache = {}; +const localStorageCache: Record = {}; export const getStorage = (key: string, fallback?: any) => { if (localStorageCache[key]) { diff --git a/packages/manager/src/utilities/subnets.ts b/packages/manager/src/utilities/subnets.ts index 9f65527a397..31564f72f8f 100644 --- a/packages/manager/src/utilities/subnets.ts +++ b/packages/manager/src/utilities/subnets.ts @@ -35,7 +35,7 @@ export type SubnetIPType = 'ipv4' | 'ipv6'; * - To get available IPs for our VPCs, subtract 4 (the number of reserved IPs) * from the given number */ -export const SubnetMaskToAvailIPv4s = { +export const SubnetMaskToAvailIPv4s: Record = { 0: 4294967296, 1: 2147483648, 2: 1073741824, @@ -107,7 +107,9 @@ export const calculateAvailableIPv4sRFC1918 = ( const [, mask] = address.split('/'); // if the IP is not in the RFC1918 ranges, hold off on displaying number of available IPs - return isValidRFC1918IPv4(address) ? SubnetMaskToAvailIPv4s[mask] : undefined; + return isValidRFC1918IPv4(address) + ? SubnetMaskToAvailIPv4s[Number(mask)] + : undefined; }; /** diff --git a/packages/manager/src/utilities/theme.ts b/packages/manager/src/utilities/theme.ts index 67f66422337..9c2257c6ddb 100644 --- a/packages/manager/src/utilities/theme.ts +++ b/packages/manager/src/utilities/theme.ts @@ -30,7 +30,7 @@ export const getNextThemeValue = (currentTheme: string | undefined) => { * Use this to validate if a value in a user's preferences is a valid value */ export const isValidTheme = (value: unknown): boolean => { - return typeof value === 'string' && themes[value] !== undefined; + return typeof value === 'string' && value in themes; }; /** diff --git a/packages/manager/src/utilities/unitConversions.ts b/packages/manager/src/utilities/unitConversions.ts index 72e950d0f2a..13514e622e1 100644 --- a/packages/manager/src/utilities/unitConversions.ts +++ b/packages/manager/src/utilities/unitConversions.ts @@ -60,12 +60,12 @@ export const readableBytes = ( // If we've been given custom unit labels, make the substitution here. if (options.unitLabels) { - Object.keys(options.unitLabels).forEach((originalLabel) => { - const idx = storageUnits.indexOf(originalLabel as StorageSymbol); + Object.keys(options.unitLabels).forEach((originalLabel: StorageSymbol) => { + const idx = storageUnits.indexOf(originalLabel); if (idx > -1) { // The TS compiler wasn't aware of the null check above, so I added // the non-null assertion operator on options.unitLabels. - storageUnits[idx] = options.unitLabels![originalLabel]; + storageUnits[idx] = options.unitLabels![originalLabel] as StorageSymbol; } }); } diff --git a/packages/manager/tsconfig.json b/packages/manager/tsconfig.json index a1ef543aa04..25ce8001a09 100644 --- a/packages/manager/tsconfig.json +++ b/packages/manager/tsconfig.json @@ -35,10 +35,6 @@ "strictNullChecks": true, "types": ["vitest/globals", "@testing-library/jest-dom"], - /* Goodluck... */ - "ignoreDeprecations": "5.0", - "suppressImplicitAnyIndexErrors": true, - /* Completeness */ "skipLibCheck": true, From 07db437149cd78942ab5a8b166eb0a948f2b3584 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 13:00:07 -0400 Subject: [PATCH 02/19] save changes #2 --- .../VPCs/VPCLanding/VPCEditDrawer.tsx | 2 +- packages/manager/src/hooks/useCreateVPC.ts | 8 ++-- .../src/hooks/useDismissibleNotifications.ts | 5 ++- packages/manager/src/hooks/useStackScript.ts | 2 +- packages/manager/src/initSentry.ts | 2 +- packages/manager/src/mocks/serverHandlers.ts | 44 ++++++++++++------- .../manager/src/queries/account/agreements.ts | 6 ++- packages/manager/src/queries/base.ts | 26 ++++++++--- .../src/queries/events/event.helpers.ts | 2 + .../manager/src/store/image/image.helpers.ts | 6 +-- .../src/store/longview/longview.reducer.ts | 11 +++-- .../manager/src/utilities/analytics/utils.ts | 6 ++- .../utilities/codesnippets/generate-cURL.ts | 4 +- packages/manager/src/utilities/deepMerge.ts | 12 ++--- packages/manager/src/utilities/errorUtils.ts | 2 +- .../manager/src/utilities/formikErrorUtils.ts | 2 +- 16 files changed, 87 insertions(+), 53 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx index 104da2923f8..73935355e6d 100644 --- a/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCLanding/VPCEditDrawer.tsx @@ -63,7 +63,7 @@ export const VPCEditDrawer = (props: Props) => { const handleFieldChange = (field: string, value: string) => { form.setFieldValue(field, value); - if (form.errors[field]) { + if (form.errors[field as keyof UpdateVPCPayloadWithNone]) { form.setFieldError(field, undefined); } }; diff --git a/packages/manager/src/hooks/useCreateVPC.ts b/packages/manager/src/hooks/useCreateVPC.ts index 2ba217f9d8a..480e77224ad 100644 --- a/packages/manager/src/hooks/useCreateVPC.ts +++ b/packages/manager/src/hooks/useCreateVPC.ts @@ -71,7 +71,7 @@ export const useCreateVPC = (inputs: UseCreateVPCInputs) => { // on the UI and still have any errors returned by the API correspond to the correct subnet const createSubnetsPayloadAndMapping = () => { const subnetsPayload: CreateSubnetPayload[] = []; - const subnetIdxMapping = {}; + const subnetIdxMapping: Record = {}; let apiSubnetIdx = 0; for (let i = 0; i < formik.values.subnets.length; i++) { @@ -93,8 +93,8 @@ export const useCreateVPC = (inputs: UseCreateVPCInputs) => { }; const combineErrorsAndSubnets = ( - errors: {}, - visualToAPISubnetMapping: {} + errors: Record, + visualToAPISubnetMapping: Record ) => { return formik.values.subnets.map((subnet, idx) => { const apiSubnetIdx: number | undefined = visualToAPISubnetMapping[idx]; @@ -206,7 +206,7 @@ export const useCreateVPC = (inputs: UseCreateVPCInputs) => { // Helper method to set a field's value and clear existing errors const onChangeField = (field: string, value: string) => { formik.setFieldValue(field, value); - if (formik.errors[field]) { + if (formik.errors[field as keyof CreateVPCFieldState]) { formik.setFieldError(field, undefined); } }; diff --git a/packages/manager/src/hooks/useDismissibleNotifications.ts b/packages/manager/src/hooks/useDismissibleNotifications.ts index 56cfc021664..bdd30f8da52 100644 --- a/packages/manager/src/hooks/useDismissibleNotifications.ts +++ b/packages/manager/src/hooks/useDismissibleNotifications.ts @@ -6,7 +6,8 @@ import { useMutatePreferences, usePreferences, } from 'src/queries/profile/preferences'; -import { DismissedNotification } from 'src/types/ManagerPreferences'; + +import type { DismissedNotification } from 'src/types/ManagerPreferences'; /** * Handlers for dismissing notifications and checking if a notification has been dismissed. @@ -111,7 +112,7 @@ export const updateDismissedNotifications = ( notificationsToDismiss: unknown[], options: DismissibleNotificationOptions ) => { - const newNotifications = {}; + const newNotifications: Record = {}; notificationsToDismiss.forEach((thisNotification) => { const hashKey = getHashKey(thisNotification, options.prefix); newNotifications[hashKey] = { diff --git a/packages/manager/src/hooks/useStackScript.ts b/packages/manager/src/hooks/useStackScript.ts index fd251d795b3..c7d0ed292c1 100644 --- a/packages/manager/src/hooks/useStackScript.ts +++ b/packages/manager/src/hooks/useStackScript.ts @@ -110,7 +110,7 @@ const getCompatibleImages = ( }; const getDefaultUDFData = (userDefinedFields: UserDefinedField[]) => { - const defaultUDFData = {}; + const defaultUDFData: Record = {}; userDefinedFields.forEach((eachField) => { if (!!eachField.default) { defaultUDFData[eachField.name] = eachField.default; diff --git a/packages/manager/src/initSentry.ts b/packages/manager/src/initSentry.ts index 01bc2aa8e8a..7c83183094e 100644 --- a/packages/manager/src/initSentry.ts +++ b/packages/manager/src/initSentry.ts @@ -166,7 +166,7 @@ const maybeAddCustomFingerprint = (event: SentryEvent): SentryEvent => { !!exception.values[0].value && !!exception.values[0].value.match(new RegExp(value, 'gmi')) ) { - acc = customFingerPrintMap[value]; + acc = customFingerPrintMap[value as keyof typeof customFingerPrintMap]; } return acc; }, ''); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 7e828c68df8..40560ba490c 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -95,7 +95,9 @@ import { pickRandom } from 'src/utilities/random'; import { getStorage } from 'src/utilities/storage'; import type { + AccountMaintenance, CreateObjectStorageKeyPayload, + FirewallStatus, NotificationType, SecurityQuestionsPayload, TokenRequest, @@ -789,13 +791,16 @@ export const handlers = [ const firewall = firewallFactory.build(); return HttpResponse.json(firewall); }), - http.put('*/v4beta/networking/firewalls/:firewallId', async ({ request }) => { - const body = await request.json(); - const firewall = firewallFactory.build({ - status: body?.['status'] ?? 'disabled', - }); - return HttpResponse.json(firewall); - }), + http.put<{}, { status: FirewallStatus }>( + '*/v4beta/networking/firewalls/:firewallId', + async ({ request }) => { + const body = await request.json(); + const firewall = firewallFactory.build({ + status: body?.['status'] ?? 'disabled', + }); + return HttpResponse.json(firewall); + } + ), // http.post('*/account/agreements', () => { // return res(ctx.status(500), ctx.json({ reason: 'Unknown error' })); // }), @@ -1234,8 +1239,8 @@ export const handlers = [ if (request.headers.get('x-filter')) { accountMaintenance.sort((a, b) => { - const statusA = a[headers['+order_by']]; - const statusB = b[headers['+order_by']]; + const statusA = a[headers['+order_by'] as keyof AccountMaintenance]; + const statusB = b[headers['+order_by'] as keyof AccountMaintenance]; if (statusA < statusB) { return -1; @@ -1333,8 +1338,12 @@ export const handlers = [ } filteredAccountUsers.sort((a, b) => { - const statusA = a[headers['+order_by']]; - const statusB = b[headers['+order_by']]; + const statusA = a[headers['+order_by'] as keyof User]; + const statusB = b[headers['+order_by'] as keyof User]; + + if (!statusA || !statusB) { + return 0; + } if (statusA < statusB) { return -1; @@ -1908,11 +1917,14 @@ export const handlers = [ http.post('*/networking/vlans', () => { return HttpResponse.json({}); }), - http.post('*/networking/ipv6/ranges', async ({ request }) => { - const body = await request.json(); - const range = body?.['prefix_length']; - return HttpResponse.json({ range, route_target: '2001:DB8::0000' }); - }), + http.post<{}, { prefix_length: number }>( + '*/networking/ipv6/ranges', + async ({ request }) => { + const body = await request.json(); + const range = body?.['prefix_length']; + return HttpResponse.json({ range, route_target: '2001:DB8::0000' }); + } + ), http.post('*/networking/ips/assign', () => { return HttpResponse.json({}); }), diff --git a/packages/manager/src/queries/account/agreements.ts b/packages/manager/src/queries/account/agreements.ts index 4b754a2d950..7c1e51eb613 100644 --- a/packages/manager/src/queries/account/agreements.ts +++ b/packages/manager/src/queries/account/agreements.ts @@ -38,8 +38,10 @@ export const useMutateAccountAgreements = () => { const newAgreements = { ...previousData }; for (const key in variables) { - if (variables[key] !== undefined) { - newAgreements[key] = variables[key]; + if (variables[key as keyof Agreements] !== undefined) { + newAgreements[key as keyof Agreements] = variables[ + key as keyof Agreements + ]!; } } diff --git a/packages/manager/src/queries/base.ts b/packages/manager/src/queries/base.ts index b4157095922..4446c01ff49 100644 --- a/packages/manager/src/queries/base.ts +++ b/packages/manager/src/queries/base.ts @@ -69,17 +69,21 @@ export type ItemsByID = Record; * */ -export const listToItemsByID = ( - entityList: E, +export const listToItemsByID = >( + entityList: E[], indexer: string = 'id' ) => { return entityList.reduce( - (map, item) => ({ ...map, [item[indexer]]: item }), + (map, item) => ({ ...map, [item[indexer as keyof E]]: item }), {} ); }; -export const mutationHandlers = ( +export const mutationHandlers = < + T, + V extends Record, + E = APIError[] +>( queryKey: QueryKey, indexer: string = 'id', queryClient: QueryClient @@ -89,7 +93,7 @@ export const mutationHandlers = ( // Update the query data to include the newly updated Entity. queryClient.setQueryData>(queryKey, (oldData) => ({ ...oldData, - [variables[indexer]]: updatedEntity, + [variables[indexer as keyof V]]: updatedEntity, })); }, }; @@ -109,7 +113,11 @@ export const simpleMutationHandlers = ( }; }; -export const creationHandlers = ( +export const creationHandlers = < + T extends Record, + V, + E = APIError[] +>( queryKey: QueryKey, indexer: string = 'id', queryClient: QueryClient @@ -125,7 +133,11 @@ export const creationHandlers = ( }; }; -export const deletionHandlers = ( +export const deletionHandlers = < + T, + V extends Record, + E = APIError[] +>( queryKey: QueryKey, indexer: string = 'id', queryClient: QueryClient diff --git a/packages/manager/src/queries/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts index b82a64a2b50..2fbf4ef2670 100644 --- a/packages/manager/src/queries/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -63,10 +63,12 @@ export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { return false; } + // @ts-expect-error todo fix filter type if (filter?.['entity.id'] && filter['entity.id'] !== event.entity?.id) { return false; } + // @ts-expect-error todo fix filter type if (filter?.['entity.type'] && filter['entity.type'] !== event.entity?.type) { return false; } diff --git a/packages/manager/src/store/image/image.helpers.ts b/packages/manager/src/store/image/image.helpers.ts index c1e792dcbb1..19228e7c1ea 100644 --- a/packages/manager/src/store/image/image.helpers.ts +++ b/packages/manager/src/store/image/image.helpers.ts @@ -1,10 +1,10 @@ -import { Image } from '@linode/api-v4/lib/images'; +import type { Image } from '@linode/api-v4'; export const filterImagesByType = ( images: Record, type: 'private' | 'public' -): Record => { - return Object.keys(images).reduce((acc, eachKey) => { +) => { + return Object.keys(images).reduce>((acc, eachKey) => { /** keep the public images if we're filtering by public images */ if (type === 'public' && !!images[eachKey].is_public) { acc[eachKey] = images[eachKey]; diff --git a/packages/manager/src/store/longview/longview.reducer.ts b/packages/manager/src/store/longview/longview.reducer.ts index 1d2f6ef2279..6b700bad75a 100644 --- a/packages/manager/src/store/longview/longview.reducer.ts +++ b/packages/manager/src/store/longview/longview.reducer.ts @@ -34,10 +34,13 @@ const reducer = reducerWithInitialState(defaultState) getLongviewClients.done, (state, { payload: { result } }) => ({ ...state, - data: result.data.reduce((acc, client) => { - acc[client.id] = client; - return acc; - }, {}), + data: result.data.reduce>( + (acc, client) => { + acc[client.id] = client; + return acc; + }, + {} + ), lastUpdated: Date.now(), loading: false, results: result.results, diff --git a/packages/manager/src/utilities/analytics/utils.ts b/packages/manager/src/utilities/analytics/utils.ts index bc0b994fe6b..8d3ef2a0101 100644 --- a/packages/manager/src/utilities/analytics/utils.ts +++ b/packages/manager/src/utilities/analytics/utils.ts @@ -1,6 +1,6 @@ import { ADOBE_ANALYTICS_URL } from 'src/constants'; -import { AnalyticsEvent, FormEventType, FormPayload } from './types'; +import { AnalyticsEvent, BasicFormEvent, FormErrorEvent, FormEventType, FormInputEvent, FormPayload, FormStepEvent } from './types'; /** * Sends a direct call rule events to Adobe for a Component Click (and optionally, with `data`, Component Details). @@ -34,7 +34,9 @@ export const sendFormEvent = ( eventPayload: FormPayload, eventType: FormEventType ): void => { - const formEventPayload = { + const formEventPayload: Partial< + BasicFormEvent & FormErrorEvent & FormInputEvent & FormStepEvent + > = { formName: eventPayload.formName.replace(/\|/g, ''), }; if (!ADOBE_ANALYTICS_URL) { diff --git a/packages/manager/src/utilities/codesnippets/generate-cURL.ts b/packages/manager/src/utilities/codesnippets/generate-cURL.ts index aac77e14372..fbc3e8b63d1 100644 --- a/packages/manager/src/utilities/codesnippets/generate-cURL.ts +++ b/packages/manager/src/utilities/codesnippets/generate-cURL.ts @@ -6,10 +6,10 @@ const headers = [ '-H "Authorization: Bearer $TOKEN" \\', ].join('\n'); -export const generateCurlCommand = (data: {}, path: string) => { +export const generateCurlCommand = (data: any, path: string) => { const keys = Object.keys(data); - const cleanData = {}; + const cleanData: any = {}; for (const key of keys) { if (typeof data[key] === 'string') { diff --git a/packages/manager/src/utilities/deepMerge.ts b/packages/manager/src/utilities/deepMerge.ts index 6852b68b973..b46d4336323 100644 --- a/packages/manager/src/utilities/deepMerge.ts +++ b/packages/manager/src/utilities/deepMerge.ts @@ -24,17 +24,17 @@ export const deepMerge = ( const output = { ...target } as T & S; if (isObject(target) && isObject(source)) { Object.keys(source).forEach((key) => { - if (isObject(source[key])) { + if (isObject((source as any)[key])) { if (!(key in target)) { - Object.assign(output, { [key]: source[key] }); + Object.assign(output, { [key]: (source as any)[key] }); } else { - (output[key] as unknown) = deepMerge( - target[key] as ObjectType, - source[key] as ObjectType + ((output as any)[key] as unknown) = deepMerge( + (target as any)[key] as ObjectType, + (source as any)[key] as ObjectType ); } } else { - Object.assign(output, { [key]: source[key] }); + Object.assign(output, { [key]: (source as any)[key] }); } }); } diff --git a/packages/manager/src/utilities/errorUtils.ts b/packages/manager/src/utilities/errorUtils.ts index c81f09ded23..8f2574a1c2c 100644 --- a/packages/manager/src/utilities/errorUtils.ts +++ b/packages/manager/src/utilities/errorUtils.ts @@ -105,6 +105,6 @@ export const getErrorMap = ( }; } }, - { none: undefined } + { none: undefined } as Record ) as Record<'none' | T, string | undefined>; }; diff --git a/packages/manager/src/utilities/formikErrorUtils.ts b/packages/manager/src/utilities/formikErrorUtils.ts index 90875bb5afa..3e4e14deea4 100644 --- a/packages/manager/src/utilities/formikErrorUtils.ts +++ b/packages/manager/src/utilities/formikErrorUtils.ts @@ -117,7 +117,7 @@ export const handleVPCAndSubnetErrors = ( setFieldError: (field: string, message: string) => void, setError?: (message: string) => void ) => { - const subnetErrors = {}; + const subnetErrors: Record = {}; const nonSubnetErrors: APIError[] = []; errors.forEach((error) => { From b763bddf7a10caf77133467098bc37410e5ae346 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 14:29:17 -0400 Subject: [PATCH 03/19] save changes #3 --- packages/api-v4/src/account/types.ts | 1 + .../NodeBalancers/NodeBalancerCreate.tsx | 8 +++++++- .../src/features/ObjectStorage/utilities.ts | 10 ++++++++-- .../features/OneClickApps/AppDetailDrawer.tsx | 5 +++-- .../src/features/Profile/APITokens/utils.ts | 4 ++-- .../src/features/Search/SearchLanding.tsx | 18 ++++++++++-------- .../src/features/Search/refinedSearch.ts | 6 +++++- packages/manager/src/features/Search/utils.ts | 4 +++- .../SupportTicketProductSelectionFields.tsx | 4 ++++ .../src/features/Users/UserPermissions.tsx | 17 +++++++++-------- .../src/queries/object-storage/queries.ts | 4 +++- 11 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/api-v4/src/account/types.ts b/packages/api-v4/src/account/types.ts index fafa09e4d3c..9dfddcaf735 100644 --- a/packages/api-v4/src/account/types.ts +++ b/packages/api-v4/src/account/types.ts @@ -182,6 +182,7 @@ export type GlobalGrantTypes = | 'add_images' | 'add_linodes' | 'add_longview' + | 'add_databases' | 'add_nodebalancers' | 'add_stackscripts' | 'add_volumes' diff --git a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx index c06b15f7b15..16dd6225894 100644 --- a/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx +++ b/packages/manager/src/features/NodeBalancers/NodeBalancerCreate.tsx @@ -352,7 +352,13 @@ const NodeBalancerCreate = () => { setDeleteConfigConfirmDialog(clone(defaultDeleteConfigConfirmDialogState)); }; - const onConfigValueChange = (configId: number, key: string, value: any) => { + const onConfigValueChange = < + Key extends keyof NodeBalancerConfigFieldsWithStatusAndErrors + >( + configId: number, + key: Key, + value: NodeBalancerConfigFieldsWithStatusAndErrors[Key] + ) => { setNodeBalancerFields((prev) => { const newConfigs = [...prev.configs]; newConfigs[configId][key] = value; diff --git a/packages/manager/src/features/ObjectStorage/utilities.ts b/packages/manager/src/features/ObjectStorage/utilities.ts index 7cbfde45994..39bd78bd897 100644 --- a/packages/manager/src/features/ObjectStorage/utilities.ts +++ b/packages/manager/src/features/ObjectStorage/utilities.ts @@ -138,8 +138,14 @@ export const confirmObjectStorage = async ( // on fields that have been touched (handleSubmit() does this // implicitly). Object.keys(validationErrors).forEach((key) => { - formikProps.setFieldTouched(key, validationErrors[key]); - formikProps.setFieldError(key, validationErrors[key]); + formikProps.setFieldTouched( + key, + Boolean(validationErrors[key as keyof T]) + ); + formikProps.setFieldError( + key, + validationErrors[key as keyof T] as string + ); }); } else { openConfirmationDialog(); diff --git a/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx b/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx index a9e1480e50c..fab152f1a4d 100644 --- a/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx +++ b/packages/manager/src/features/OneClickApps/AppDetailDrawer.tsx @@ -142,8 +142,9 @@ export const AppDetailDrawer: React.FunctionComponent = (props) => { > {`${selectedApp.name} { )} {!loading && ( - {Object.keys(finalResults).map((entityType, idx: number) => ( - - ))} + {Object.keys(finalResults).map( + (entityType: keyof typeof displayMap, idx: number) => ( + + ) + )} )} diff --git a/packages/manager/src/features/Search/refinedSearch.ts b/packages/manager/src/features/Search/refinedSearch.ts index f3329f9b315..6fd093981f1 100644 --- a/packages/manager/src/features/Search/refinedSearch.ts +++ b/packages/manager/src/features/Search/refinedSearch.ts @@ -221,7 +221,11 @@ export const getRealEntityKey = (key: string): SearchField | string => { title: LABEL, }; - return substitutions[key] || key; + if (key in substitutions) { + return substitutions[key as keyof typeof substitutions]; + } + + return key; }; // Returns true if all values in array are true diff --git a/packages/manager/src/features/Search/utils.ts b/packages/manager/src/features/Search/utils.ts index 9000210e92c..4b23ac68683 100644 --- a/packages/manager/src/features/Search/utils.ts +++ b/packages/manager/src/features/Search/utils.ts @@ -26,7 +26,9 @@ export const separateResultsByEntity = ( searchResults.forEach((result) => { // EntityTypes are singular; we'd like the resulting keys to be plural const pluralizedEntityType = result.entityType + 's'; - separatedResults[pluralizedEntityType].push(result); + separatedResults[ + pluralizedEntityType as keyof typeof separatedResults + ].push(result); }); return separatedResults; }; diff --git a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx index 2fd3a8b7bd0..35f0fccc41e 100644 --- a/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx +++ b/packages/manager/src/features/Support/SupportTickets/SupportTicketProductSelectionFields.tsx @@ -96,6 +96,10 @@ export const SupportTicketProductSelectionFields = (props: Props) => { volume_id: volumes, }; + if (entityType === 'none' || entityType === 'general') { + return []; + } + if (!reactQueryEntityDataMap[entityType]) { return []; } diff --git a/packages/manager/src/features/Users/UserPermissions.tsx b/packages/manager/src/features/Users/UserPermissions.tsx index 0d95678a7fc..c17347cc8d4 100644 --- a/packages/manager/src/features/Users/UserPermissions.tsx +++ b/packages/manager/src/features/Users/UserPermissions.tsx @@ -1,4 +1,5 @@ import { + GlobalGrantTypes, Grant, GrantLevel, GrantType, @@ -67,7 +68,7 @@ interface Props { interface TabInfo { showTabs: boolean; - tabs: string[]; + tabs: GrantType[]; } interface State { @@ -84,7 +85,7 @@ interface State { setAllPerm: 'null' | 'read_only' | 'read_write'; /* Large Account Support */ showTabs?: boolean; - tabs?: string[]; + tabs?: GrantType[]; userType: null | string; } @@ -144,7 +145,7 @@ class UserPermissions extends React.Component { } }; - entityIsAll = (entity: string, value: GrantLevel): boolean => { + entityIsAll = (entity: GrantType, value: GrantLevel): boolean => { const { grants } = this.state; if (!(grants && grants[entity])) { return false; @@ -261,7 +262,7 @@ class UserPermissions extends React.Component { } }; - globalBooleanPerms = [ + globalBooleanPerms: GlobalGrantTypes[] = [ 'add_databases', 'add_domains', 'add_firewalls', @@ -478,8 +479,8 @@ class UserPermissions extends React.Component { ); }; - renderGlobalPerm = (perm: string, checked: boolean) => { - const permDescriptionMap = { + renderGlobalPerm = (perm: GlobalGrantTypes, checked: boolean) => { + const permDescriptionMap: Partial> = { add_databases: 'Can add Databases to this account ($)', add_domains: 'Can add Domains using the DNS Manager', add_firewalls: 'Can add Firewalls to this account', @@ -687,7 +688,7 @@ class UserPermissions extends React.Component { ); }; - savePermsType = (type: string) => () => { + savePermsType = (type: keyof Grants) => () => { this.setState({ errors: undefined }); const { clearNewUser, currentUsername } = this.props; const { grants } = this.state; @@ -809,7 +810,7 @@ class UserPermissions extends React.Component { }); }; - setGrantTo = (entity: string, idx: number, value: GrantLevel) => () => { + setGrantTo = (entity: GrantType, idx: number, value: GrantLevel) => () => { const { grants } = this.state; if (!(grants && grants[entity])) { return; diff --git a/packages/manager/src/queries/object-storage/queries.ts b/packages/manager/src/queries/object-storage/queries.ts index edc3074a808..131401a5401 100644 --- a/packages/manager/src/queries/object-storage/queries.ts +++ b/packages/manager/src/queries/object-storage/queries.ts @@ -276,7 +276,9 @@ export const useCreateObjectUrlMutation = ( }); export const useBucketSSLQuery = (cluster: string, bucket: string) => - useQuery(objectStorageQueries.bucket(cluster, bucket)._ctx.ssl); + useQuery( + objectStorageQueries.bucket(cluster, bucket)._ctx.ssl + ); export const useBucketSSLMutation = (cluster: string, bucket: string) => { const queryClient = useQueryClient(); From d2d4c1da135117abaf7f2318d941af5fb92b6ffb Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 15:12:00 -0400 Subject: [PATCH 04/19] save changes #4 --- .../features/Domains/DomainRecordDrawer.tsx | 3 +- .../features/Events/factories/firewall.tsx | 8 ++- .../FirewallLanding/CreateFirewallDrawer.tsx | 4 ++ .../LinodeConfigs/LinodeConfigDialog.tsx | 12 +++- .../LinodeNetworking/IPSharing.tsx | 62 ++++++++++--------- .../MutateDrawer/MutateDrawer.tsx | 43 +++++++------ .../Managed/SSHAccess/EditSSHAccessDrawer.tsx | 4 +- 7 files changed, 83 insertions(+), 53 deletions(-) diff --git a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx index 4b724e5b06d..2dbf8a17bab 100644 --- a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx @@ -468,6 +468,7 @@ export class DomainRecordDrawer extends React.Component< * editable data or defaults. */ static defaultFieldsState = (props: Partial) => ({ + description: '', axfr_ips: getInitialIPs(props.axfr_ips), domain: props.domain, expire_sec: props.expire_sec ?? 0, @@ -715,7 +716,6 @@ export class DomainRecordDrawer extends React.Component< }; types = { - // }, AAAA: { fields: [ (idx: number) => ( @@ -733,6 +733,7 @@ export class DomainRecordDrawer extends React.Component< // (idx: number) => , // (idx: number) => , // ], + // }, CAA: { fields: [ (idx: number) => ( diff --git a/packages/manager/src/features/Events/factories/firewall.tsx b/packages/manager/src/features/Events/factories/firewall.tsx index 86f180fc1d9..41fcbf8a6ba 100644 --- a/packages/manager/src/features/Events/factories/firewall.tsx +++ b/packages/manager/src/features/Events/factories/firewall.tsx @@ -41,7 +41,9 @@ export const firewall: PartialEventMap<'firewall'> = { notification: (e) => { if (e.secondary_entity?.type) { const secondaryEntityName = - secondaryFirewallEntityNameMap[e.secondary_entity.type]; + secondaryFirewallEntityNameMap[ + e.secondary_entity.type as FirewallDeviceEntityType + ]; return ( <> {secondaryEntityName} {' '} @@ -62,7 +64,9 @@ export const firewall: PartialEventMap<'firewall'> = { notification: (e) => { if (e.secondary_entity?.type) { const secondaryEntityName = - secondaryFirewallEntityNameMap[e.secondary_entity.type]; + secondaryFirewallEntityNameMap[ + e.secondary_entity.type as FirewallDeviceEntityType + ]; return ( <> {secondaryEntityName} {' '} diff --git a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx index 844150fdf9a..50d38245280 100644 --- a/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx +++ b/packages/manager/src/features/Firewalls/FirewallLanding/CreateFirewallDrawer.tsx @@ -241,7 +241,9 @@ export const CreateFirewallDrawer = React.memo( const generalError = status?.generalError || + // @ts-expect-error this form intentionally breaks Formik's error type errors['rules.inbound'] || + // @ts-expect-error this form intentionally breaks Formik's error type errors['rules.outbound'] || errors.rules; @@ -346,6 +348,7 @@ export const CreateFirewallDrawer = React.memo( linodes.map((linode) => linode.id) ); }} + // @ts-expect-error this form intentionally breaks Formik's error type errorText={errors['devices.linodes']} helperText={deviceSelectGuidance} multiple @@ -364,6 +367,7 @@ export const CreateFirewallDrawer = React.memo( nodebalancers.map((nodebalancer) => nodebalancer.id) ); }} + // @ts-expect-error this form intentionally breaks Formik's error type errorText={errors['devices.nodebalancers']} helperText={deviceSelectGuidance} multiple diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx index 20f951e8dfb..941a1761340 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/LinodeConfigDialog.tsx @@ -996,11 +996,13 @@ export const LinodeConfigDialog = (props: Props) => { thisInterface.ip_ranges ?? [] ).map((ip_range, index) => { // Display a more user-friendly error to the user as opposed to, for example, "interfaces[1].ip_ranges[1] is invalid" + // @ts-expect-error this form intentionally breaks formik's error type const errorString: string = formik.errors[ `interfaces[${idx}].ip_ranges[${index}]` ]?.includes('is invalid') ? 'Invalid IP range' - : formik.errors[`interfaces[${idx}].ip_ranges[${index}]`]; + : // @ts-expect-error this form intentionally breaks formik's error type + formik.errors[`interfaces[${idx}].ip_ranges[${index}]`]; return { address: ip_range, @@ -1019,18 +1021,26 @@ export const LinodeConfigDialog = (props: Props) => { { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx index 674c5fc885b..736ef2ba0f3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPSharing.tsx @@ -105,27 +105,30 @@ const IPSharingPanel = (props: Props) => { linodeID: number, linodes: Linode[] ): Record => { - const choiceLabels = linodes.reduce((previousValue, currentValue) => { - // Filter out the current Linode - if (currentValue.id === linodeID) { - return previousValue; - } + const choiceLabels = linodes.reduce>( + (previousValue, currentValue) => { + // Filter out the current Linode + if (currentValue.id === linodeID) { + return previousValue; + } - currentValue.ipv4.forEach((ip) => { - previousValue[ip] = currentValue.label; - }); + currentValue.ipv4.forEach((ip) => { + previousValue[ip] = currentValue.label; + }); - if (flags.ipv6Sharing) { - availableRangesMap?.[currentValue.id]?.forEach((range: string) => { - previousValue[range] = currentValue.label; - updateIPToLinodeID({ - [range]: [...(ipToLinodeID?.[range] ?? []), currentValue.id], + if (flags.ipv6Sharing) { + availableRangesMap?.[currentValue.id]?.forEach((range: string) => { + previousValue[range] = currentValue.label; + updateIPToLinodeID({ + [range]: [...(ipToLinodeID?.[range] ?? []), currentValue.id], + }); }); - }); - } + } - return previousValue; - }, {}); + return previousValue; + }, + {} + ); linodeSharedIPs.forEach((range) => { if (!choiceLabels.hasOwnProperty(range)) { @@ -136,7 +139,7 @@ const IPSharingPanel = (props: Props) => { return choiceLabels; }; - let ipToLinodeID = {}; + let ipToLinodeID: Record = {}; const updateIPToLinodeID = (newData: Record) => { ipToLinodeID = { ...ipToLinodeID, ...newData }; @@ -185,7 +188,7 @@ const IPSharingPanel = (props: Props) => { }; const onSubmit = () => { - const groupedUnsharedRanges = {}; + const groupedUnsharedRanges: Record = {}; const finalIPs: string[] = uniq( ipsToShare.reduce((previousValue, currentValue) => { if (currentValue === undefined || currentValue === null) { @@ -407,15 +410,18 @@ const IPSharingPanel = (props: Props) => { const formatAvailableRanges = ( availableRanges: IPRangeInformation[] ): AvailableRangesMap => { - return availableRanges.reduce((previousValue, currentValue) => { - // use the first entry in linodes as we're only dealing with ranges unassociated with this - // Linode, so we just use whatever the first Linode is to later get the label for this range - previousValue[currentValue.linodes[0]] = [ - ...(previousValue?.[currentValue.linodes[0]] ?? []), - `${currentValue.range}/${currentValue.prefix}`, - ]; - return previousValue; - }, {}); + return availableRanges.reduce>( + (previousValue, currentValue) => { + // use the first entry in linodes as we're only dealing with ranges unassociated with this + // Linode, so we just use whatever the first Linode is to later get the label for this range + previousValue[currentValue.linodes[0]] = [ + ...(previousValue?.[currentValue.linodes[0]] ?? []), + `${currentValue.range}/${currentValue.prefix}`, + ]; + return previousValue; + }, + {} + ); }; interface WrapperProps { diff --git a/packages/manager/src/features/Linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx index b06aec5608a..dc4b56a8d77 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/MutateDrawer/MutateDrawer.tsx @@ -19,6 +19,7 @@ interface Spec { currentAmount: number; label: string; newAmount: null | number; + unit: string; } interface ExtendedUpgradeInfo { @@ -114,28 +115,30 @@ export class MutateDrawer extends React.Component { ) : (
    - {Object.keys(extendedUpgradeInfo).map((newSpec) => { - const { - currentAmount, - label, - newAmount, - unit, - } = extendedUpgradeInfo[newSpec]; + {Object.keys(extendedUpgradeInfo).map( + (newSpec: keyof typeof extendedUpgradeInfo) => { + const { + currentAmount, + label, + newAmount, + unit, + } = extendedUpgradeInfo[newSpec]; - if (newAmount === null) { - return null; + if (newAmount === null) { + return null; + } + return ( + + + {label} goes from {currentAmount} {unit} to{' '} + + {newAmount} {unit} + + + + ); } - return ( - - - {label} goes from {currentAmount} {unit} to{' '} - - {newAmount} {unit} - - - - ); - })} + )}
)} diff --git a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx index fba16f19195..1684f27b21e 100644 --- a/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx +++ b/packages/manager/src/features/Managed/SSHAccess/EditSSHAccessDrawer.tsx @@ -108,12 +108,14 @@ const EditSSHAccessDrawer = (props: EditSSHAccessDrawerProps) => { }) => { const { access, ip, port, user } = values.ssh; + // @ts-expect-error This form intentionally breaks Formik's error type const userError = errors['ssh.user']; // API oddity: IP errors come back as {field: 'ip'} instead of {field: 'ssh.ip'} liked we'd expect. - // tslint:disable-next-line + // @ts-expect-error This form intentionally breaks Formik's error type const ipError = errors['ssh.ip'] || errors['ip']; + // @ts-expect-error This form intentionally breaks Formik's error type const portError = errors['ssh.port']; return ( From 15f13e9a9a4570f854a8aaeae6d8d5e1c653b5bb Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 15:52:13 -0400 Subject: [PATCH 05/19] save changes #5 --- .../DatabaseCreate/DatabaseCreate.tsx | 3 +- .../FirewallDetail/Rules/FirewallRuleForm.tsx | 3 +- .../manager/src/features/Firewalls/shared.ts | 5 +-- .../Linodes/CloneLanding/utilities.ts | 10 +++--- .../UserDefinedFieldInput.tsx | 3 ++ .../UserDefinedFields/utilities.ts | 2 +- .../Linodes/LinodeCreatev2/resolvers.ts | 8 +++-- .../Linodes/LinodeCreatev2/utilities.ts | 4 +-- .../LinodesDetail/LinodeConfigs/ConfigRow.tsx | 34 +++++++++++++------ .../LinodeStorage/LinodeDiskRow.tsx | 4 +-- .../features/components/PlansPanel/utils.ts | 16 +++++---- packages/manager/src/queries/base.ts | 6 ++-- 12 files changed, 63 insertions(+), 35 deletions(-) diff --git a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx index 35127bcd302..db6e037c3dd 100644 --- a/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx +++ b/packages/manager/src/features/Databases/DatabaseCreate/DatabaseCreate.tsx @@ -137,6 +137,7 @@ const engineIcons = { mongodb: , mysql: , postgresql: , + redis: null, }; const getEngineOptions = (engines: DatabaseEngine[]) => { @@ -406,7 +407,7 @@ const DatabaseCreate = () => { return; } - const engineType = values.engine.split('/')[0]; + const engineType = values.engine.split('/')[0] as Engine; setNodePricing({ multi: type.engines[engineType].find( diff --git a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx index f55a8b0fcca..a0beae974dc 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleForm.tsx @@ -12,6 +12,7 @@ import { TextField } from 'src/components/TextField'; import { Typography } from 'src/components/Typography'; import { FirewallOptionItem, + FirewallPreset, addressOptions, firewallOptionItemsShort, portPresets, @@ -83,7 +84,7 @@ export const FirewallRuleForm = React.memo((props: FirewallRuleFormProps) => { // These handlers are all memoized because the form was laggy when I tried them inline. const handleTypeChange = React.useCallback( - (item: FirewallOptionItem | null) => { + (item: FirewallOptionItem | null) => { const selectedType = item?.value; // If the user re-selects the same preset or selectedType is undefined, don't do anything diff --git a/packages/manager/src/features/Firewalls/shared.ts b/packages/manager/src/features/Firewalls/shared.ts index 5f372cd4a61..4fd4fc6c04d 100644 --- a/packages/manager/src/features/Firewalls/shared.ts +++ b/packages/manager/src/features/Firewalls/shared.ts @@ -5,6 +5,7 @@ import type { FirewallRuleProtocol, FirewallRuleType, } from '@linode/api-v4/lib/firewalls/types'; +import { PORT_PRESETS } from './FirewallDetail/Rules/shared'; export type FirewallPreset = 'dns' | 'http' | 'https' | 'mysql' | 'ssh'; @@ -58,7 +59,7 @@ export const firewallOptionItemsShort = [ label: 'DNS', value: 'dns', }, -]; +] as const; export const protocolOptions: FirewallOptionItem[] = [ { label: 'TCP', value: 'TCP' }, @@ -74,7 +75,7 @@ export const addressOptions = [ { label: 'IP / Netmask', value: 'ip/netmask' }, ]; -export const portPresets: Record = { +export const portPresets: Record = { dns: '53', http: '80', https: '443', diff --git a/packages/manager/src/features/Linodes/CloneLanding/utilities.ts b/packages/manager/src/features/Linodes/CloneLanding/utilities.ts index 7578050b46d..93c527223b4 100644 --- a/packages/manager/src/features/Linodes/CloneLanding/utilities.ts +++ b/packages/manager/src/features/Linodes/CloneLanding/utilities.ts @@ -1,8 +1,9 @@ -import { Config, Disk } from '@linode/api-v4/lib/linodes'; +import { Config, Devices, Disk } from '@linode/api-v4/lib/linodes'; import { APIError } from '@linode/api-v4/lib/types'; import produce from 'immer'; import { DateTime } from 'luxon'; import { append, compose, flatten, keys, map, pickBy, uniqBy } from 'ramda'; +import { isDiskDevice } from '../LinodesDetail/LinodeConfigs/ConfigRow'; /** * TYPES @@ -242,9 +243,10 @@ export const getAssociatedDisks = ( const disksOnConfig: number[] = []; // Go through the devices and grab all the disks - Object.keys(config.devices).forEach((key) => { - if (config.devices[key] && config.devices[key].disk_id) { - disksOnConfig.push(config.devices[key].disk_id); + Object.keys(config.devices).forEach((key: keyof Devices) => { + const device = config.devices[key]; + if (device && isDiskDevice(device) && device.disk_id) { + disksOnConfig.push(device.disk_id); } }); diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/UserDefinedFieldInput.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/UserDefinedFieldInput.tsx index 1dc9dc84a1b..d83f982b156 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/UserDefinedFieldInput.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/UserDefinedFieldInput.tsx @@ -38,10 +38,13 @@ export const UserDefinedFieldInput = ({ userDefinedField }: Props) => { name: `stackscript_data.${userDefinedField.name}`, }); + // @ts-expect-error UDFs don't abide by the form's error type. const error = formState.errors?.[userDefinedField.name]?.message?.replace( 'the UDF', '' ); + // We might be able to fix this by checking the message for "UDF" and fixing the key + // when we put the error message in the react hook form state. if (getIsUDFHeader(userDefinedField)) { return ( diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/utilities.ts index 869c91867dc..db0dc6b01e7 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/StackScripts/UserDefinedFields/utilities.ts @@ -31,7 +31,7 @@ export const getIsUDFRequired = (udf: UserDefinedField) => export const getDefaultUDFData = ( userDefinedFields: UserDefinedField[] ): Record => - userDefinedFields.reduce((accum, field) => { + userDefinedFields.reduce>((accum, field) => { if (field.default) { accum[field.name] = field.default; } else { diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/resolvers.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/resolvers.ts index 70666df27a5..5f1f9e34082 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/resolvers.ts +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/resolvers.ts @@ -15,7 +15,7 @@ import { getLinodeCreatePayload } from './utilities'; import type { LinodeCreateType } from '../LinodesCreate/types'; import type { LinodeCreateFormValues } from './utilities'; import type { QueryClient } from '@tanstack/react-query'; -import type { Resolver } from 'react-hook-form'; +import type { FieldErrors, Resolver } from 'react-hook-form'; export const getLinodeCreateResolver = ( tab: LinodeCreateType | undefined, @@ -38,7 +38,7 @@ export const getLinodeCreateResolver = ( )(transformedValues, context, options); if (tab === 'Clone Linode' && !values.linode) { - errors['linode'] = { + (errors as FieldErrors)['linode'] = { message: 'You must select a Linode to clone from.', type: 'validate', }; @@ -60,7 +60,9 @@ export const getLinodeCreateResolver = ( const hasSignedEUAgreement = agreements.eu_model; if (!hasSignedEUAgreement && !values.hasSignedEUAgreement) { - errors['hasSignedEUAgreement'] = { + (errors as FieldErrors)[ + 'hasSignedEUAgreement' + ] = { message: 'You must agree to the EU agreement to deploy to this region.', type: 'validate', diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts index 0c5d7a879b5..df1af1f046d 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/utilities.ts @@ -65,10 +65,10 @@ export const useLinodeCreateQueryParams = () => { const newParams = new URLSearchParams(rawParams); for (const key in params) { - if (!params[key]) { + if (!params[key as keyof LinodeCreateQueryParams]) { newParams.delete(key); } else { - newParams.set(key, params[key]); + newParams.set(key, params[key as keyof LinodeCreateQueryParams]!); } } diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx index 6310baecb29..445fa48bf57 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeConfigs/ConfigRow.tsx @@ -1,4 +1,9 @@ -import { Config } from '@linode/api-v4/lib/linodes'; +import { + Config, + Devices, + DiskDevice, + VolumeDevice, +} from '@linode/api-v4/lib/linodes'; import { styled } from '@mui/material/styles'; import * as React from 'react'; @@ -21,6 +26,18 @@ interface Props { readOnly: boolean; } +export const isDiskDevice = ( + device: VolumeDevice | DiskDevice +): device is DiskDevice => { + return 'disk_id' in device; +}; + +const isVolumeDevice = ( + device: VolumeDevice | DiskDevice +): device is VolumeDevice => { + return 'volume_id' in device; +}; + export const ConfigRow = React.memo((props: Props) => { const { config, linodeId, onBoot, onDelete, onEdit, readOnly } = props; @@ -39,20 +56,17 @@ export const ConfigRow = React.memo((props: Props) => { const validDevices = React.useMemo( () => Object.keys(config.devices) - .map((thisDevice) => { + .map((thisDevice: keyof Devices) => { const device = config.devices[thisDevice]; let label: null | string = null; - if (device?.disk_id) { + if (device && isDiskDevice(device)) { label = - disks?.find( - (thisDisk) => - thisDisk.id === config.devices[thisDevice]?.disk_id - )?.label ?? `disk-${device.disk_id}`; - } else if (device?.volume_id) { + disks?.find((thisDisk) => thisDisk.id === device.disk_id) + ?.label ?? `disk-${device.disk_id}`; + } else if (device && isVolumeDevice(device)) { label = volumes?.data.find( - (thisVolume) => - thisVolume.id === config.devices[thisDevice]?.volume_id + (thisVolume) => thisVolume.id === device.volume_id )?.label ?? `volume-${device.volume_id}`; } diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx index b93f6090e31..55111a064ed 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/LinodeDiskRow.tsx @@ -10,7 +10,7 @@ import { useInProgressEvents } from 'src/queries/events/events'; import { LinodeDiskActionMenu } from './LinodeDiskActionMenu'; -import type { Disk, Linode } from '@linode/api-v4'; +import type { Disk, EventAction, Linode } from '@linode/api-v4'; interface Props { disk: Disk; @@ -35,7 +35,7 @@ export const LinodeDiskRow = React.memo((props: Props) => { readOnly, } = props; - const diskEventLabelMap = { + const diskEventLabelMap: Partial> = { disk_create: 'Creating', disk_delete: 'Deleting', disk_resize: 'Resizing', diff --git a/packages/manager/src/features/components/PlansPanel/utils.ts b/packages/manager/src/features/components/PlansPanel/utils.ts index 5c1081377ad..e79a52c9cfe 100644 --- a/packages/manager/src/features/components/PlansPanel/utils.ts +++ b/packages/manager/src/features/components/PlansPanel/utils.ts @@ -80,12 +80,16 @@ export const getPlanSelectionsByPlanType = < } // filter empty plan group - return Object.keys(plansByType).reduce((acc, key) => { - if (plansByType[key].length > 0) { - acc[key] = plansByType[key]; - } - return acc; - }, {} as PlansByType); + return Object.keys(plansByType).reduce>>( + (acc, key) => { + if (plansByType[key as keyof PlansByType].length > 0) { + acc[key as keyof PlansByType] = + plansByType[key as keyof PlansByType]; + } + return acc; + }, + {} as PlansByType + ); }; export const determineInitialPlanCategoryTab = ( diff --git a/packages/manager/src/queries/base.ts b/packages/manager/src/queries/base.ts index 4446c01ff49..b1728a0a770 100644 --- a/packages/manager/src/queries/base.ts +++ b/packages/manager/src/queries/base.ts @@ -69,12 +69,12 @@ export type ItemsByID = Record; * */ -export const listToItemsByID = >( +export const listToItemsByID = ( entityList: E[], indexer: string = 'id' ) => { - return entityList.reduce( - (map, item) => ({ ...map, [item[indexer as keyof E]]: item }), + return entityList.reduce>( + (map, item) => ({ ...map, [item[indexer]]: item }), {} ); }; From a8139dd9d718cd4d13bbaf42d3046eba07dbcef8 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 17:00:19 -0400 Subject: [PATCH 06/19] save changes #6 --- packages/manager/src/components/Button/Button.tsx | 2 +- packages/manager/src/components/Flag.tsx | 15 ++++++++++----- .../src/components/ImageSelect/ImageSelect.tsx | 2 +- .../InlineMenuAction/InlineMenuAction.tsx | 3 --- packages/manager/src/components/OSIcon.tsx | 4 +++- .../PaymentMethodRow/ThirdPartyPayment.tsx | 9 ++++++--- .../RegionSelect/RegionSelect.utils.tsx | 4 +++- .../manager/src/components/Uploaders/reducer.ts | 8 +++++--- .../DatabaseDetail/DatabaseStatusDisplay.tsx | 2 +- .../Rules/FirewallRuleDrawer.utils.ts | 5 ++--- .../Linodes/LinodesCreate/LinodeCreate.tsx | 5 +++-- .../TabbedContent/FromStackScriptContent.tsx | 15 +++++++++------ .../LinodesDetail/LinodeNetworking/IPTransfer.tsx | 2 +- .../src/features/Longview/shared/utilities.ts | 7 +++++-- .../components/PlansPanel/PlanInformation.tsx | 5 ++++- packages/manager/src/queries/profile/profile.ts | 2 +- 16 files changed, 55 insertions(+), 35 deletions(-) diff --git a/packages/manager/src/components/Button/Button.tsx b/packages/manager/src/components/Button/Button.tsx index 3758cbe554b..79ebcc92dcc 100644 --- a/packages/manager/src/components/Button/Button.tsx +++ b/packages/manager/src/components/Button/Button.tsx @@ -151,7 +151,7 @@ export const Button = React.forwardRef( color={color} compactX={compactX} compactY={compactY} - data-testid={rest['data-testid'] || 'Button'} + data-testid="Button" disableRipple={disabled} disabled={loading} loading={loading} diff --git a/packages/manager/src/components/Flag.tsx b/packages/manager/src/components/Flag.tsx index 76feab1da78..84decd01815 100644 --- a/packages/manager/src/components/Flag.tsx +++ b/packages/manager/src/components/Flag.tsx @@ -18,11 +18,16 @@ interface Props { export const Flag = (props: Props) => { const country = props.country.toLowerCase(); - return ( - - ); + return ; +}; + +const getFlagClass = (country: Country | string) => { + if (country in COUNTRY_FLAG_OVERRIDES) { + return COUNTRY_FLAG_OVERRIDES[ + country as keyof typeof COUNTRY_FLAG_OVERRIDES + ]; + } + return country; }; const StyledFlag = styled('div', { label: 'StyledFlag' })(({ theme }) => ({ diff --git a/packages/manager/src/components/ImageSelect/ImageSelect.tsx b/packages/manager/src/components/ImageSelect/ImageSelect.tsx index 5756a5f5092..92e43254322 100644 --- a/packages/manager/src/components/ImageSelect/ImageSelect.tsx +++ b/packages/manager/src/components/ImageSelect/ImageSelect.tsx @@ -117,7 +117,7 @@ export const imagesToGroupedItems = (images: Image[]) => { acc.push({ className: vendor ? // Use Tux as a fallback. - `fl-${OS_ICONS[vendor] ?? 'tux'}` + `fl-${OS_ICONS[vendor as keyof typeof OS_ICONS] ?? 'tux'}` : `fl-tux`, created, isCloudInitCompatible: capabilities?.includes('cloud-init'), diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index dba7a9ae442..c8e0b884fe4 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -37,7 +37,6 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { onClick, tooltip, tooltipAnalyticsEvent, - ...rest } = props; if (href) { @@ -51,7 +50,6 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { return ( { sx={buttonHeight !== undefined ? { height: buttonHeight } : {}} tooltipAnalyticsEvent={tooltipAnalyticsEvent} tooltipText={tooltip} - {...rest} > {actionText} diff --git a/packages/manager/src/components/OSIcon.tsx b/packages/manager/src/components/OSIcon.tsx index 9036f99ff80..3a0524da8a5 100644 --- a/packages/manager/src/components/OSIcon.tsx +++ b/packages/manager/src/components/OSIcon.tsx @@ -21,7 +21,9 @@ interface Props extends BoxProps { export const OSIcon = (props: Props) => { const { os, ...rest } = props; - const className = os ? `fl-${OS_ICONS[os] ?? 'tux'}` : `fl-tux`; + const className = os + ? `fl-${OS_ICONS[os as keyof typeof OS_ICONS] ?? 'tux'}` + : `fl-tux`; return ; }; diff --git a/packages/manager/src/components/PaymentMethodRow/ThirdPartyPayment.tsx b/packages/manager/src/components/PaymentMethodRow/ThirdPartyPayment.tsx index a705ea605be..4e45d4fcf80 100644 --- a/packages/manager/src/components/PaymentMethodRow/ThirdPartyPayment.tsx +++ b/packages/manager/src/components/PaymentMethodRow/ThirdPartyPayment.tsx @@ -84,11 +84,14 @@ export const ThirdPartyPayment = (props: Props) => { - {!matchesSmDown ? ( + {!matchesSmDown && ( - {thirdPartyPaymentMap[paymentMethod.type].label} + { + thirdPartyPaymentMap[paymentMethod.type as _ThirdPartyPayment] + .label + } - ) : null} + )} {renderThirdPartyPaymentBody(paymentMethod)} diff --git a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx index 6d329cfad77..2333c1fdabc 100644 --- a/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx +++ b/packages/manager/src/components/RegionSelect/RegionSelect.utils.tsx @@ -43,7 +43,9 @@ export const getRegionOptions = ({ return ( region.site_type === 'edge' || (region.site_type === 'distributed' && - CONTINENT_CODE_TO_CONTINENT[distributedContinentCode] === group) + CONTINENT_CODE_TO_CONTINENT[ + distributedContinentCode as keyof typeof CONTINENT_CODE_TO_CONTINENT + ] === group) ); } return regionFilter.includes(region.site_type); diff --git a/packages/manager/src/components/Uploaders/reducer.ts b/packages/manager/src/components/Uploaders/reducer.ts index 1e291ae85a9..dbe628312e0 100644 --- a/packages/manager/src/components/Uploaders/reducer.ts +++ b/packages/manager/src/components/Uploaders/reducer.ts @@ -98,9 +98,11 @@ const cloneLandingReducer = ( }); if (existingFile) { - Object.keys(action.data).forEach((key) => { - existingFile[key] = action.data[key]; - }); + Object.keys(action.data).forEach( + (key: keyof Partial) => { + (existingFile as any)[key] = action.data[key]; + } + ); // If the status has been changed, we need to update the count. if (action.data.status) { diff --git a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx index 968c2f4d2cb..f35942adede 100644 --- a/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx +++ b/packages/manager/src/features/Databases/DatabaseDetail/DatabaseStatusDisplay.tsx @@ -43,7 +43,7 @@ export const DatabaseStatusDisplay = (props: Props) => { ) { displayedStatus = ( <> - + {`Resizing ${progress ? `(${progress}%)` : '(0%)'}`} 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 e57bd284795..cd5a44bf5b0 100644 --- a/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts +++ b/packages/manager/src/features/Firewalls/FirewallDetail/Rules/FirewallRuleDrawer.utils.ts @@ -283,9 +283,8 @@ export const portStringToItems = ( const customInput: string[] = []; ports.forEach((thisPort) => { - const preset = PORT_PRESETS[thisPort]; - if (preset) { - items.push(preset); + if (thisPort in PORT_PRESETS) { + items.push(PORT_PRESETS[thisPort as keyof typeof PORT_PRESETS]); } else { customInput.push(thisPort); } diff --git a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx index b5619b8b0fc..4e68b575e38 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/LinodeCreate.tsx @@ -92,10 +92,11 @@ import type { import type { PlacementGroup } from '@linode/api-v4'; import type { CreateLinodePlacementGroupPayload, + CreateLinodeRequest, EncryptionStatus, InterfacePayload, PriceObject, -} from '@linode/api-v4/lib/linodes'; +} from '@linode/api-v4'; import type { Tag } from '@linode/api-v4/lib/tags/types'; import type { MapDispatchToProps } from 'react-redux'; import type { RouteComponentProps } from 'react-router-dom'; @@ -289,7 +290,7 @@ export class LinodeCreate extends React.PureComponent< // eslint-disable-next-line sonarjs/no-unused-collection const interfaces: InterfacePayload[] = []; - const payload = { + const payload: CreateLinodeRequest = { authorized_users: this.props.authorized_users, backup_id: this.props.selectedBackupID, backups_enabled: this.props.backupsEnabled, diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx index 20ef138311b..0dc9de9ec00 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromStackScriptContent.tsx @@ -157,12 +157,15 @@ export class FromStackScriptContent extends React.PureComponent { * if a UDF field comes back from the API with a "default" * value, it means we need to pre-populate the field and form state */ - const defaultUDFData = userDefinedFields.reduce((accum, eachField) => { - if (eachField.default) { - accum[eachField.name] = eachField.default; - } - return accum; - }, {}); + const defaultUDFData = userDefinedFields.reduce>( + (accum, eachField) => { + if (eachField.default) { + accum[eachField.name] = eachField.default; + } + return accum; + }, + {} + ); this.props.updateStackScript( id, diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx index d341552d618..0e0a27d989a 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeNetworking/IPTransfer.tsx @@ -401,7 +401,7 @@ export const IPTransfer = (props: Props) => { */ if (!equals(previousIPAddresses, ipAddresses)) { setIPs( - ipAddresses.reduce((acc, ip) => { + ipAddresses.reduce((acc, ip) => { acc[ip] = defaultState(ip, linodeId); return acc; }, {}) diff --git a/packages/manager/src/features/Longview/shared/utilities.ts b/packages/manager/src/features/Longview/shared/utilities.ts index dfd23d6b7af..df72d1f1ff3 100644 --- a/packages/manager/src/features/Longview/shared/utilities.ts +++ b/packages/manager/src/features/Longview/shared/utilities.ts @@ -253,9 +253,12 @@ export const sumStatsObject = ( if (thisObject && typeof thisObject === 'object') { Object.keys(thisObject).forEach((thisKey) => { if (thisKey in accum) { - accum[thisKey] = appendStats(accum[thisKey], thisObject[thisKey]); + (accum as any)[thisKey] = appendStats( + (accum as any)[thisKey], + (thisObject as any)[thisKey] + ); } else { - accum[thisKey] = thisObject[thisKey]; + (accum as any)[thisKey] = (thisObject as any)[thisKey]; } }); } diff --git a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx index 2f9bd20478d..02b819e3b90 100644 --- a/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlanInformation.tsx @@ -135,7 +135,10 @@ export const ClassDescriptionCopy = (props: ClassDescriptionCopyProps) => { - {planTabInfoContent[planType]?.typography}{' '} + { + planTabInfoContent[planType as keyof typeof planTabInfoContent] + ?.typography + }{' '} Learn more about our {planTypeLabel} plans. ) : null; diff --git a/packages/manager/src/queries/profile/profile.ts b/packages/manager/src/queries/profile/profile.ts index cc57db89c4e..8192d8bbbb2 100644 --- a/packages/manager/src/queries/profile/profile.ts +++ b/packages/manager/src/queries/profile/profile.ts @@ -141,7 +141,7 @@ export const useSSHKeysQuery = ( filter?: Filter, enabled = true ) => - useQuery({ + useQuery, APIError[]>({ ...profileQueries.sshKeys(params, filter), enabled, keepPreviousData: true, From 97ee5edf5544720040c74b38149c3a16ffe92787 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 17:27:43 -0400 Subject: [PATCH 07/19] save changes #7 --- .../src/dev-tools/EnvironmentToggleTool.tsx | 8 ++-- .../src/features/Betas/BetasLanding.tsx | 20 ++++++---- .../PaymentDrawer/PaymentMethodCard.tsx | 10 ++--- .../shared/CloudPulseTimeRangeSelect.tsx | 12 +----- .../ConfirmTransferDialog.tsx | 38 ++++++++++--------- .../Tabs/Marketplace/AppDetailDrawer.tsx | 5 ++- .../LinodesCreate/LinodeCreateContainer.tsx | 9 +---- .../TabbedContent/FromAppsContent.test.tsx | 13 +++++-- .../TabbedContent/FromAppsContent.tsx | 15 +++++--- .../DetailTabs/Processes/ProcessesLanding.tsx | 11 +++--- 10 files changed, 70 insertions(+), 71 deletions(-) diff --git a/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx b/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx index f50cd49697c..f0e83faa717 100644 --- a/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx +++ b/packages/manager/src/dev-tools/EnvironmentToggleTool.tsx @@ -35,10 +35,10 @@ export const getOptions = (env: Partial) => { return [ ...acc, { - apiRoot: env[`${base}_API_ROOT`] ?? '', - clientID: env[`${base}_CLIENT_ID`] ?? '', - label: env[thisEnvVariable] ?? '', - loginRoot: env[`${base}_LOGIN_ROOT`] ?? '', + apiRoot: (env as any)[`${base}_API_ROOT`] ?? '', + clientID: (env as any)[`${base}_CLIENT_ID`] ?? '', + label: (env as any)[thisEnvVariable] ?? '', + loginRoot: (env as any)[`${base}_LOGIN_ROOT`] ?? '', }, ]; }, []); diff --git a/packages/manager/src/features/Betas/BetasLanding.tsx b/packages/manager/src/features/Betas/BetasLanding.tsx index bf868e56529..9cc93fc6a1c 100644 --- a/packages/manager/src/features/Betas/BetasLanding.tsx +++ b/packages/manager/src/features/Betas/BetasLanding.tsx @@ -7,6 +7,7 @@ import { BetaDetailsList } from 'src/features/Betas/BetaDetailsList'; import { useAccountBetasQuery } from 'src/queries/account/betas'; import { useBetasQuery } from 'src/queries/betas'; import { categorizeBetasByStatus } from 'src/utilities/betaUtils'; +import { AccountBeta, Beta } from '@linode/api-v4'; const BetasLanding = () => { const { @@ -24,14 +25,17 @@ const BetasLanding = () => { const betas = betasRequest?.data ?? []; const allBetas = [...accountBetas, ...betas]; - const allBetasMerged = allBetas.reduce((acc, beta) => { - if (acc[beta.id]) { - acc[beta.id] = Object.assign(beta, acc[beta.id]); - } else { - acc[beta.id] = beta; - } - return acc; - }, {}); + const allBetasMerged = allBetas.reduce>( + (acc, beta) => { + if (acc[beta.id]) { + acc[beta.id] = Object.assign(beta, acc[beta.id]); + } else { + acc[beta.id] = beta; + } + return acc; + }, + {} + ); const { active, available, historical } = categorizeBetasByStatus( Object.values(allBetasMerged) diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx index 72ae44d5302..03de6207741 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx @@ -38,12 +38,12 @@ const getIcon = (paymentMethod: PaymentMethod) => { } }; -const getHeading = (paymentMethod: PaymentMethod, type: PaymentType) => { +const getHeading = (paymentMethod: PaymentMethod) => { switch (paymentMethod.type) { case 'paypal': - return thirdPartyPaymentMap[type].label; + return thirdPartyPaymentMap[paymentMethod.type].label; case 'google_pay': - return `${thirdPartyPaymentMap[type].label} ${paymentMethod.data.card_type} ****${paymentMethod.data.last_four}`; + return `${thirdPartyPaymentMap[paymentMethod.type].label} ${paymentMethod.data.card_type} ****${paymentMethod.data.last_four}`; default: return `${paymentMethod.data.card_type} ****${paymentMethod.data.last_four}`; } @@ -69,9 +69,9 @@ export const PaymentMethodCard = (props: Props) => { paymentMethod, paymentMethodId, } = props; - const { id, is_default, type } = paymentMethod; + const { id, is_default } = paymentMethod; - const heading = getHeading(paymentMethod, type); + const heading = getHeading(paymentMethod); const cardIsExpired = getIsCardExpired(paymentMethod); const subHeading = getSubHeading(paymentMethod, cardIsExpired); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx index 57073c9ae21..bf8af72997a 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx @@ -13,6 +13,7 @@ import type { BaseSelectProps, Item, } from 'src/components/EnhancedSelect/Select'; +import R from 'ramda'; interface Props extends Omit< @@ -136,14 +137,3 @@ export const generateStartTime = (modifier: Labels, nowInSeconds: number) => { return nowInSeconds - 30 * 24 * 60 * 60; } }; - -/** - * - * @param label label for time duration to get the corresponding time duration object - * @returns time duration object for the label - */ -export const getTimeDurationFromTimeRange = (label: string): TimeDuration => { - const options = generateSelectOptions(); - - return options[label] || { unit: 'min', vlaue: 30 }; -}; diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx index 27f7057130d..d1f99dab257 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersLanding/ConfirmTransferDialog.tsx @@ -219,24 +219,26 @@ export const DialogContent = React.memo((props: ContentProps) => { This transfer contains: - {Object.keys(entities).map((thisEntityType) => { - // According to spec, all entity names are plural and lowercase - // (NB: This may cause problems for NodeBalancers if/when they are added to the payload) - const entityName = capitalize(thisEntityType).slice(0, -1); - return ( -
  • - - - {pluralize( - entityName, - entityName + 's', - entities[thisEntityType].length - )} - - -
  • - ); - })} + {Object.keys(entities).map( + (thisEntityType: keyof TransferEntities) => { + // According to spec, all entity names are plural and lowercase + // (NB: This may cause problems for NodeBalancers if/when they are added to the payload) + const entityName = capitalize(thisEntityType).slice(0, -1); + return ( +
  • + + + {pluralize( + entityName, + entityName + 's', + entities[thisEntityType].length + )} + + +
  • + ); + } + )}
    {timeRemaining ? ( diff --git a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppDetailDrawer.tsx b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppDetailDrawer.tsx index 2c6f0d88903..a01aec66a99 100644 --- a/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppDetailDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodeCreatev2/Tabs/Marketplace/AppDetailDrawer.tsx @@ -108,8 +108,9 @@ export const AppDetailDrawerv2 = (props: Props) => { > {`${selectedApp.name} { it('should exist', () => { expect(FromAppsContent).toBeDefined(); @@ -34,10 +36,13 @@ describe('getCompatibleImages', () => { it('should an array of Images compatible with the StackScript', () => { const imagesDataArray = imageFactory.buildList(5); - const imagesData = imagesDataArray.reduce((acc, imageData) => { - acc[imageData.label] = imageData; - return acc; - }, {}); + const imagesData = imagesDataArray.reduce>( + (acc, imageData) => { + acc[imageData.label] = imageData; + return acc; + }, + {} + ); const stackScriptImages = Object.keys(imagesData).slice(0, 2); const result = getCompatibleImages(imagesData, stackScriptImages); expect(result.length).toBe(2); diff --git a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx index d1b24940b06..6abde7a3f78 100644 --- a/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx +++ b/packages/manager/src/features/Linodes/LinodesCreate/TabbedContent/FromAppsContent.tsx @@ -94,12 +94,15 @@ export const getCompatibleImages = ( ); export const getDefaultUDFData = (userDefinedFields: UserDefinedField[]) => { - return userDefinedFields.reduce((accum, eachField) => { - if (eachField.default) { - accum[eachField.name] = eachField.default; - } - return accum; - }, {}); + return userDefinedFields.reduce>( + (accum, eachField) => { + if (eachField.default) { + accum[eachField.name] = eachField.default; + } + return accum; + }, + {} + ); }; export const handleSelectStackScript = ( 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 0adbe61d8a4..78d1ee48dd8 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx @@ -177,17 +177,16 @@ export const extendData = ( // with a string as the value. Here, we separate these keys. const { longname, ...users } = processesData.Processes![processName]; - Object.keys(users).forEach((user) => { + Object.keys(users).forEach((user: keyof typeof users) => { const userProcess = processesData.Processes![processName][user]; extendedData.push({ - averageCPU: statAverage(userProcess.cpu), + averageCPU: statAverage(users.cpu), averageIO: - statAverage(userProcess.ioreadkbytes) + - statAverage(userProcess.iowritekbytes), - averageMem: statAverage(userProcess.mem), + statAverage(users.ioreadkbytes) + statAverage(users.iowritekbytes), + averageMem: statAverage(users.mem), id: `${processName}-${user}`, - maxCount: statMax(userProcess.count), + maxCount: statMax(users.count), name: processName, user, }); From 1047e90ca3314b0f37ba2b8c0be5e85ade0efb32 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 17:35:59 -0400 Subject: [PATCH 08/19] save changes #8 --- .../LinodeSettings/KernelSelect.tsx | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/KernelSelect.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/KernelSelect.tsx index 4d7c5e3d89f..650c5fb56f3 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/KernelSelect.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/KernelSelect.tsx @@ -1,13 +1,19 @@ -import { Kernel } from '@linode/api-v4/lib/linodes/types'; import { groupBy } from 'ramda'; import * as React from 'react'; -import Select, { GroupType, Item } from 'src/components/EnhancedSelect/Select'; +import Select from 'src/components/EnhancedSelect/Select'; + +import type { Kernel } from '@linode/api-v4'; + +interface Option { + label: string; + value: string; +} export interface KernelSelectProps { errorText?: string; kernels: Kernel[]; - onChange: (selected: Item) => void; + onChange: (selected: Option) => void; readOnly?: boolean; selectedKernel?: string; } @@ -40,7 +46,7 @@ export const KernelSelect = React.memo((props: KernelSelectProps) => { export const getSelectedKernelId = ( kernelID: string | undefined, - options: GroupType[] + options: KernelGroupOption[] ) => { if (!kernelID) { return null; @@ -74,27 +80,37 @@ export const groupKernels = (kernel: Kernel) => { return 'Current'; }; +type KernelGroup = ReturnType; + +interface KernelGroupOption { + label: KernelGroup; + options: Option[]; +} + export const kernelsToGroupedItems = (kernels: Kernel[]) => { const groupedKernels = groupBy(groupKernels, kernels); groupedKernels.Current = sortCurrentKernels(groupedKernels.Current); return Object.keys(groupedKernels) - .reduce((accum: GroupType[], thisGroup) => { - const group = groupedKernels[thisGroup]; - if (!group || group.length === 0) { - return accum; - } - return [ - ...accum, - { - label: thisGroup, - options: groupedKernels[thisGroup].map((thisKernel) => ({ - label: thisKernel.label, - value: thisKernel.id, - })), - }, - ]; - }, []) + .reduce( + (accum: KernelGroupOption[], thisGroup: KernelGroup) => { + const group = groupedKernels[thisGroup]; + if (!group || group.length === 0) { + return accum; + } + return [ + ...accum, + { + label: thisGroup, + options: groupedKernels[thisGroup].map((thisKernel) => ({ + label: thisKernel.label, + value: thisKernel.id, + })), + }, + ]; + }, + [] + ) .sort(sortKernelGroups); }; @@ -105,7 +121,7 @@ const PRIORITY = { Deprecated: 1, }; -const sortKernelGroups = (a: GroupType, b: GroupType) => { +const sortKernelGroups = (a: KernelGroupOption, b: KernelGroupOption) => { if (PRIORITY[a.label] > PRIORITY[b.label]) { return -1; } From 937eb3f82312f22fcbda04e3c4e1911934c55bed Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 18:21:24 -0400 Subject: [PATCH 09/19] fix image select --- .../src/features/Images/ImageSelect.tsx | 179 +++++------------- .../LinodeSettings/ImageAndPassword.tsx | 7 +- .../LinodeStorage/CreateDiskDrawer.tsx | 7 +- .../StackScriptCreate/StackScriptCreate.tsx | 14 +- .../StackScriptForm/StackScriptForm.tsx | 9 +- packages/manager/src/utilities/images.ts | 41 ++-- 6 files changed, 77 insertions(+), 180 deletions(-) diff --git a/packages/manager/src/features/Images/ImageSelect.tsx b/packages/manager/src/features/Images/ImageSelect.tsx index af442529330..024decfa6d4 100644 --- a/packages/manager/src/features/Images/ImageSelect.tsx +++ b/packages/manager/src/features/Images/ImageSelect.tsx @@ -1,31 +1,17 @@ -import Autocomplete from '@mui/material/Autocomplete'; -import { clone, propOr } from 'ramda'; import * as React from 'react'; +import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Box } from 'src/components/Box'; -import { TextField } from 'src/components/TextField'; -import { TooltipIcon } from 'src/components/TooltipIcon'; -import { useAllImagesQuery } from 'src/queries/images'; -import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; -import { groupImages } from 'src/utilities/images'; +import { imageFactory } from 'src/factories'; +import { getImageGroup } from 'src/utilities/images'; import type { Image } from '@linode/api-v4/lib/images'; -export interface SelectImageOption { - label: string; - value: string; -} - -export interface ImagesGroupType { - label: string; - options: SelectImageOption[]; -} interface BaseProps { anyAllOption?: boolean; disabled?: boolean; + errorText?: string; helperText?: string; - imageError?: string; - imageFieldError?: string; images: Image[]; label?: string; required?: boolean; @@ -33,23 +19,22 @@ interface BaseProps { interface Props extends BaseProps { isMulti?: false; - onSelect: (selected: SelectImageOption) => void; - value?: SelectImageOption; + onSelect: (image: Image) => void; + value?: string; } interface MultiProps extends BaseProps { isMulti: true; - onSelect: (selected: SelectImageOption[]) => void; - value?: SelectImageOption[]; + onSelect: (selected: Image[]) => void; + value?: string[]; } export const ImageSelect = (props: MultiProps | Props) => { const { anyAllOption, disabled, + errorText, helperText, - imageError, - imageFieldError, images, isMulti, label, @@ -58,36 +43,19 @@ export const ImageSelect = (props: MultiProps | Props) => { value, } = props; - const { error, isError, isLoading: imagesLoading } = useAllImagesQuery( - {}, - {} - ); - - // Check for request errors in RQ - const rqError = isError - ? getAPIErrorOrDefault(error ?? [], 'Unable to load Images')[0].reason - : undefined; - - const renderedImages = React.useMemo(() => getImagesOptions(images), [ - images, - ]); - - const imageSelectOptions = clone(renderedImages); - - if (anyAllOption) { - imageSelectOptions.unshift({ - label: '', - options: [ - { - label: 'Any/All', - value: 'any/all', - }, - ], - }); - } - - const formattedOptions = imageSelectOptions.flatMap( - (option) => option.options + const options = React.useMemo( + () => [ + ...(anyAllOption + ? [ + imageFactory.build({ + id: 'any/all', + label: 'Any/All', + }), + ] + : []), + ...images, + ], + [anyAllOption, images] ); return ( @@ -98,86 +66,35 @@ export const ImageSelect = (props: MultiProps | Props) => { width: '100%', }} > - { + if (isMulti && Array.isArray(value)) { + onSelect((value ?? []) as Image[]); + } else if (!isMulti) { + onSelect(value as Image); + } + }} + textFieldProps={{ + required, + tooltipText: helperText ?? 'Choosing a 64-bit distro is recommended.', }} - > - { - const group = imageSelectOptions.find((group) => - group.options.includes(option) - ); - return group ? String(group.label) : ''; - }} - onChange={(event, value) => { - onSelect(value ?? []); - }} - renderInput={(params) => ( - - )} - disabled={disabled || Boolean(imageError)} - id={'image-select'} - loading={imagesLoading} - multiple={Boolean(isMulti)} - options={formattedOptions} - value={value} - /> - - - - + value={ + isMulti + ? options.filter((o) => value?.includes(o.id)) ?? [] + : options.find((o) => o.id === value) ?? null + } + disableSelectAll + disabled={disabled} + errorText={errorText} + filterSelectedOptions + groupBy={getImageGroup} + label={label ?? 'Image'} + multiple={isMulti} + options={options} + placeholder="Select an Image" + /> ); }; -export const getImagesOptions = (images: Image[]) => { - const groupedImages = groupImages(images); - return ['recommended', 'older', 'images', 'deleted'].reduce( - (accumulator: ImagesGroupType[], category: string) => { - if (groupedImages[category]) { - return [ - ...accumulator, - { - label: getDisplayNameForGroup(category), - options: groupedImages[category].map(({ id, label }: Image) => ({ - label, - value: id, - })), - }, - ]; - } - return accumulator; - }, - [] - ); -}; - -export const groupNameMap = { - _default: 'Other', - deleted: 'Recently Deleted Disks', - images: 'Images', - older: 'Older Distributions', - recommended: '64-bit Distributions - Recommended', -}; - -const getDisplayNameForGroup = (key: string) => - propOr('Other', key, groupNameMap); - export default ImageSelect; diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/ImageAndPassword.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/ImageAndPassword.tsx index 97b48199e35..ef341b06ae1 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/ImageAndPassword.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeSettings/ImageAndPassword.tsx @@ -9,13 +9,13 @@ import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; import { LinodePermissionsError } from '../LinodePermissionsError'; -import type { SelectImageOption } from 'src/features/Images/ImageSelect'; +import type { Image } from '@linode/api-v4'; interface Props { authorizedUsers: string[]; imageFieldError?: string; linodeId: number; - onImageChange: (selected: SelectImageOption) => void; + onImageChange: (selected: Image) => void; onPasswordChange: (password: string) => void; password: string; passwordError?: string; @@ -51,8 +51,7 @@ export const ImageAndPassword = (props: Props) => { {disabled && } diff --git a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx index 30fe4d0df74..83d27bf867c 100644 --- a/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx +++ b/packages/manager/src/features/Linodes/LinodesDetail/LinodeStorage/CreateDiskDrawer.tsx @@ -10,7 +10,6 @@ import * as React from 'react'; import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; import { Autocomplete } from 'src/components/Autocomplete/Autocomplete'; import { Drawer } from 'src/components/Drawer'; -import { Item } from 'src/components/EnhancedSelect/Select'; import { FormHelperText } from 'src/components/FormHelperText'; import { InputAdornment } from 'src/components/InputAdornment'; import { Mode, ModeSelect } from 'src/components/ModeSelect/ModeSelect'; @@ -26,6 +25,8 @@ import { handleAPIErrors } from 'src/utilities/formikErrorUtils'; import { ImageAndPassword } from '../LinodeSettings/ImageAndPassword'; +import type { Image } from '@linode/api-v4' + type FileSystem = 'ext3' | 'ext4' | 'initrd' | 'raw' | 'swap'; type CreateMode = 'empty' | 'from_image'; @@ -171,8 +172,8 @@ export const CreateDiskDrawer = (props: Props) => { imageFieldError={ formik.touched.image ? formik.errors.image : undefined } - onImageChange={(selected: Item) => - formik.setFieldValue('image', selected?.value ?? null) + onImageChange={(selected: Image) => + formik.setFieldValue('image', selected?.id ?? null) } onPasswordChange={(root_pass: string) => formik.setFieldValue('root_pass', root_pass) diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx index 2d05092f8d9..3181f007ddf 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx @@ -27,18 +27,18 @@ import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; import { scrollErrorIntoView } from 'src/utilities/scrollErrorIntoView'; import { storage } from 'src/utilities/storage'; -import type { Account, Grant } from '@linode/api-v4/lib/account'; import type { + APIError, + Image, StackScript, StackScriptPayload, -} from '@linode/api-v4/lib/stackscripts'; -import type { APIError } from '@linode/api-v4/lib/types'; +} from '@linode/api-v4'; +import type { Account, Grant } from '@linode/api-v4/lib/account'; import type { QueryClient } from '@tanstack/react-query'; import type { RouteComponentProps } from 'react-router-dom'; import type { WithImagesProps } from 'src/containers/images.container'; import type { WithProfileProps } from 'src/containers/profile.container'; import type { WithQueryClientProps } from 'src/containers/withQueryClient.container'; -import type { SelectImageOption } from 'src/features/Images/ImageSelect'; interface State { apiResponse?: StackScript; @@ -129,10 +129,11 @@ export class StackScriptCreate extends React.Component { ); }; - handleChooseImage = (images: SelectImageOption[]) => { - const imageList = images.map((image) => image.value); + handleChooseImage = (images: Image[]) => { + const imageList = images.map((image) => image.id); const anyAllOptionChosen = imageList.includes('any/all'); + console.log(anyAllOptionChosen) this.setState( { @@ -436,7 +437,6 @@ export class StackScriptCreate extends React.Component { const availableImages = Object.values(_imagesData).filter( (thisImage) => - !this.state.images.includes(thisImage.id) && !thisImage.label.match(/kube/i) ); diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx index 3f3deb0f5ed..0f0d68a2a1d 100644 --- a/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptForm/StackScriptForm.tsx @@ -14,11 +14,9 @@ import { StyledNotice, StyledTextField, } from './StackScriptForm.styles'; -import { imageToImageOptions } from './utils'; import type { Image } from '@linode/api-v4/lib/images'; import type { APIError } from '@linode/api-v4/lib/types'; -import type { SelectImageOption } from 'src/features/Images/ImageSelect'; interface TextFieldHandler { handler: (e: React.ChangeEvent) => void; @@ -43,7 +41,7 @@ interface Props { label: TextFieldHandler; mode: 'create' | 'edit'; onCancel: () => void; - onSelectChange: (image: SelectImageOption[]) => void; + onSelectChange: (image: Image[]) => void; onSubmit: () => void; revision: TextFieldHandler; script: TextFieldHandler; @@ -74,7 +72,6 @@ export const StackScriptForm = React.memo((props: Props) => { } = props; const hasErrorFor = getAPIErrorFor(errorResources, errors); - const selectedImages = imageToImageOptions(images.selected); return ( ({ padding: theme.spacing(2) })}> @@ -113,13 +110,13 @@ export const StackScriptForm = React.memo((props: Props) => { anyAllOption data-qa-stackscript-target-select disabled={disabled} - imageFieldError={hasErrorFor('images')} + errorText={hasErrorFor('images')} images={images.available} isMulti label="Target Images" onSelect={onSelectChange} required - value={selectedImages} + value={images.selected} /> diff --git a/packages/manager/src/utilities/images.ts b/packages/manager/src/utilities/images.ts index 48e507ef9b5..75df83c4dbc 100644 --- a/packages/manager/src/utilities/images.ts +++ b/packages/manager/src/utilities/images.ts @@ -1,5 +1,4 @@ -import { Image } from '@linode/api-v4/lib/images'; -import { always, cond, groupBy, propOr } from 'ramda'; +import type { Image } from '@linode/api-v4'; const isRecentlyDeleted = (i: Image) => i.created_by === null && i.type === 'automatic'; @@ -9,31 +8,15 @@ const isDeprecated = (i: Image) => i.deprecated === true; const isRecommended = (i: Image) => isByLinode(i) && !isDeprecated(i); const isOlderImage = (i: Image) => isByLinode(i) && isDeprecated(i); -interface GroupedImages { - deleted?: Image[]; - images?: Image[]; - older?: Image[]; - recommended?: Image[]; -} - -export let groupImages: (i: Image[]) => GroupedImages; -// eslint-disable-next-line -groupImages = groupBy( - cond([ - [isRecentlyDeleted, always('deleted')], - [isRecommended, always('recommended')], - [isOlderImage, always('older')], - [(_) => true, always('images')], - ]) -); - -export const groupNameMap = { - _default: 'Other', - deleted: 'Recently Deleted Disks', - images: 'Images', - older: 'Older Distributions', - recommended: '64-bit Distributions - Recommended', +export const getImageGroup = (image: Image) => { + if (isRecentlyDeleted(image)) { + return 'Recently Deleted Disks'; + } + if (isRecommended(image)) { + return '64-bit Distributions - Recommended'; + } + if (isOlderImage(image)) { + return 'Older Distributions'; + } + return 'Images'; }; - -export const getDisplayNameForGroup = (key: string) => - propOr('Other', key, groupNameMap); From adeb30a1d36a80c4d66242204b2d3ff4501078fe Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Mon, 5 Aug 2024 18:47:25 -0400 Subject: [PATCH 10/19] save changes #9 --- .../components/DownloadCSV/DownloadCSV.tsx | 2 +- packages/manager/src/factories/grants.ts | 1 + .../PaymentDrawer/PaymentMethodCard.tsx | 5 +- .../shared/CloudPulseTimeRangeSelect.tsx | 2 - .../TransferCheckoutBar.tsx | 2 +- .../Events/EventsMessages.stories.tsx | 5 +- .../features/Events/eventMessageGenerator.ts | 8 +- .../manager/src/features/Events/factory.tsx | 2 +- .../src/features/Images/ImageSelect.test.tsx | 170 +++++++----------- .../StackScriptCreate/StackScriptCreate.tsx | 5 +- .../StackScriptForm/utils.test.ts | 25 --- .../StackScripts/StackScriptForm/utils.ts | 14 -- packages/manager/src/utilities/images.test.ts | 73 -------- 13 files changed, 82 insertions(+), 232 deletions(-) delete mode 100644 packages/manager/src/features/StackScripts/StackScriptForm/utils.test.ts delete mode 100644 packages/manager/src/features/StackScripts/StackScriptForm/utils.ts delete mode 100644 packages/manager/src/utilities/images.test.ts diff --git a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx index 7a9d3791f9d..2f41e238845 100644 --- a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx +++ b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx @@ -86,7 +86,7 @@ export const cleanCSVData = (data: any): any => { /** if it's an object, recursively sanitize each key value pair */ if (typeof data === 'object') { - return Object.keys(data).reduce((acc, eachKey) => { + return Object.keys(data).reduce((acc, eachKey) => { acc[eachKey] = cleanCSVData(data[eachKey]); return acc; }, {}); diff --git a/packages/manager/src/factories/grants.ts b/packages/manager/src/factories/grants.ts index bfa53a7de8b..b641263ca03 100644 --- a/packages/manager/src/factories/grants.ts +++ b/packages/manager/src/factories/grants.ts @@ -31,6 +31,7 @@ export const grantsFactory = Factory.Sync.makeFactory({ ], global: { account_access: 'read_write', + add_databases: true, add_domains: true, add_firewalls: true, add_images: true, diff --git a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx index 03de6207741..540956a6f39 100644 --- a/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx +++ b/packages/manager/src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/PaymentMethodCard.tsx @@ -1,6 +1,5 @@ -import { PaymentMethod, PaymentType } from '@linode/api-v4'; -import Grid from '@mui/material/Unstable_Grid2'; import { useTheme } from '@mui/material/styles'; +import Grid from '@mui/material/Unstable_Grid2'; import * as React from 'react'; import { Chip } from 'src/components/Chip'; @@ -12,6 +11,8 @@ import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; import { getIcon as getCreditCardIcon } from 'src/features/Billing/BillingPanels/BillingSummary/PaymentDrawer/CreditCard'; import { formatExpiry, isCreditCardExpired } from 'src/utilities/creditCard'; +import type { PaymentMethod } from '@linode/api-v4'; + interface Props { disabled?: boolean; handlePaymentMethodChange: (id: number, cardExpired: boolean) => void; diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx index bf8af72997a..f05a52225de 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseTimeRangeSelect.tsx @@ -8,12 +8,10 @@ import { updateGlobalFilterPreference, } from '../Utils/UserPreference'; -import type { TimeDuration } from '@linode/api-v4'; import type { BaseSelectProps, Item, } from 'src/components/EnhancedSelect/Select'; -import R from 'ramda'; interface Props extends Omit< diff --git a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferCheckoutBar.tsx b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferCheckoutBar.tsx index 783e72be4e9..df8283ee4c3 100644 --- a/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferCheckoutBar.tsx +++ b/packages/manager/src/features/EntityTransfers/EntityTransfersCreate/TransferCheckoutBar.tsx @@ -26,7 +26,7 @@ export const generatePayload = ( selectedEntities: TransferState ): CreateTransferPayload => { const entities = Object.keys(selectedEntities).reduce( - (acc, entityType) => { + (acc, entityType: keyof TransferState) => { return { ...acc, [entityType]: Object.keys(selectedEntities[entityType]).map(Number), diff --git a/packages/manager/src/features/Events/EventsMessages.stories.tsx b/packages/manager/src/features/Events/EventsMessages.stories.tsx index 72325c69c1f..35ad2beb4cf 100644 --- a/packages/manager/src/features/Events/EventsMessages.stories.tsx +++ b/packages/manager/src/features/Events/EventsMessages.stories.tsx @@ -10,6 +10,7 @@ import { Typography } from 'src/components/Typography'; import { eventFactory } from 'src/factories/events'; import { eventMessages } from 'src/features/Events/factory'; +import type { EventMessage } from './types'; import type { Event } from '@linode/api-v4/lib/account'; import type { Meta, StoryObj } from '@storybook/react'; @@ -51,8 +52,8 @@ export const EventMessages: StoryObj = { - {Object.keys(statuses).map((status, key) => { - const message = statuses[status](event); + {Object.keys(statuses).map((status: keyof EventMessage, key) => { + const message = statuses[status]?.(event); return ( diff --git a/packages/manager/src/features/Events/eventMessageGenerator.ts b/packages/manager/src/features/Events/eventMessageGenerator.ts index 800ab1068e7..6004fd994e4 100644 --- a/packages/manager/src/features/Events/eventMessageGenerator.ts +++ b/packages/manager/src/features/Events/eventMessageGenerator.ts @@ -297,7 +297,9 @@ export const eventMessageCreators: { [index: string]: CreatorsForStatus } = { notification: (e) => { if (e.secondary_entity?.type) { const secondaryEntityName = - secondaryFirewallEntityNameMap[e.secondary_entity.type]; + secondaryFirewallEntityNameMap[ + e.secondary_entity.type as FirewallDeviceEntityType + ]; return `${secondaryEntityName} ${ e.secondary_entity?.label } has been added to Firewall ${e.entity?.label ?? ''}.`; @@ -309,7 +311,9 @@ export const eventMessageCreators: { [index: string]: CreatorsForStatus } = { notification: (e) => { if (e.secondary_entity?.type) { const secondaryEntityName = - secondaryFirewallEntityNameMap[e.secondary_entity.type]; + secondaryFirewallEntityNameMap[ + e.secondary_entity.type as FirewallDeviceEntityType + ]; return `${secondaryEntityName} ${ e.secondary_entity?.label } has been removed from Firewall ${e.entity?.label ?? ''}.`; diff --git a/packages/manager/src/features/Events/factory.tsx b/packages/manager/src/features/Events/factory.tsx index 7a1dcddde67..0654ec21ecc 100644 --- a/packages/manager/src/features/Events/factory.tsx +++ b/packages/manager/src/features/Events/factory.tsx @@ -40,7 +40,7 @@ export const withTypography = (eventMap: EventMap): OptionalEventMap => { export const eventMessages: EventMap = Object.keys(factories).reduce( (acc, factoryName) => ({ ...acc, - ...withTypography(factories[factoryName]), + ...withTypography((factories as any)[factoryName]), }), {} as EventMap ); diff --git a/packages/manager/src/features/Images/ImageSelect.test.tsx b/packages/manager/src/features/Images/ImageSelect.test.tsx index 402da31392c..96960772f2f 100644 --- a/packages/manager/src/features/Images/ImageSelect.test.tsx +++ b/packages/manager/src/features/Images/ImageSelect.test.tsx @@ -1,39 +1,20 @@ -import { fireEvent, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import * as React from 'react'; import { imageFactory } from 'src/factories/images'; +import { getImageGroup } from 'src/utilities/images'; import { renderWithTheme } from 'src/utilities/testHelpers'; -import { ImageSelect, getImagesOptions, groupNameMap } from './ImageSelect'; +import { ImageSelect } from './ImageSelect'; -const images = imageFactory.buildList(10); - -const props = { - images, - onSelect: vi.fn(), -}; - -const privateImage1 = imageFactory.build({ - deprecated: false, - is_public: false, - type: 'manual', -}); - -const recommendedImage1 = imageFactory.build({ +const recommendedImage = imageFactory.build({ created_by: 'linode', deprecated: false, id: 'linode/0001', type: 'manual', }); -const recommendedImage2 = imageFactory.build({ - created_by: 'linode', - deprecated: false, - id: 'linode/0002', - type: 'manual', -}); - -const deletedImage1 = imageFactory.build({ +const deletedImage = imageFactory.build({ created_by: null, deprecated: false, expiry: '2019-04-09T04:13:37', @@ -41,90 +22,67 @@ const deletedImage1 = imageFactory.build({ type: 'automatic', }); +describe('getImageGroup', () => { + it('handles the recommended group', () => { + expect(getImageGroup(recommendedImage)).toBe( + '64-bit Distributions - Recommended' + ); + }); + + it('handles the deleted image group', () => { + expect(getImageGroup(deletedImage)).toBe('Recently Deleted Disks'); + }); +}); + describe('ImageSelect', () => { - describe('getImagesOptions function', () => { - it('should return a list of GroupType', () => { - const items = getImagesOptions([recommendedImage1, recommendedImage2]); - expect(items[0]).toHaveProperty('label', groupNameMap.recommended); - expect(items[0].options).toHaveLength(2); - }); - - it('should handle multiple groups', () => { - const items = getImagesOptions([ - recommendedImage1, - recommendedImage2, - privateImage1, - deletedImage1, - ]); - expect(items).toHaveLength(3); - const deleted = items.find((item) => item.label === groupNameMap.deleted); - expect(deleted!.options).toHaveLength(1); - }); - - it('should properly format GroupType options', () => { - const category = getImagesOptions([recommendedImage1])[0]; - const option = category.options[0]; - expect(option).toHaveProperty('label', recommendedImage1.label); - expect(option).toHaveProperty('value', recommendedImage1.id); - }); - - it('should handle empty input', () => { - expect(getImagesOptions([])).toEqual([]); - }); + it('should display an error', () => { + const { getByText } = renderWithTheme( + + ); + expect(getByText('An error')).toBeInTheDocument(); }); - describe('ImageSelect component', () => { - it('should render', () => { - renderWithTheme(); - screen.getByRole('combobox'); - }); - - it('should display an error', () => { - const imageError = 'An error'; - renderWithTheme(); - expect(screen.getByText(imageError)).toBeInTheDocument(); - }); - - it('should call onSelect with the selected value', () => { - const onSelectMock = vi.fn(); - renderWithTheme( - - ); - - const inputElement = screen.getByRole('combobox'); - - fireEvent.change(inputElement, { target: { value: 'image-1' } }); - fireEvent.keyDown(inputElement, { key: 'ArrowDown' }); - fireEvent.keyDown(inputElement, { key: 'Enter' }); - - expect(onSelectMock).toHaveBeenCalledWith({ - label: 'image-1', - value: 'private/1', - }); - }); - - it('should handle multiple selections', () => { - const onSelectMock = vi.fn(); - renderWithTheme( - - ); - - const inputElement = screen.getByRole('combobox'); - - // Select first option - fireEvent.change(inputElement, { target: { value: 'image-1' } }); - fireEvent.keyDown(inputElement, { key: 'ArrowDown' }); - fireEvent.keyDown(inputElement, { key: 'Enter' }); - - // Select second option - fireEvent.change(inputElement, { target: { value: 'image-2' } }); - fireEvent.keyDown(inputElement, { key: 'ArrowDown' }); - fireEvent.keyDown(inputElement, { key: 'Enter' }); - - expect(onSelectMock).toHaveBeenCalledWith([ - { label: 'image-1', value: 'private/1' }, - { label: 'image-2', value: 'private/2' }, - ]); - }); + it('should call onSelect with the selected value', async () => { + const onSelect = vi.fn(); + const images = [ + imageFactory.build({ label: 'image-0' }), + imageFactory.build({ label: 'image-1' }), + imageFactory.build({ label: 'image-2' }), + ]; + + const { getByLabelText, getByText } = renderWithTheme( + + ); + + await userEvent.click(getByLabelText('Image')); + + await userEvent.click(getByText('image-1')); + + expect(onSelect).toHaveBeenCalledWith(images[1]); + }); + + it('should handle any/all', async () => { + const onSelect = vi.fn(); + const images = [ + imageFactory.build({ label: 'image-0' }), + imageFactory.build({ label: 'image-1' }), + imageFactory.build({ label: 'image-2' }), + ]; + + const { getByLabelText, getByText } = renderWithTheme( + + ); + + await userEvent.click(getByLabelText('Image')); + + await userEvent.click(getByText('Any/All')); + + expect(onSelect).toHaveBeenCalledWith([ + expect.objectContaining({ id: 'any/all' }), + ]); }); }); diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx index 3181f007ddf..57909307270 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx @@ -133,7 +133,7 @@ export class StackScriptCreate extends React.Component { const imageList = images.map((image) => image.id); const anyAllOptionChosen = imageList.includes('any/all'); - console.log(anyAllOptionChosen) + console.log(anyAllOptionChosen); this.setState( { @@ -436,8 +436,7 @@ export class StackScriptCreate extends React.Component { const hasUnsavedChanges = this.hasUnsavedChanges(); const availableImages = Object.values(_imagesData).filter( - (thisImage) => - !thisImage.label.match(/kube/i) + (thisImage) => !thisImage.label.match(/kube/i) ); const stackScriptGrants = grants.data?.stackscript; diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/utils.test.ts b/packages/manager/src/features/StackScripts/StackScriptForm/utils.test.ts deleted file mode 100644 index cca9dccaeba..00000000000 --- a/packages/manager/src/features/StackScripts/StackScriptForm/utils.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { imageToImageOptions } from './utils'; - -const mockImages = ['linode/debian9', 'linode/arch']; - -describe('imageToItem utility function', () => { - it('should convert images to Item[]', () => { - const items = imageToImageOptions(mockImages); - expect(items).toHaveLength(mockImages.length); - expect(items[0]).toEqual({ - label: 'debian9', - value: 'linode/debian9', - }); - }); - - it('should return an empty array if the initial list is empty', () => { - expect(imageToImageOptions([])).toEqual([]); - }); - - it('should leave non-linode image labels unchanged', () => { - expect(imageToImageOptions(['exampleuser/myprivateimage'])[0]).toEqual({ - label: 'exampleuser/myprivateimage', - value: 'exampleuser/myprivateimage', - }); - }); -}); diff --git a/packages/manager/src/features/StackScripts/StackScriptForm/utils.ts b/packages/manager/src/features/StackScripts/StackScriptForm/utils.ts deleted file mode 100644 index be87ca209c1..00000000000 --- a/packages/manager/src/features/StackScripts/StackScriptForm/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { SelectImageOption } from 'src/features/Images/ImageSelect'; - -/** - * Takes a list of images (string[]) and converts - * them to SelectImageOption[] for use in the ImageSelect component. - * - * Also trims 'linode/' off the name of public images. - */ -export const imageToImageOptions = (images: string[]): SelectImageOption[] => { - return images.map((image) => ({ - label: image.replace('linode/', ''), - value: image, - })); -}; diff --git a/packages/manager/src/utilities/images.test.ts b/packages/manager/src/utilities/images.test.ts deleted file mode 100644 index 4d8717c216e..00000000000 --- a/packages/manager/src/utilities/images.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { imageFactory } from 'src/factories/images'; - -import { groupImages } from './images'; - -const [privateImage1, privateImage2] = imageFactory.buildList(2, { - is_public: false, -}); - -const [deprecatedImage1, deprecatedImage2] = imageFactory.buildList(2, { - created_by: 'linode', - deprecated: true, -}); - -const [recommendedImage1, recommendedImage2] = imageFactory.buildList(2, { - created_by: 'linode', - id: 'linode/image', -}); - -const [deletedImage1, deletedImage2] = imageFactory.buildList(2, { - created: '', - created_by: null, - deprecated: false, - description: '', - expiry: '2019-05-09T04:13:37', - is_public: false, - label: '', - size: 0, - type: 'automatic', - vendor: null, -}); - -describe('groupImages method', () => { - it("should return a group of the user's private images", () => { - const result = groupImages([privateImage1, privateImage2]); - - const expected = { - images: [privateImage1, privateImage2], - }; - - expect(result).toEqual(expected); - }); - - it('should return group deprecated images', () => { - const result = groupImages([deprecatedImage1, deprecatedImage2]); - - const expected = { - older: [deprecatedImage1, deprecatedImage2], - }; - - expect(result).toEqual(expected); - }); - - it('should return group recommended images', () => { - const _images = [recommendedImage1, recommendedImage2]; - const result = groupImages(_images); - - const expected = { - recommended: _images, - }; - - expect(result).toEqual(expected); - }); - - it('should return group deleted images', () => { - const result = groupImages([deletedImage1, deletedImage2]); - - const expected = { - deleted: [deletedImage1, deletedImage2], - }; - - expect(result).toEqual(expected); - }); -}); From 5511ff9de92351751cd12a0eadc87d47f5c50543 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 10:20:33 -0400 Subject: [PATCH 11/19] save changes #10 --- .../features/Domains/DomainRecordDrawer.tsx | 41 ++++-- .../DetailTabs/Processes/ProcessesGraphs.tsx | 1 + .../DetailTabs/Processes/ProcessesLanding.tsx | 12 +- .../manager/src/features/Longview/request.ts | 9 +- .../components/PlansPanel/PlansPanel.tsx | 122 +++++++++--------- 5 files changed, 103 insertions(+), 82 deletions(-) diff --git a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx index 2dbf8a17bab..3dd0753072c 100644 --- a/packages/manager/src/features/Domains/DomainRecordDrawer.tsx +++ b/packages/manager/src/features/Domains/DomainRecordDrawer.tsx @@ -272,7 +272,9 @@ export class DomainRecordDrawer extends React.Component< eachOption.value === defaultTo( DomainRecordDrawer.defaultFieldsState(this.props)[field], - this.state.fields[field] + (this.state.fields as EditableDomainFields & EditableRecordFields)[ + field + ] ) ); }); @@ -302,9 +304,10 @@ export class DomainRecordDrawer extends React.Component< multiline?: boolean; }) => { const { domain, type } = this.props; - const value = this.state.fields[field]; + const value = (this.state.fields as EditableDomainFields & + EditableRecordFields)[field]; const hasAliasToResolve = - value.indexOf('@') >= 0 && shouldResolve(type, field); + value && value.indexOf('@') >= 0 && shouldResolve(type, field); return ( ) => this.updateField(field)(e.target.value) } + value={ + (this.state.fields as EditableDomainFields & EditableRecordFields)[ + field + ] as number + } data-qa-target={label} label={label} type="number" - value={this.state.fields[field]} {...rest} /> ); @@ -449,8 +456,12 @@ export class DomainRecordDrawer extends React.Component< this.updateField(field)(e.target.value) } value={defaultTo( - DomainRecordDrawer.defaultFieldsState(this.props)[field], - this.state.fields[field] + DomainRecordDrawer.defaultFieldsState(this.props)[field] as + | number + | string, + (this.state.fields as EditableDomainFields & EditableRecordFields)[ + field + ] as number | string )} data-qa-target={label} helperText={helperText} @@ -468,8 +479,8 @@ export class DomainRecordDrawer extends React.Component< * editable data or defaults. */ static defaultFieldsState = (props: Partial) => ({ - description: '', axfr_ips: getInitialIPs(props.axfr_ips), + description: '', domain: props.domain, expire_sec: props.expire_sec ?? 0, id: props.id, @@ -716,6 +727,12 @@ export class DomainRecordDrawer extends React.Component< }; types = { + A: { + fields: [], + }, + PTR: { + fields: [], + }, AAAA: { fields: [ (idx: number) => ( @@ -727,13 +744,6 @@ export class DomainRecordDrawer extends React.Component< (idx: number) => , ], }, - // slave: { - // fields: [ - // (idx: number) => , - // (idx: number) => , - // (idx: number) => , - // ], - // }, CAA: { fields: [ (idx: number) => ( @@ -843,6 +853,9 @@ export class DomainRecordDrawer extends React.Component< (idx: number) => , ], }, + slave: { + fields: [], + }, }; } diff --git a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesGraphs.tsx b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesGraphs.tsx index f96fdfb2819..bc8ac8949dc 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesGraphs.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesGraphs.tsx @@ -50,6 +50,7 @@ export const ProcessesGraphs = (props: Props) => { const name = selectedProcess?.name ?? ''; const user = selectedProcess?.user ?? ''; + // @ts-expect-error The types are completely wrong. They don't account for "user" const process = processesData.Processes?.[name]?.[user] ?? {}; const cpu = process.cpu ?? []; 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 78d1ee48dd8..24227d3d99b 100644 --- a/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx +++ b/packages/manager/src/features/Longview/LongviewDetail/DetailTabs/Processes/ProcessesLanding.tsx @@ -177,16 +177,18 @@ export const extendData = ( // with a string as the value. Here, we separate these keys. const { longname, ...users } = processesData.Processes![processName]; - Object.keys(users).forEach((user: keyof typeof users) => { + Object.keys(users).forEach((user) => { + // @ts-expect-error The types are completely wrong. They don't account for "user" const userProcess = processesData.Processes![processName][user]; extendedData.push({ - averageCPU: statAverage(users.cpu), + averageCPU: statAverage(userProcess.cpu), averageIO: - statAverage(users.ioreadkbytes) + statAverage(users.iowritekbytes), - averageMem: statAverage(users.mem), + statAverage(userProcess.ioreadkbytes) + + statAverage(userProcess.iowritekbytes), + averageMem: statAverage(userProcess.mem), id: `${processName}-${user}`, - maxCount: statMax(users.count), + maxCount: statMax(userProcess.count), name: processName, user, }); diff --git a/packages/manager/src/features/Longview/request.ts b/packages/manager/src/features/Longview/request.ts index 5832b42f2c9..f8081df5e1f 100644 --- a/packages/manager/src/features/Longview/request.ts +++ b/packages/manager/src/features/Longview/request.ts @@ -91,7 +91,7 @@ export const baseRequest = Axios.create({ }); export const handleLongviewResponse = ( - response: AxiosResponse> + response: AxiosResponse[]> ) => { const notifications = response.data[0].NOTIFICATIONS; /** @@ -108,7 +108,7 @@ export const handleLongviewResponse = ( } }; -export const get: Get = ( +export const get: Get = async ( token: string, action: LongviewAction, options: Partial = {} @@ -131,9 +131,10 @@ export const get: Get = ( if (end) { data.set('end', `${end}`); } - return request({ + const response = await request({ data, - }).then(handleLongviewResponse); + }); + return handleLongviewResponse(response); }; export const getLastUpdated = (token: string) => { diff --git a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx index a4b0cf0ecbd..f1e8f4742bc 100644 --- a/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx +++ b/packages/manager/src/features/components/PlansPanel/PlansPanel.tsx @@ -129,67 +129,71 @@ export const PlansPanel = (props: PlansPanelProps) => { selectedRegionID, }); - const tabs = Object.keys(plans).map((plan: LinodeTypeClass) => { - const plansMap: PlanSelectionType[] = plans[plan]; - const { - allDisabledPlans, - hasMajorityOfPlansDisabled, - plansForThisLinodeTypeClass, - } = extractPlansInformation({ - disableLargestGbPlansFlag: flags.disableLargestGbPlans, - disabledClasses, - disabledSmallerPlans, - plans: plansMap, - regionAvailabilities, - selectedRegionId: selectedRegionID, - }); - - return { - disabled: props.disabledTabs ? props.disabledTabs?.includes(plan) : false, - render: () => { - return ( - <> - ) => { + const plansMap: PlanSelectionType[] = plans[plan]!; + const { + allDisabledPlans, + hasMajorityOfPlansDisabled, + plansForThisLinodeTypeClass, + } = extractPlansInformation({ + disableLargestGbPlansFlag: flags.disableLargestGbPlans, + disabledClasses, + disabledSmallerPlans, + plans: plansMap, + regionAvailabilities, + selectedRegionId: selectedRegionID, + }); + + return { + disabled: props.disabledTabs + ? props.disabledTabs?.includes(plan) + : false, + render: () => { + return ( + <> + + {showDistributedRegionPlanTable && ( + )} - disabledClasses={disabledClasses} - hasMajorityOfPlansDisabled={hasMajorityOfPlansDisabled} - hasSelectedRegion={hasSelectedRegion} - planType={plan} - regionsData={regionsData || []} - /> - {showDistributedRegionPlanTable && ( - - )} - - - ); - }, - title: planTabInfoContent[plan === 'standard' ? 'shared' : plan]?.title, - }; - }); + + ); + }, + title: planTabInfoContent[plan === 'edge' ? 'dedicated' : plan]?.title, + }; + } + ); const initialTab = determineInitialPlanCategoryTab( types, From ff20337761e928ee5497357a486db663276f3d51 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 10:32:21 -0400 Subject: [PATCH 12/19] last issue is filtering --- .../KubernetesPlansPanel.tsx | 88 ++++++++++--------- .../StackScriptCreate/StackScriptCreate.tsx | 1 - .../FieldTypes/UserDefinedMultiSelect.tsx | 6 +- 3 files changed, 48 insertions(+), 47 deletions(-) diff --git a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx index 2dc8bc961ae..1605532560b 100644 --- a/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx +++ b/packages/manager/src/features/Kubernetes/KubernetesPlansPanel/KubernetesPlansPanel.tsx @@ -78,50 +78,52 @@ export const KubernetesPlansPanel = (props: Props) => { : _types ); - const tabs = Object.keys(plans).map((plan: LinodeTypeClass) => { - const plansMap: PlanSelectionType[] = plans[plan]; - const { - allDisabledPlans, - hasMajorityOfPlansDisabled, - plansForThisLinodeTypeClass, - } = extractPlansInformation({ - disableLargestGbPlansFlag: flags.disableLargestGbPlans, - plans: plansMap, - regionAvailabilities, - selectedRegionId, - }); + const tabs = Object.keys(plans).map( + (plan: Exclude) => { + const plansMap: PlanSelectionType[] = plans[plan]!; + const { + allDisabledPlans, + hasMajorityOfPlansDisabled, + plansForThisLinodeTypeClass, + } = extractPlansInformation({ + disableLargestGbPlansFlag: flags.disableLargestGbPlans, + plans: plansMap, + regionAvailabilities, + selectedRegionId, + }); - return { - render: () => { - return ( - <> - - - - ); - }, - title: planTabInfoContent[plan === 'standard' ? 'shared' : plan]?.title, - }; - }); + return { + render: () => { + return ( + <> + + + + ); + }, + title: planTabInfoContent[plan === 'edge' ? 'dedicated' : plan]?.title, + }; + } + ); const initialTab = determineInitialPlanCategoryTab( types, diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx index 57909307270..05d3c515b55 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.tsx @@ -133,7 +133,6 @@ export class StackScriptCreate extends React.Component { const imageList = images.map((image) => image.id); const anyAllOptionChosen = imageList.includes('any/all'); - console.log(anyAllOptionChosen); this.setState( { diff --git a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx index 42720ec7506..64d230bbc87 100644 --- a/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx +++ b/packages/manager/src/features/StackScripts/UserDefinedFieldsPanel/FieldTypes/UserDefinedMultiSelect.tsx @@ -5,7 +5,7 @@ import { Notice } from 'src/components/Notice/Notice'; import { RenderGuard } from 'src/components/RenderGuard'; import type { UserDefinedField } from '@linode/api-v4/lib/stackscripts'; -import type { SelectImageOption } from 'src/features/Images/ImageSelect'; +import type { Item } from 'src/components/EnhancedSelect'; interface Props { error?: string; @@ -27,11 +27,11 @@ interface State { } class UserDefinedMultiSelect extends React.Component { - handleSelectManyOf = (selectedOptions: SelectImageOption[]) => { + handleSelectManyOf = (selectedOptions: Item[]) => { const { field, updateFormState } = this.props; const arrayToString = Array.prototype.map - .call(selectedOptions, (opt: SelectImageOption) => opt.value) + .call(selectedOptions, (opt: Item) => opt.value) .toString(); updateFormState(field.name, arrayToString); From bd82d8f0eddd1d23eb488badc31e9ce25e0635ed Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 12:04:51 -0400 Subject: [PATCH 13/19] fix unit test by fixing button types --- packages/manager/src/components/Button/Button.tsx | 6 +++++- .../src/components/InlineMenuAction/InlineMenuAction.tsx | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/components/Button/Button.tsx b/packages/manager/src/components/Button/Button.tsx index 79ebcc92dcc..db54b5cec07 100644 --- a/packages/manager/src/components/Button/Button.tsx +++ b/packages/manager/src/components/Button/Button.tsx @@ -30,6 +30,10 @@ export interface ButtonProps extends _ButtonProps { * @default false */ compactY?: boolean; + /** + * Optional test ID + */ + 'data-testid'?: string; /** * Show a loading indicator * @default false @@ -151,7 +155,7 @@ export const Button = React.forwardRef( color={color} compactX={compactX} compactY={compactY} - data-testid="Button" + data-testid={rest['data-testid'] || 'Button'} disableRipple={disabled} disabled={loading} loading={loading} diff --git a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx index c8e0b884fe4..1f91db3d338 100644 --- a/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx +++ b/packages/manager/src/components/InlineMenuAction/InlineMenuAction.tsx @@ -8,10 +8,14 @@ import { StyledActionButton } from 'src/components/Button/StyledActionButton'; interface InlineMenuActionProps { /** Required action text */ actionText: string; + /** Optional aria label */ + 'aria-label'?: string; /** Optional height when displayed as a button */ buttonHeight?: number; /** Optional class names */ className?: string; + /** Optional test id */ + 'data-testid'?: string; /** Optional disabled */ disabled?: boolean; /** Optional href */ @@ -37,6 +41,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { onClick, tooltip, tooltipAnalyticsEvent, + ...rest } = props; if (href) { @@ -57,6 +62,7 @@ export const InlineMenuAction = (props: InlineMenuActionProps) => { sx={buttonHeight !== undefined ? { height: buttonHeight } : {}} tooltipAnalyticsEvent={tooltipAnalyticsEvent} tooltipText={tooltip} + {...rest} > {actionText} From 7829fe297ff00dd0468fbab60e41b67b2adf88be Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 13:13:26 -0400 Subject: [PATCH 14/19] finally resolved all tsc errors --- .../account/third-party-access-tokens.spec.ts | 2 +- .../e2e/core/account/user-permissions.spec.ts | 1 + .../e2e/core/firewalls/update-firewall.spec.ts | 2 +- .../support/plugins/test-tagging-info.ts | 2 +- .../setup/page-visit-tracking-commands.ts | 1 + .../cypress/support/util/feature-flags.ts | 17 +++++++++-------- .../Account/SwitchAccounts/ChildAccountList.tsx | 4 +--- .../src/features/Events/EventsLanding.tsx | 4 +++- .../Images/ImagesLanding/ImagesLanding.tsx | 16 +++++----------- .../PlacementGroupsLanding.tsx | 9 +++------ .../StackScriptBase/StackScriptBase.tsx | 2 +- .../StackScriptCreate.test.tsx | 2 +- .../features/StackScripts/stackScriptUtils.ts | 2 +- .../src/features/Volumes/VolumesLanding.tsx | 11 +++++------ .../manager/src/queries/events/event.helpers.ts | 4 ++-- 15 files changed, 36 insertions(+), 43 deletions(-) diff --git a/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts b/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts index 9e918bf9dcd..0f020d73aa0 100644 --- a/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts +++ b/packages/manager/cypress/e2e/core/account/third-party-access-tokens.spec.ts @@ -66,7 +66,7 @@ describe('Third party access tokens', () => { .findByTitle(token.label) .should('be.visible') .within(() => { - Object.keys(access).forEach((key) => { + Object.keys(access).forEach((key: keyof typeof access) => { cy.findByText(key) .closest('tr') .within(() => { diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index d8f21fa1366..53be928aded 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -281,6 +281,7 @@ describe('User permission management', () => { cancel_account: true, child_account_access: true, add_domains: true, + add_databases: true, add_firewalls: true, add_images: true, add_linodes: true, 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 85520e1432e..d67ac52fd3a 100644 --- a/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts +++ b/packages/manager/cypress/e2e/core/firewalls/update-firewall.spec.ts @@ -78,7 +78,7 @@ const addFirewallRules = (rule: FirewallRuleType, direction: string) => { .within(() => { const port = rule.ports ? rule.ports : '22'; cy.findByPlaceholderText('Select a rule preset...').type( - portPresetMap[port] + '{enter}' + portPresetMap[port as keyof typeof portPresetMap] + '{enter}' ); const label = rule.label ? rule.label : 'test-label'; const description = rule.description diff --git a/packages/manager/cypress/support/plugins/test-tagging-info.ts b/packages/manager/cypress/support/plugins/test-tagging-info.ts index 47aa97cd462..d44af4245ab 100644 --- a/packages/manager/cypress/support/plugins/test-tagging-info.ts +++ b/packages/manager/cypress/support/plugins/test-tagging-info.ts @@ -23,7 +23,7 @@ export const logTestTagInfo: CypressPlugin = (_on, config) => { console.table( getHumanReadableQueryRules(query).reduce( - (acc: {}, cur: string, index: number) => { + (acc: any, cur: string, index: number) => { acc[index] = cur; return acc; }, diff --git a/packages/manager/cypress/support/setup/page-visit-tracking-commands.ts b/packages/manager/cypress/support/setup/page-visit-tracking-commands.ts index 7c03d1751df..cb6275164e7 100644 --- a/packages/manager/cypress/support/setup/page-visit-tracking-commands.ts +++ b/packages/manager/cypress/support/setup/page-visit-tracking-commands.ts @@ -11,6 +11,7 @@ Cypress.Commands.add('trackPageVisit', { prevSubject: false }, () => { const pageLoadId = randomNumber(100000, 999999); cy.window({ log: false }).then((window) => { + // @ts-expect-error not in the cypress type window['cypress-visit-id'] = pageLoadId; }); diff --git a/packages/manager/cypress/support/util/feature-flags.ts b/packages/manager/cypress/support/util/feature-flags.ts index 8d09514edec..10bc7f0c651 100644 --- a/packages/manager/cypress/support/util/feature-flags.ts +++ b/packages/manager/cypress/support/util/feature-flags.ts @@ -72,20 +72,21 @@ export const isPartialFeatureFlagData = ( * @returns Feature flag response data that can be used for mocking purposes. */ export const getResponseDataFromMockData = (data: FeatureFlagMockData) => { - const output = { ...data }; - return Object.keys(output).reduce((acc: FeatureFlagMockData, cur: string) => { - const mockData = output[cur]; + return Object.keys(data).reduce< + Partial>> + >((acc, cur: keyof FeatureFlagMockData) => { + const mockData = acc[cur]; if (isPartialFeatureFlagData(mockData)) { - output[cur] = { + acc[cur] = { ...defaultFeatureFlagData, ...mockData, }; - return output; + return acc; } else { - output[cur] = makeFeatureFlagData(mockData); + acc[cur] = makeFeatureFlagData(mockData); } - return output; - }, output); + return acc; + }, {}); }; /** diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx index 0e2242a2c18..145799f445b 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx @@ -40,10 +40,8 @@ export const ChildAccountList = React.memo( const filter: Filter = { ['+order']: 'asc', ['+order_by']: 'company', + ...(searchQuery && { company: { '+contains': searchQuery } }), }; - if (searchQuery) { - filter['company'] = { '+contains': searchQuery }; - } const [ isSwitchingChildAccounts, diff --git a/packages/manager/src/features/Events/EventsLanding.tsx b/packages/manager/src/features/Events/EventsLanding.tsx index 0caf895222f..68cf12add5d 100644 --- a/packages/manager/src/features/Events/EventsLanding.tsx +++ b/packages/manager/src/features/Events/EventsLanding.tsx @@ -23,6 +23,8 @@ import { StyledTypography, } from './EventsLanding.styles'; +import type { Filter } from '@linode/api-v4'; + interface Props { emptyMessage?: string; // Custom message for the empty state (i.e. no events). entityId?: number; @@ -32,7 +34,7 @@ export const EventsLanding = (props: Props) => { const { emptyMessage, entityId } = props; const flags = useFlags(); - const filter = { ...EVENTS_LIST_FILTER }; + const filter: Filter = { ...EVENTS_LIST_FILTER }; if (entityId) { filter['entity.id'] = entityId; diff --git a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx index 8f02bf1927b..dcf39b8151d 100644 --- a/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx +++ b/packages/manager/src/features/Images/ImagesLanding/ImagesLanding.tsx @@ -53,7 +53,7 @@ import { ImagesLandingEmptyState } from './ImagesLandingEmptyState'; import { RebuildImageDrawer } from './RebuildImageDrawer'; import type { Handlers as ImageHandlers } from './ImagesActionMenu'; -import type { ImageStatus } from '@linode/api-v4'; +import type { Filter, ImageStatus } from '@linode/api-v4'; import type { Theme } from '@mui/material/styles'; const searchQueryKey = 'query'; @@ -118,15 +118,12 @@ export const ImagesLanding = () => { 'manual' ); - const manualImagesFilter = { + const manualImagesFilter: Filter = { ['+order']: manualImagesOrder, ['+order_by']: manualImagesOrderBy, + ...(imageLabelFromParam && { label: { '+contains': imageLabelFromParam } }), }; - if (imageLabelFromParam) { - manualImagesFilter['label'] = { '+contains': imageLabelFromParam }; - } - const { data: manualImages, error: manualImagesError, @@ -170,15 +167,12 @@ export const ImagesLanding = () => { 'automatic' ); - const automaticImagesFilter = { + const automaticImagesFilter: Filter = { ['+order']: automaticImagesOrder, ['+order_by']: automaticImagesOrderBy, + ...(imageLabelFromParam && { label: { '+contains': imageLabelFromParam } }), }; - if (imageLabelFromParam) { - automaticImagesFilter['label'] = { '+contains': imageLabelFromParam }; - } - const { data: automaticImages, error: automaticImagesError, diff --git a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx index 92d7d75778d..8b6dfbdf6ba 100644 --- a/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx +++ b/packages/manager/src/features/PlacementGroups/PlacementGroupsLanding/PlacementGroupsLanding.tsx @@ -36,7 +36,7 @@ import { getPlacementGroupLinodes } from '../utils'; import { PlacementGroupsLandingEmptyState } from './PlacementGroupsLandingEmptyState'; import { PlacementGroupsRow } from './PlacementGroupsRow'; -import type { PlacementGroup } from '@linode/api-v4'; +import type { Filter, PlacementGroup } from '@linode/api-v4'; const preferenceKey = 'placement-groups'; @@ -55,15 +55,12 @@ export const PlacementGroupsLanding = React.memo(() => { `${preferenceKey}-order` ); - const filter = { + const filter: Filter = { ['+order']: order, ['+order_by']: orderBy, + ...(query && { label: { '+contains': query } }), }; - if (query) { - filter['label'] = { '+contains': query }; - } - const { data: placementGroups, error, diff --git a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx index 4d183833047..71163057cff 100644 --- a/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptBase/StackScriptBase.tsx @@ -543,7 +543,7 @@ const withStackScriptBase = (options: WithStackScriptBaseOptions) => ( } else { this.setState({ allStackScriptsLoaded: false, - currentSearchFilter: [], + currentSearchFilter: {}, }); } }) diff --git a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.test.tsx b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.test.tsx index 42c28894a10..c1978cd7c30 100644 --- a/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.test.tsx +++ b/packages/manager/src/features/StackScripts/StackScriptCreate/StackScriptCreate.test.tsx @@ -39,7 +39,7 @@ describe('StackScriptCreate', () => { expect(getByLabelText('StackScript Label (required)')).toBeVisible(); expect(getByLabelText('Description')).toBeVisible(); - expect(getByLabelText('Target Images')).toBeVisible(); + expect(getByLabelText('Target Images (required)')).toBeVisible(); expect(getByLabelText('Script (required)')).toBeVisible(); expect(getByLabelText('Revision Note')).toBeVisible(); diff --git a/packages/manager/src/features/StackScripts/stackScriptUtils.ts b/packages/manager/src/features/StackScripts/stackScriptUtils.ts index 144645ead73..ca14507c552 100644 --- a/packages/manager/src/features/StackScripts/stackScriptUtils.ts +++ b/packages/manager/src/features/StackScripts/stackScriptUtils.ts @@ -23,8 +23,8 @@ const oneClickFilter = [ }, }, ], + '+order_by': 'ordinal', }, - { '+order_by': 'ordinal' }, ]; export const getOneClickApps = (params?: Params) => diff --git a/packages/manager/src/features/Volumes/VolumesLanding.tsx b/packages/manager/src/features/Volumes/VolumesLanding.tsx index 939107840ef..2f08ed2c211 100644 --- a/packages/manager/src/features/Volumes/VolumesLanding.tsx +++ b/packages/manager/src/features/Volumes/VolumesLanding.tsx @@ -36,7 +36,7 @@ import { VolumeDetailsDrawer } from './VolumeDetailsDrawer'; import { VolumesLandingEmptyState } from './VolumesLandingEmptyState'; import { VolumeTableRow } from './VolumeTableRow'; -import type { Volume } from '@linode/api-v4'; +import type { Filter, Volume } from '@linode/api-v4'; const preferenceKey = 'volumes'; const searchQueryKey = 'query'; @@ -59,15 +59,14 @@ export const VolumesLanding = () => { `${preferenceKey}-order` ); - const filter = { + const filter: Filter = { ['+order']: order, ['+order_by']: orderBy, + ...(volumeLabelFromParam && { + label: { '+contains': volumeLabelFromParam }, + }), }; - if (volumeLabelFromParam) { - filter['label'] = { '+contains': volumeLabelFromParam }; - } - const { data: volumes, error, isFetching, isLoading } = useVolumesQuery( { page: pagination.page, diff --git a/packages/manager/src/queries/events/event.helpers.ts b/packages/manager/src/queries/events/event.helpers.ts index 2fbf4ef2670..5d3b500a99e 100644 --- a/packages/manager/src/queries/events/event.helpers.ts +++ b/packages/manager/src/queries/events/event.helpers.ts @@ -63,12 +63,12 @@ export const doesEventMatchAPIFilter = (event: Event, filter: Filter) => { return false; } - // @ts-expect-error todo fix filter type + // @ts-expect-error todo improve indexability of filter type if (filter?.['entity.id'] && filter['entity.id'] !== event.entity?.id) { return false; } - // @ts-expect-error todo fix filter type + // @ts-expect-error todo improve indexability of filter type if (filter?.['entity.type'] && filter['entity.type'] !== event.entity?.type) { return false; } From 798781287d67a63e7341a1ba7aabb93f3cc7d682 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 13:36:18 -0400 Subject: [PATCH 15/19] Added changeset: Remove `suppressImplicitAnyIndexErrors` and `ignoreDeprecations` Typescript Options --- .../.changeset/pr-10755-tech-stories-1722965778724.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-10755-tech-stories-1722965778724.md diff --git a/packages/manager/.changeset/pr-10755-tech-stories-1722965778724.md b/packages/manager/.changeset/pr-10755-tech-stories-1722965778724.md new file mode 100644 index 00000000000..49e4db22435 --- /dev/null +++ b/packages/manager/.changeset/pr-10755-tech-stories-1722965778724.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +Remove `suppressImplicitAnyIndexErrors` and `ignoreDeprecations` Typescript Options ([#10755](https://github.com/linode/manager/pull/10755)) From 0bcdae06c6fc2abc4e3a7d1cc451b91f4c257fc7 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 15:46:11 -0400 Subject: [PATCH 16/19] fix cypress feature flag util hopefully --- .../manager/cypress/support/constants/user-permissions.ts | 1 + packages/manager/cypress/support/util/feature-flags.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/manager/cypress/support/constants/user-permissions.ts b/packages/manager/cypress/support/constants/user-permissions.ts index d6ca785951d..462f1033fa7 100644 --- a/packages/manager/cypress/support/constants/user-permissions.ts +++ b/packages/manager/cypress/support/constants/user-permissions.ts @@ -17,6 +17,7 @@ export const userPermissionsGrants: Grants = grantsFactory.build({ add_longview: false, add_nodebalancers: false, add_stackscripts: false, + add_databases: false, add_volumes: false, add_vpcs: false, longview_subscription: false, diff --git a/packages/manager/cypress/support/util/feature-flags.ts b/packages/manager/cypress/support/util/feature-flags.ts index 10bc7f0c651..6056f3e2888 100644 --- a/packages/manager/cypress/support/util/feature-flags.ts +++ b/packages/manager/cypress/support/util/feature-flags.ts @@ -73,7 +73,7 @@ export const isPartialFeatureFlagData = ( */ export const getResponseDataFromMockData = (data: FeatureFlagMockData) => { return Object.keys(data).reduce< - Partial>> + Partial>> >((acc, cur: keyof FeatureFlagMockData) => { const mockData = acc[cur]; if (isPartialFeatureFlagData(mockData)) { @@ -81,9 +81,8 @@ export const getResponseDataFromMockData = (data: FeatureFlagMockData) => { ...defaultFeatureFlagData, ...mockData, }; - return acc; - } else { - acc[cur] = makeFeatureFlagData(mockData); + } else if (mockData) { + acc[cur] = makeFeatureFlagData(mockData); } return acc; }, {}); From 171875965cf23cbac3de254903413b4d3b5a1320 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 16:48:01 -0400 Subject: [PATCH 17/19] another attemnpt to fix cypress feature flag util function --- .../cypress/support/util/feature-flags.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/manager/cypress/support/util/feature-flags.ts b/packages/manager/cypress/support/util/feature-flags.ts index 6056f3e2888..a86183e2c7d 100644 --- a/packages/manager/cypress/support/util/feature-flags.ts +++ b/packages/manager/cypress/support/util/feature-flags.ts @@ -72,20 +72,22 @@ export const isPartialFeatureFlagData = ( * @returns Feature flag response data that can be used for mocking purposes. */ export const getResponseDataFromMockData = (data: FeatureFlagMockData) => { - return Object.keys(data).reduce< - Partial>> - >((acc, cur: keyof FeatureFlagMockData) => { - const mockData = acc[cur]; - if (isPartialFeatureFlagData(mockData)) { - acc[cur] = { - ...defaultFeatureFlagData, - ...mockData, - }; - } else if (mockData) { - acc[cur] = makeFeatureFlagData(mockData); - } - return acc; - }, {}); + return Object.keys(data).reduce>>( + (output, cur: keyof FeatureFlagMockData) => { + const mockData = output[cur]; + if (isPartialFeatureFlagData(mockData)) { + output[cur] = { + ...defaultFeatureFlagData, + ...mockData, + }; + return output; + } else { + output[cur] = makeFeatureFlagData(mockData); + } + return output; + }, + data as Record> + ); }; /** From 7b9b7ee027b3673eeaa47d4b42c81e2e703a1bf6 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Tue, 6 Aug 2024 18:14:43 -0400 Subject: [PATCH 18/19] make image select close onSelect to match previous behavior --- .../cypress/e2e/core/stackscripts/update-stackscripts.spec.ts | 4 ++-- packages/manager/src/features/Images/ImageSelect.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts b/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts index 94a3ffa582d..40ca71afe7a 100644 --- a/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts +++ b/packages/manager/cypress/e2e/core/stackscripts/update-stackscripts.spec.ts @@ -55,9 +55,9 @@ const fillOutStackscriptForm = ( .type(description); } - cy.findByText('Target Images').click().type(`${targetImage}`); + cy.findByText('Target Images').click().type(targetImage); - cy.findByText(`${targetImage}`).should('be.visible').click(); + cy.findByText(targetImage).should('be.visible').click(); // Insert a script with invalid UDF data. cy.get('[data-qa-textfield-label="Script"]') diff --git a/packages/manager/src/features/Images/ImageSelect.tsx b/packages/manager/src/features/Images/ImageSelect.tsx index 024decfa6d4..6bbd7435f7a 100644 --- a/packages/manager/src/features/Images/ImageSelect.tsx +++ b/packages/manager/src/features/Images/ImageSelect.tsx @@ -83,6 +83,7 @@ export const ImageSelect = (props: MultiProps | Props) => { ? options.filter((o) => value?.includes(o.id)) ?? [] : options.find((o) => o.id === value) ?? null } + disableCloseOnSelect={false} disableSelectAll disabled={disabled} errorText={errorText} From 9aaa7463404b2592b116eb255cdd2ae3e7f4f2e5 Mon Sep 17 00:00:00 2001 From: Banks Nussman Date: Wed, 7 Aug 2024 09:27:02 -0400 Subject: [PATCH 19/19] feedback @jaalah-akamai --- packages/manager/src/components/DownloadCSV/DownloadCSV.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx index 2f41e238845..4a0c5153b61 100644 --- a/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx +++ b/packages/manager/src/components/DownloadCSV/DownloadCSV.tsx @@ -86,7 +86,7 @@ export const cleanCSVData = (data: any): any => { /** if it's an object, recursively sanitize each key value pair */ if (typeof data === 'object') { - return Object.keys(data).reduce((acc, eachKey) => { + return Object.keys(data).reduce<{ [key: string]: any }>((acc, eachKey) => { acc[eachKey] = cleanCSVData(data[eachKey]); return acc; }, {});