From 216b2003f935c247cf5d4e0c4cb8d5e55b1582ff Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Wed, 8 May 2024 10:20:30 -0400 Subject: [PATCH 01/17] Set up endpoint, query, and save work --- packages/api-v4/src/object-storage/index.ts | 2 ++ packages/api-v4/src/object-storage/prices.ts | 17 +++++++++++++ .../BucketLanding/OveragePricing.tsx | 25 ++++++++++++++++--- packages/manager/src/queries/objectStorage.ts | 22 +++++++++++++++- .../src/utilities/pricing/dynamicPricing.ts | 8 +++++- 5 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 packages/api-v4/src/object-storage/prices.ts diff --git a/packages/api-v4/src/object-storage/index.ts b/packages/api-v4/src/object-storage/index.ts index f4a9bdf8d18..e2985222d3f 100644 --- a/packages/api-v4/src/object-storage/index.ts +++ b/packages/api-v4/src/object-storage/index.ts @@ -9,3 +9,5 @@ export * from './objects'; export * from './objectStorageKeys'; export * from './types'; + +export * from './prices'; diff --git a/packages/api-v4/src/object-storage/prices.ts b/packages/api-v4/src/object-storage/prices.ts new file mode 100644 index 00000000000..7a5747b3ba9 --- /dev/null +++ b/packages/api-v4/src/object-storage/prices.ts @@ -0,0 +1,17 @@ +import { Params, PriceType, ResourcePage } from 'src/types'; +import { API_ROOT } from '../constants'; +import Request, { setMethod, setParams, setURL } from '../request'; + +// TODO: decide the best place to put this. +/** + * getObjectStorageTypes + * + * Return a paginated list of available Object Storage types; used for pricing. + * This endpoint does not require authentication. + */ +export const getObjectStorageTypes = (params?: Params) => + Request>( + setURL(`${API_ROOT}/object-storage/types`), + setMethod('GET'), + setParams(params) + ); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 25279f3efc9..78f27e1569e 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -4,8 +4,12 @@ import React from 'react'; import { TextTooltip } from 'src/components/TextTooltip'; import { Typography } from 'src/components/Typography'; +import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; -import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; +import { + getDCSpecificPriceByType, + objectStoragePriceIncreaseMap, +} from 'src/utilities/pricing/dynamicPricing'; interface Props { regionId: Region['id']; @@ -18,6 +22,11 @@ export const GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT = export const OveragePricing = (props: Props) => { const { regionId } = props; + + const { data: types /*, isError, isLoading*/ } = useObjectStorageTypesQuery(); + + const overageType = types?.find((type) => type.id.includes('overage')); + const isDcSpecificPricingRegion = objectStoragePriceIncreaseMap.hasOwnProperty( regionId ); @@ -28,9 +37,14 @@ export const OveragePricing = (props: Props) => { For this region, additional storage costs{' '} $ - {isDcSpecificPricingRegion + {/* {isDcSpecificPricingRegion ? objectStoragePriceIncreaseMap[regionId].storage_overage - : OBJ_STORAGE_PRICE.storage_overage}{' '} + : OBJ_STORAGE_PRICE.storage_overage}{' '} */} + {getDCSpecificPriceByType({ + regionId, + timePeriod: 'hourly', + type: overageType, + })}{' '} per GB . @@ -42,6 +56,11 @@ export const OveragePricing = (props: Props) => { {isDcSpecificPricingRegion ? objectStoragePriceIncreaseMap[regionId].transfer_overage : OBJ_STORAGE_PRICE.transfer_overage}{' '} + {/* {getDCSpecificPriceByType({ + regionId, + timePeriod: 'hourly', + type: overageType, + })}{' '} */} per GB {' '} if it exceeds{' '} diff --git a/packages/manager/src/queries/objectStorage.ts b/packages/manager/src/queries/objectStorage.ts index 052a81d7989..f8fceeae9e7 100644 --- a/packages/manager/src/queries/objectStorage.ts +++ b/packages/manager/src/queries/objectStorage.ts @@ -20,11 +20,17 @@ import { getClusters, getObjectList, getObjectStorageKeys, + getObjectStorageTypes, getObjectURL, getSSLCert, uploadSSLCert, } from '@linode/api-v4'; -import { APIError, Params, ResourcePage } from '@linode/api-v4/lib/types'; +import { + APIError, + Params, + PriceType, + ResourcePage, +} from '@linode/api-v4/lib/types'; import { QueryClient, useInfiniteQuery, @@ -406,3 +412,17 @@ export const useBucketSSLDeleteMutation = (cluster: string, bucket: string) => { }, }); }; + +const getAllObjectStorageTypes = () => + getAll((params) => getObjectStorageTypes(params))().then( + (data) => data.data + ); + +export const useObjectStorageTypesQuery = () => + useQuery( + [`${queryKey}-types`], + getAllObjectStorageTypes, + { + ...queryPresets.oneTimeFetch, + } + ); diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.ts b/packages/manager/src/utilities/pricing/dynamicPricing.ts index 90598a37e6f..e52ee4e159c 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.ts @@ -2,6 +2,7 @@ import { UNKNOWN_PRICE } from './constants'; import type { PriceType, Region, RegionPriceObject } from '@linode/api-v4'; +type TimePeriod = 'hourly' | 'monthly'; export interface RegionPrice extends RegionPriceObject { id: string; } @@ -31,6 +32,10 @@ export interface DataCenterPricingByTypeOptions { * @example 20 (GB) for a volume */ size?: number; + /** + * The time period for which to find pricing data for (hourly or monthly). + */ + timePeriod?: TimePeriod; /** * The type data from a product's /types endpoint. */ @@ -96,6 +101,7 @@ export const getDCSpecificPrice = ({ export const getDCSpecificPriceByType = ({ regionId, size, + timePeriod = 'monthly', type, }: DataCenterPricingByTypeOptions): string | undefined => { if (!regionId || !type) { @@ -106,7 +112,7 @@ export const getDCSpecificPriceByType = ({ const price = type.region_prices.find((region_price: RegionPrice) => { return region_price.id === regionId; - })?.monthly ?? type.price.monthly; + })?.[timePeriod] ?? type.price?.[timePeriod]; // If pricing is determined by size of the entity if (size && price) { From 78d8142ed590417d5240c09e18da0f1e399ce00f Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 9 May 2024 14:34:52 -0400 Subject: [PATCH 02/17] Add loading and error state for Create Bucket button --- .../BucketLanding/CreateBucketDrawer.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 9b4997e76ed..4eb87584d83 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -19,6 +19,7 @@ import { useCreateBucketMutation, useObjectStorageBuckets, useObjectStorageClusters, + useObjectStorageTypesQuery, } from 'src/queries/objectStorage'; import { useProfile } from 'src/queries/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -30,6 +31,7 @@ import { getGDPRDetails } from 'src/utilities/formatRegion'; import { EnableObjectStorageModal } from '../EnableObjectStorageModal'; import ClusterSelect from './ClusterSelect'; import { OveragePricing } from './OveragePricing'; +import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; interface Props { isOpen: boolean; @@ -74,6 +76,14 @@ export const CreateBucketDrawer = (props: Props) => { : undefined, }); + const { + data: types, + isError: isErrorTypes, + isLoading: isLoadingTypes, + } = useObjectStorageTypesQuery(); + + const isInvalidPrice = !types || isErrorTypes; + const { error, isLoading, @@ -199,9 +209,15 @@ export const CreateBucketDrawer = (props: Props) => { 'data-testid': 'create-bucket-button', disabled: !formik.values.cluster || - (showGDPRCheckbox && !hasSignedAgreement), + (showGDPRCheckbox && !hasSignedAgreement) || + isErrorTypes, label: 'Create Bucket', - loading: isLoading, + loading: + isLoading || Boolean(clusterRegion?.[0]?.id && isLoadingTypes), + tooltipText: + !isLoadingTypes && isInvalidPrice + ? PRICES_RELOAD_ERROR_NOTICE_TEXT + : '', type: 'submit', }} secondaryButtonProps={{ label: 'Cancel', onClick: onClose }} From fa8e2433804ec42bc78295f67c1e23b37e62565b Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 9 May 2024 15:40:53 -0400 Subject: [PATCH 03/17] Add dynamic pricing to modal; add loading/error state to drawer --- .../features/Account/EnableObjectStorage.tsx | 2 +- .../BucketLanding/OveragePricing.tsx | 45 +++++++++++-------- .../EnableObjectStorageModal.tsx | 30 ++++++++++++- .../src/utilities/pricing/dynamicPricing.ts | 17 ++++--- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/packages/manager/src/features/Account/EnableObjectStorage.tsx b/packages/manager/src/features/Account/EnableObjectStorage.tsx index 51e74662cf0..f6562865e36 100644 --- a/packages/manager/src/features/Account/EnableObjectStorage.tsx +++ b/packages/manager/src/features/Account/EnableObjectStorage.tsx @@ -2,8 +2,8 @@ import { AccountSettings } from '@linode/api-v4/lib/account'; import { cancelObjectStorage } from '@linode/api-v4/lib/object-storage'; import { APIError } from '@linode/api-v4/lib/types'; import Grid from '@mui/material/Unstable_Grid2'; -import * as React from 'react'; import { useQueryClient } from '@tanstack/react-query'; +import * as React from 'react'; import { Accordion } from 'src/components/Accordion'; import { Button } from 'src/components/Button/Button'; diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 78f27e1569e..d52628ddd64 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -2,10 +2,14 @@ import { Region } from '@linode/api-v4'; import { styled } from '@mui/material/styles'; import React from 'react'; +import { CircularProgress } from 'src/components/CircularProgress'; import { TextTooltip } from 'src/components/TextTooltip'; import { Typography } from 'src/components/Typography'; import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { + OBJ_STORAGE_PRICE, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; import { getDCSpecificPriceByType, objectStoragePriceIncreaseMap, @@ -23,32 +27,37 @@ export const GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT = export const OveragePricing = (props: Props) => { const { regionId } = props; - const { data: types /*, isError, isLoading*/ } = useObjectStorageTypesQuery(); + const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); const overageType = types?.find((type) => type.id.includes('overage')); + const storageOveragePrice = getDCSpecificPriceByType({ + interval: 'hourly', + regionId, + type: overageType, + }); + const isDcSpecificPricingRegion = objectStoragePriceIncreaseMap.hasOwnProperty( regionId ); return ( <> - - For this region, additional storage costs{' '} - - $ - {/* {isDcSpecificPricingRegion - ? objectStoragePriceIncreaseMap[regionId].storage_overage - : OBJ_STORAGE_PRICE.storage_overage}{' '} */} - {getDCSpecificPriceByType({ - regionId, - timePeriod: 'hourly', - type: overageType, - })}{' '} - per GB - - . - + {isLoading ? ( + + ) : ( + + For this region, additional storage costs{' '} + + $ + {storageOveragePrice && !isError + ? storageOveragePrice + : UNKNOWN_PRICE}{' '} + per GB + + . + + )} Outbound transfer will cost{' '} diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx index 8e990d54e31..a992c335db0 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx @@ -7,7 +7,12 @@ import { ConfirmationDialog } from 'src/components/ConfirmationDialog/Confirmati import { Link } from 'src/components/Link'; import { Notice } from 'src/components/Notice/Notice'; import { Typography } from 'src/components/Typography'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; +import { + PRICES_RELOAD_ERROR_NOTICE_TEXT, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; +import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; export const OBJ_STORAGE_STORAGE_AMT = '250 GB'; export const OBJ_STORAGE_NETWORK_TRANSFER_AMT = '1 TB'; @@ -22,17 +27,38 @@ export const EnableObjectStorageModal = React.memo( (props: EnableObjectStorageProps) => { const { handleSubmit, onClose, open, regionId } = props; + const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); + + const isInvalidPrice = !types || isError; + + const objectStorageType = types?.find( + (type) => type.id === 'objectstorage' + ); + + const price = regionId + ? getDCSpecificPriceByType({ + regionId, + type: objectStorageType, + }) + : objectStorageType?.price.monthly?.toFixed(2); + return ( ( { onClose(); handleSubmit(); }, + tooltipText: + !isLoading && isInvalidPrice + ? PRICES_RELOAD_ERROR_NOTICE_TEXT + : '', }} secondaryButtonProps={{ 'data-testid': 'cancel', @@ -51,7 +77,7 @@ export const EnableObjectStorageModal = React.memo( Object Storage costs a flat rate of{' '} - ${OBJ_STORAGE_PRICE.monthly}/month, and includes{' '} + ${price ?? UNKNOWN_PRICE}/month, and includes{' '} {OBJ_STORAGE_STORAGE_AMT} of storage. When you enable Object Storage,{' '} {OBJ_STORAGE_NETWORK_TRANSFER_AMT} of outbound data transfer will be added to your global network transfer pool. diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.ts b/packages/manager/src/utilities/pricing/dynamicPricing.ts index e52ee4e159c..58cd85d3fb1 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.ts @@ -2,7 +2,6 @@ import { UNKNOWN_PRICE } from './constants'; import type { PriceType, Region, RegionPriceObject } from '@linode/api-v4'; -type TimePeriod = 'hourly' | 'monthly'; export interface RegionPrice extends RegionPriceObject { id: string; } @@ -22,6 +21,11 @@ export interface DataCenterPricingOptions { } export interface DataCenterPricingByTypeOptions { + /** + * The time period for which to find pricing data for (hourly or monthly). + * @default monthly + */ + interval?: 'hourly' | 'monthly'; /** * The `id` of the region we intended to get the price for. * @example us-east @@ -32,10 +36,6 @@ export interface DataCenterPricingByTypeOptions { * @example 20 (GB) for a volume */ size?: number; - /** - * The time period for which to find pricing data for (hourly or monthly). - */ - timePeriod?: TimePeriod; /** * The type data from a product's /types endpoint. */ @@ -99,27 +99,26 @@ export const getDCSpecificPrice = ({ * @returns a data center specific price or undefined if this cannot be calculated */ export const getDCSpecificPriceByType = ({ + interval = 'monthly', regionId, size, - timePeriod = 'monthly', type, }: DataCenterPricingByTypeOptions): string | undefined => { if (!regionId || !type) { return undefined; } - // Apply the DC-specific price if it exists; otherwise, use the base price. const price = type.region_prices.find((region_price: RegionPrice) => { return region_price.id === regionId; - })?.[timePeriod] ?? type.price?.[timePeriod]; + })?.[interval] ?? type.price?.[interval]; // If pricing is determined by size of the entity if (size && price) { return (size * price).toFixed(2); } - return price?.toFixed(2) ?? undefined; + return price?.toFixed(interval === 'hourly' ? 3 : 2) ?? undefined; }; export const renderMonthlyPriceToCorrectDecimalPlace = ( From 91c1b2b1faa615b3baa783a37ea7db5084a2eb53 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 9 May 2024 16:01:39 -0400 Subject: [PATCH 04/17] Add dynamicPricing test coverage for interval --- packages/manager/src/factories/types.ts | 45 +++++++++++++++++++ .../utilities/pricing/dynamicPricing.test.ts | 20 +++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/manager/src/factories/types.ts b/packages/manager/src/factories/types.ts index 830f7efdbcd..404b50ad646 100644 --- a/packages/manager/src/factories/types.ts +++ b/packages/manager/src/factories/types.ts @@ -172,3 +172,48 @@ export const volumeTypeFactory = Factory.Sync.makeFactory({ ], transfer: 0, }); + +export const objectStorageTypeFactory = Factory.Sync.makeFactory([ + { + id: 'objectstorage', + label: 'Object Storage', + price: { + hourly: 0.0075, + monthly: 5.0, + }, + region_prices: [ + { + hourly: 0.0075, + id: 'id-cgk', + monthly: 5.0, + }, + { + hourly: 0.0075, + id: 'br-gru', + monthly: 5.0, + }, + ], + transfer: 1000, + }, + { + id: 'objectstorage-overage', + label: 'Object Storage Overage', + price: { + hourly: 0.02, + monthly: null, + }, + region_prices: [ + { + hourly: 0.024, + id: 'id-cgk', + monthly: null, + }, + { + hourly: 0.028, + id: 'br-gru', + monthly: null, + }, + ], + transfer: 0, + }, +]); diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts index 4381a738abc..b3961c19d0a 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts @@ -75,6 +75,26 @@ describe('getDCSpecificPricingByType', () => { ).toBe('14.00'); }); + it('calculates dynamic pricing for a region without an increase on an hourly interval', () => { + expect( + getDCSpecificPriceByType({ + interval: 'hourly', + regionId: 'us-east', + type: mockNodeBalancerType, + }) + ).toBe('0.015'); + }); + + it('calculates dynamic pricing for a region with an increase on an hourly interval', () => { + expect( + getDCSpecificPriceByType({ + interval: 'hourly', + regionId: 'id-cgk', + type: mockNodeBalancerType, + }) + ).toBe('0.018'); + }); + it('calculates dynamic pricing for a volume based on size', () => { expect( getDCSpecificPriceByType({ From d9e27019e62fe06b2d3244487716cbca8c81804f Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Tue, 14 May 2024 12:03:13 -0700 Subject: [PATCH 05/17] Add decimal precision to util args --- .../ObjectStorage/BucketLanding/OveragePricing.tsx | 1 + .../ObjectStorage/EnableObjectStorageModal.tsx | 3 ++- .../manager/src/utilities/pricing/dynamicPricing.ts | 10 ++++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index d52628ddd64..2a51adb06d5 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -32,6 +32,7 @@ export const OveragePricing = (props: Props) => { const overageType = types?.find((type) => type.id.includes('overage')); const storageOveragePrice = getDCSpecificPriceByType({ + decimalPrecision: 3, interval: 'hourly', regionId, type: overageType, diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx index a992c335db0..529d430b81a 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx @@ -37,10 +37,11 @@ export const EnableObjectStorageModal = React.memo( const price = regionId ? getDCSpecificPriceByType({ + decimalPrecision: 0, regionId, type: objectStorageType, }) - : objectStorageType?.price.monthly?.toFixed(2); + : objectStorageType?.price.monthly; return ( Date: Tue, 14 May 2024 12:03:50 -0700 Subject: [PATCH 06/17] Add WIP tests --- .../BucketLanding/OveragePricing.test.tsx | 20 +++++++++++++------ .../EnableObjectStorageModal.test.tsx | 20 +++++++++++++++++++ 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index efc2669ea5a..6b4c7f167eb 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -1,6 +1,7 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; +import { objectStorageTypeFactory } from 'src/factories'; import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -11,22 +12,29 @@ import { OveragePricing, } from './OveragePricing'; -describe('OveragePricing', () => { - it('Renders base overage pricing for a region without price increases', () => { +const mockObjectStorageTypes = objectStorageTypeFactory.build(); +const storageOveragePriceType = mockObjectStorageTypes[1]; + +describe('OveragePricing', async () => { + it.skip('Renders base overage pricing for a region without price increases', () => { const { getByText } = renderWithTheme( ); - getByText(`$${OBJ_STORAGE_PRICE.storage_overage} per GB`, { exact: false }); + getByText(`$${storageOveragePriceType.price.hourly} per GB`, { + exact: false, + }); getByText(`$${OBJ_STORAGE_PRICE.transfer_overage} per GB`, { exact: false, }); }); - it('Renders DC-specific overage pricing for a region with price increases', () => { + it.skip('Renders DC-specific overage pricing for a region with price increases', () => { const { getByText } = renderWithTheme(); getByText( - `$${objectStoragePriceIncreaseMap['br-gru'].storage_overage} per GB`, - { exact: false } + `$${storageOveragePriceType?.region_prices?.['br-gru']?.hourly} per GB`, + { + exact: false, + } ); getByText( `$${objectStoragePriceIncreaseMap['br-gru'].transfer_overage} per GB`, diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx index e36ba887af3..3474219db6d 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx @@ -1,6 +1,7 @@ import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; +import { objectStorageTypeFactory } from 'src/factories'; import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; import { wrapWithTheme } from 'src/utilities/testHelpers'; @@ -23,7 +24,26 @@ const props: EnableObjectStorageProps = { open: true, }; +const queryMocks = vi.hoisted(() => ({ + useObjectStorageTypes: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/objectStorage', async () => { + const actual = await vi.importActual('src/queries/objectStorage'); + return { + ...actual, + useObjectStorageTypesQuery: queryMocks.useObjectStorageTypes, + }; +}); + describe('EnableObjectStorageModal', () => { + beforeEach(() => { + const mockObjectStorageTypes = objectStorageTypeFactory.build(); + queryMocks.useObjectStorageTypes.mockReturnValue({ + data: mockObjectStorageTypes, + }); + }); + it('includes a header', () => { const { getAllByText } = render( wrapWithTheme() From e50416340e87826636f9473d9102032dccc6867f Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 16 May 2024 23:54:08 -0700 Subject: [PATCH 07/17] Add mock endpoint to MSW --- packages/manager/src/factories/types.ts | 43 ++++++++++---------- packages/manager/src/mocks/serverHandlers.ts | 13 ++++++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/factories/types.ts b/packages/manager/src/factories/types.ts index 404b50ad646..3714a92947a 100644 --- a/packages/manager/src/factories/types.ts +++ b/packages/manager/src/factories/types.ts @@ -173,28 +173,29 @@ export const volumeTypeFactory = Factory.Sync.makeFactory({ transfer: 0, }); -export const objectStorageTypeFactory = Factory.Sync.makeFactory([ - { - id: 'objectstorage', - label: 'Object Storage', - price: { +export const objectStorageTypeFactory = Factory.Sync.makeFactory({ + id: 'objectstorage', + label: 'Object Storage', + price: { + hourly: 0.0075, + monthly: 5.0, + }, + region_prices: [ + { hourly: 0.0075, + id: 'id-cgk', monthly: 5.0, }, - region_prices: [ - { - hourly: 0.0075, - id: 'id-cgk', - monthly: 5.0, - }, - { - hourly: 0.0075, - id: 'br-gru', - monthly: 5.0, - }, - ], - transfer: 1000, - }, + { + hourly: 0.0075, + id: 'br-gru', + monthly: 5.0, + }, + ], + transfer: 1000, +}); + +export const objectStorageTypeOverageFactory = Factory.Sync.makeFactory( { id: 'objectstorage-overage', label: 'Object Storage Overage', @@ -215,5 +216,5 @@ export const objectStorageTypeFactory = Factory.Sync.makeFactory([ }, ], transfer: 0, - }, -]); + } +); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index c0dceb9e76e..f7d8aa12fad 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -76,6 +76,8 @@ import { objectStorageBucketFactory, objectStorageClusterFactory, objectStorageKeyFactory, + objectStorageTypeFactory, + objectStorageTypeOverageFactory, paymentFactory, paymentMethodFactory, placementGroupFactory, @@ -919,6 +921,17 @@ export const handlers = [ ]; return HttpResponse.json(makeResourcePage(configs)); }), + http.get('*/v4/nodebalancers/types', () => { + const nodeBalancerTypes = nodeBalancerTypeFactory.buildList(1); + return HttpResponse.json(makeResourcePage(nodeBalancerTypes)); + }), + http.get('*/v4/object-storage/types', () => { + const objectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageTypeOverageFactory.build(), + ]; + return HttpResponse.json(makeResourcePage(objectStorageTypes)); + }), http.get('*object-storage/buckets/*/*/access', async () => { await sleep(2000); return HttpResponse.json({ From 9f3a6ed5c978a97c0a17fc10d8b2d39cc1016313 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 16 May 2024 23:54:47 -0700 Subject: [PATCH 08/17] Clean up loading --- .../BucketLanding/OveragePricing.tsx | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 2a51adb06d5..4ec0d5fa941 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -29,7 +29,9 @@ export const OveragePricing = (props: Props) => { const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); - const overageType = types?.find((type) => type.id.includes('overage')); + const overageType = types?.find( + (type) => type.id === 'objectstorage-overage' + ); const storageOveragePrice = getDCSpecificPriceByType({ decimalPrecision: 3, @@ -42,23 +44,22 @@ export const OveragePricing = (props: Props) => { regionId ); - return ( + return isLoading ? ( + + ) : ( <> - {isLoading ? ( - - ) : ( - - For this region, additional storage costs{' '} - - $ - {storageOveragePrice && !isError - ? storageOveragePrice - : UNKNOWN_PRICE}{' '} - per GB - - . - - )} + + For this region, additional storage costs{' '} + + $ + {storageOveragePrice && !isError + ? storageOveragePrice + : UNKNOWN_PRICE}{' '} + per GB + + . + + Outbound transfer will cost{' '} @@ -66,11 +67,6 @@ export const OveragePricing = (props: Props) => { {isDcSpecificPricingRegion ? objectStoragePriceIncreaseMap[regionId].transfer_overage : OBJ_STORAGE_PRICE.transfer_overage}{' '} - {/* {getDCSpecificPriceByType({ - regionId, - timePeriod: 'hourly', - type: overageType, - })}{' '} */} per GB {' '} if it exceeds{' '} From 4ba42917b26743712c98211c349b21de7b3efc34 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Thu, 16 May 2024 23:55:23 -0700 Subject: [PATCH 09/17] Update OveragePricing test coverage --- .../BucketLanding/OveragePricing.test.tsx | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index 6b4c7f167eb..044cf28adac 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -1,7 +1,10 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; -import { objectStorageTypeFactory } from 'src/factories'; +import { + objectStorageTypeFactory, + objectStorageTypeOverageFactory, +} from 'src/factories'; import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -12,15 +15,35 @@ import { OveragePricing, } from './OveragePricing'; -const mockObjectStorageTypes = objectStorageTypeFactory.build(); -const storageOveragePriceType = mockObjectStorageTypes[1]; +const mockObjectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageTypeOverageFactory.build(), +]; + +const queryMocks = vi.hoisted(() => ({ + useObjectStorageTypesQuery: vi.fn().mockReturnValue({}), +})); + +vi.mock('src/queries/objectStorage', async () => { + const actual = await vi.importActual('src/queries/objectStorage'); + return { + ...actual, + useObjectStorageTypesQuery: queryMocks.useObjectStorageTypesQuery, + }; +}); describe('OveragePricing', async () => { - it.skip('Renders base overage pricing for a region without price increases', () => { + beforeAll(() => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: mockObjectStorageTypes, + }); + }); + + it('Renders base overage pricing for a region without price increases', () => { const { getByText } = renderWithTheme( ); - getByText(`$${storageOveragePriceType.price.hourly} per GB`, { + getByText(`$${mockObjectStorageTypes[1].price.hourly?.toFixed(3)} per GB`, { exact: false, }); getByText(`$${OBJ_STORAGE_PRICE.transfer_overage} per GB`, { @@ -28,14 +51,11 @@ describe('OveragePricing', async () => { }); }); - it.skip('Renders DC-specific overage pricing for a region with price increases', () => { + it('Renders DC-specific overage pricing for a region with price increases', () => { const { getByText } = renderWithTheme(); - getByText( - `$${storageOveragePriceType?.region_prices?.['br-gru']?.hourly} per GB`, - { - exact: false, - } - ); + getByText(`$${mockObjectStorageTypes[1].region_prices[1].hourly} per GB`, { + exact: false, + }); getByText( `$${objectStoragePriceIncreaseMap['br-gru'].transfer_overage} per GB`, { exact: false } @@ -67,4 +87,40 @@ describe('OveragePricing', async () => { expect(tooltip).toBeInTheDocument(); expect(getByText(GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT)).toBeVisible(); }); + + it('Renders a loading state while prices are loading', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + isLoading: true, + }); + + const { getByRole } = renderWithTheme( + + ); + + expect(getByRole('progressbar')).toBeVisible(); + }); + + it('Renders placeholder unknown pricing when there is an error', async () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + isError: true, + }); + + const { getAllByText } = renderWithTheme( + + ); + + expect(getAllByText('$--.-- per GB')).toHaveLength(1); + }); + + it('Renders placeholder unknown pricing when prices are undefined', async () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: undefined, + }); + + const { getAllByText } = renderWithTheme( + + ); + + expect(getAllByText('$--.-- per GB')).toHaveLength(1); + }); }); From 27e2ef5cca35d79e2be02c8094a11836833b0e6e Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Fri, 17 May 2024 00:08:34 -0700 Subject: [PATCH 10/17] Update dynamicPricing utils test coverage --- .../manager/src/utilities/pricing/dynamicPricing.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts index b3961c19d0a..56c73482029 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.test.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.test.ts @@ -75,9 +75,10 @@ describe('getDCSpecificPricingByType', () => { ).toBe('14.00'); }); - it('calculates dynamic pricing for a region without an increase on an hourly interval', () => { + it('calculates dynamic pricing for a region without an increase on an hourly interval to the specified decimal', () => { expect( getDCSpecificPriceByType({ + decimalPrecision: 3, interval: 'hourly', regionId: 'us-east', type: mockNodeBalancerType, @@ -85,9 +86,10 @@ describe('getDCSpecificPricingByType', () => { ).toBe('0.015'); }); - it('calculates dynamic pricing for a region with an increase on an hourly interval', () => { + it('calculates dynamic pricing for a region with an increase on an hourly interval to the specified decimal', () => { expect( getDCSpecificPriceByType({ + decimalPrecision: 3, interval: 'hourly', regionId: 'id-cgk', type: mockNodeBalancerType, From 87925d792a573c591a88e085d08b80de1f83eb43 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Fri, 17 May 2024 00:41:20 -0700 Subject: [PATCH 11/17] Clean up to disable types query if only creating an access key --- .../src/features/ObjectStorage/EnableObjectStorageModal.tsx | 6 ++++-- packages/manager/src/queries/objectStorage.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx index 529d430b81a..fbc6d26cbd4 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.tsx @@ -27,9 +27,11 @@ export const EnableObjectStorageModal = React.memo( (props: EnableObjectStorageProps) => { const { handleSubmit, onClose, open, regionId } = props; - const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); + const { data: types, isError, isLoading } = useObjectStorageTypesQuery( + Boolean(regionId) + ); - const isInvalidPrice = !types || isError; + const isInvalidPrice = Boolean(regionId) && (!types || isError); const objectStorageType = types?.find( (type) => type.id === 'objectstorage' diff --git a/packages/manager/src/queries/objectStorage.ts b/packages/manager/src/queries/objectStorage.ts index f8fceeae9e7..754ac007d4f 100644 --- a/packages/manager/src/queries/objectStorage.ts +++ b/packages/manager/src/queries/objectStorage.ts @@ -418,11 +418,12 @@ const getAllObjectStorageTypes = () => (data) => data.data ); -export const useObjectStorageTypesQuery = () => +export const useObjectStorageTypesQuery = (enabled = true) => useQuery( [`${queryKey}-types`], getAllObjectStorageTypes, { ...queryPresets.oneTimeFetch, + enabled, } ); From 519f687bc9078921fe05d764c7087298583a5535 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Fri, 17 May 2024 00:42:36 -0700 Subject: [PATCH 12/17] Add test coverage to modal; clean up --- packages/manager/src/factories/types.ts | 2 +- .../BucketLanding/OveragePricing.test.tsx | 13 +++--- .../EnableObjectStorageModal.test.tsx | 42 ++++++++++++++----- packages/manager/src/mocks/serverHandlers.ts | 4 +- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/packages/manager/src/factories/types.ts b/packages/manager/src/factories/types.ts index 3714a92947a..5229d085b50 100644 --- a/packages/manager/src/factories/types.ts +++ b/packages/manager/src/factories/types.ts @@ -195,7 +195,7 @@ export const objectStorageTypeFactory = Factory.Sync.makeFactory({ transfer: 1000, }); -export const objectStorageTypeOverageFactory = Factory.Sync.makeFactory( +export const objectStorageOverageTypeFactory = Factory.Sync.makeFactory( { id: 'objectstorage-overage', label: 'Object Storage Overage', diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index 044cf28adac..26e3a97e213 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -2,10 +2,13 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import { + objectStorageOverageTypeFactory, objectStorageTypeFactory, - objectStorageTypeOverageFactory, } from 'src/factories'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { + OBJ_STORAGE_PRICE, + UNKNOWN_PRICE, +} from 'src/utilities/pricing/constants'; import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -17,7 +20,7 @@ import { const mockObjectStorageTypes = [ objectStorageTypeFactory.build(), - objectStorageTypeOverageFactory.build(), + objectStorageOverageTypeFactory.build(), ]; const queryMocks = vi.hoisted(() => ({ @@ -109,7 +112,7 @@ describe('OveragePricing', async () => { ); - expect(getAllByText('$--.-- per GB')).toHaveLength(1); + expect(getAllByText(`$${UNKNOWN_PRICE} per GB`)).toHaveLength(1); }); it('Renders placeholder unknown pricing when prices are undefined', async () => { @@ -121,6 +124,6 @@ describe('OveragePricing', async () => { ); - expect(getAllByText('$--.-- per GB')).toHaveLength(1); + expect(getAllByText(`$${UNKNOWN_PRICE} per GB`)).toHaveLength(1); }); }); diff --git a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx index 3474219db6d..834cd38f4ec 100644 --- a/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx +++ b/packages/manager/src/features/ObjectStorage/EnableObjectStorageModal.test.tsx @@ -1,8 +1,11 @@ import { fireEvent, render } from '@testing-library/react'; import * as React from 'react'; -import { objectStorageTypeFactory } from 'src/factories'; -import { OBJ_STORAGE_PRICE } from 'src/utilities/pricing/constants'; +import { + objectStorageOverageTypeFactory, + objectStorageTypeFactory, +} from 'src/factories'; +import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { wrapWithTheme } from 'src/utilities/testHelpers'; import { @@ -25,21 +28,24 @@ const props: EnableObjectStorageProps = { }; const queryMocks = vi.hoisted(() => ({ - useObjectStorageTypes: vi.fn().mockReturnValue({}), + useObjectStorageTypesQuery: vi.fn().mockReturnValue({}), })); vi.mock('src/queries/objectStorage', async () => { const actual = await vi.importActual('src/queries/objectStorage'); return { ...actual, - useObjectStorageTypesQuery: queryMocks.useObjectStorageTypes, + useObjectStorageTypesQuery: queryMocks.useObjectStorageTypesQuery, }; }); describe('EnableObjectStorageModal', () => { - beforeEach(() => { - const mockObjectStorageTypes = objectStorageTypeFactory.build(); - queryMocks.useObjectStorageTypes.mockReturnValue({ + beforeAll(() => { + const mockObjectStorageTypes = [ + objectStorageTypeFactory.build(), + objectStorageOverageTypeFactory.build(), + ]; + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ data: mockObjectStorageTypes, }); }); @@ -57,7 +63,7 @@ describe('EnableObjectStorageModal', () => { ) ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); @@ -71,7 +77,7 @@ describe('EnableObjectStorageModal', () => { /> ) ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); @@ -80,11 +86,27 @@ describe('EnableObjectStorageModal', () => { const { getByText } = render( wrapWithTheme() ); - getByText(`$${OBJ_STORAGE_PRICE.monthly}/month`, { exact: false }); + getByText(`$5/month`, { exact: false }); getByText(OBJ_STORAGE_STORAGE_AMT, { exact: false }); getByText(OBJ_STORAGE_NETWORK_TRANSFER_AMT, { exact: false }); }); + it('displays placeholder unknown pricing and disables the primary action button if pricing is not available', () => { + queryMocks.useObjectStorageTypesQuery.mockReturnValue({ + data: undefined, + isError: true, + }); + + const { getByTestId, getByText } = render( + wrapWithTheme() + ); + + const primaryActionButton = getByTestId('enable-obj'); + + expect(getByText(`${UNKNOWN_PRICE}/month`, { exact: false })).toBeVisible(); + expect(primaryActionButton).toBeDisabled(); + }); + it('includes a link to linode.com/pricing', () => { const { getByText } = render( wrapWithTheme() diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index f7d8aa12fad..71425aa9b2a 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -77,7 +77,7 @@ import { objectStorageClusterFactory, objectStorageKeyFactory, objectStorageTypeFactory, - objectStorageTypeOverageFactory, + objectStorageOverageTypeFactory, paymentFactory, paymentMethodFactory, placementGroupFactory, @@ -928,7 +928,7 @@ export const handlers = [ http.get('*/v4/object-storage/types', () => { const objectStorageTypes = [ objectStorageTypeFactory.build(), - objectStorageTypeOverageFactory.build(), + objectStorageOverageTypeFactory.build(), ]; return HttpResponse.json(makeResourcePage(objectStorageTypes)); }), From 702f2ce1d32606ce4cd5c83c3e3a4a377c3d046c Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Mon, 20 May 2024 08:26:45 -0700 Subject: [PATCH 13/17] Clean up old constants in test spec --- .../core/objectStorage/enable-object-storage.spec.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts index 85735a3cf48..76459bd7b5b 100644 --- a/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts +++ b/packages/manager/cypress/e2e/core/objectStorage/enable-object-storage.spec.ts @@ -36,16 +36,6 @@ import { makeFeatureFlagData } from 'support/util/feature-flags'; // Various messages, notes, and warnings that may be shown when enabling Object Storage // under different circumstances. const objNotes = { - // When enabling OBJ using a region with a regular pricing structure, when OBJ DC-specific pricing is disabled. - regularPricing: /Linode Object Storage costs a flat rate of \$5\/month, and includes 250 GB of storage and 1 TB of outbound data transfer. Beyond that, it.*s \$0.02 per GB per month./, - - // When enabling OBJ using a region with special pricing during the free beta period (OBJ DC-specific pricing is disabled). - dcSpecificBetaPricing: /Object Storage for .* is currently in beta\. During the beta period, Object Storage in these regions is free\. After the beta period, customers will be notified before charges for this service begin./, - - // When enabling OBJ without having selected a region, when OBJ DC-specific pricing is disabled. - dcPricingGenericExplanation: - 'Pricing for monthly rate and overage costs will depend on the data center you select for deployment.', - // When enabling OBJ, in both the Access Key flow and Create Bucket flow, when OBJ DC-specific pricing is enabled. objDCPricing: 'Object Storage costs a flat rate of $5/month, and includes 250 GB of storage. When you enable Object Storage, 1 TB of outbound data transfer will be added to your global network transfer pool.', From e7ec121bab27b0b792a7fa93003eb7fcf9d780d1 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Mon, 20 May 2024 08:31:08 -0700 Subject: [PATCH 14/17] Remove trailing zero for base price storage overage --- .../ObjectStorage/BucketLanding/OveragePricing.test.tsx | 2 +- .../src/features/ObjectStorage/BucketLanding/OveragePricing.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index 26e3a97e213..4371403b28e 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -46,7 +46,7 @@ describe('OveragePricing', async () => { const { getByText } = renderWithTheme( ); - getByText(`$${mockObjectStorageTypes[1].price.hourly?.toFixed(3)} per GB`, { + getByText(`$${mockObjectStorageTypes[1].price.hourly?.toFixed(2)} per GB`, { exact: false, }); getByText(`$${OBJ_STORAGE_PRICE.transfer_overage} per GB`, { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 4ec0d5fa941..727c20d5cdc 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -53,7 +53,7 @@ export const OveragePricing = (props: Props) => { $ {storageOveragePrice && !isError - ? storageOveragePrice + ? parseFloat(storageOveragePrice) : UNKNOWN_PRICE}{' '} per GB From 251773174fee20103f85f9acf07c94c2fa04c899 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Mon, 20 May 2024 08:34:15 -0700 Subject: [PATCH 15/17] Add changesets --- packages/api-v4/.changeset/pr-10468-added-1716219170108.md | 5 +++++ .../manager/.changeset/pr-10468-changed-1716219242980.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 packages/api-v4/.changeset/pr-10468-added-1716219170108.md create mode 100644 packages/manager/.changeset/pr-10468-changed-1716219242980.md diff --git a/packages/api-v4/.changeset/pr-10468-added-1716219170108.md b/packages/api-v4/.changeset/pr-10468-added-1716219170108.md new file mode 100644 index 00000000000..2a5eab0136d --- /dev/null +++ b/packages/api-v4/.changeset/pr-10468-added-1716219170108.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +New endpoint for `object-storage/types` ([#10468](https://github.com/linode/manager/pull/10468)) diff --git a/packages/manager/.changeset/pr-10468-changed-1716219242980.md b/packages/manager/.changeset/pr-10468-changed-1716219242980.md new file mode 100644 index 00000000000..729048a7d33 --- /dev/null +++ b/packages/manager/.changeset/pr-10468-changed-1716219242980.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Use dynamic pricing with `object-storage/types` endpoint ([#10468](https://github.com/linode/manager/pull/10468)) From 5ad54d8049b9189b904f0655a1173e2ea8083f67 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Mon, 20 May 2024 08:50:46 -0700 Subject: [PATCH 16/17] Clean up from self review --- packages/api-v4/src/object-storage/prices.ts | 1 - .../ObjectStorage/BucketLanding/OveragePricing.test.tsx | 4 ++-- packages/manager/src/mocks/serverHandlers.ts | 4 ---- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/api-v4/src/object-storage/prices.ts b/packages/api-v4/src/object-storage/prices.ts index 7a5747b3ba9..2907a6a110c 100644 --- a/packages/api-v4/src/object-storage/prices.ts +++ b/packages/api-v4/src/object-storage/prices.ts @@ -2,7 +2,6 @@ import { Params, PriceType, ResourcePage } from 'src/types'; import { API_ROOT } from '../constants'; import Request, { setMethod, setParams, setURL } from '../request'; -// TODO: decide the best place to put this. /** * getObjectStorageTypes * diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index 4371403b28e..43330dcaa49 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -103,7 +103,7 @@ describe('OveragePricing', async () => { expect(getByRole('progressbar')).toBeVisible(); }); - it('Renders placeholder unknown pricing when there is an error', async () => { + it('Renders placeholder unknown pricing when there is an error', () => { queryMocks.useObjectStorageTypesQuery.mockReturnValue({ isError: true, }); @@ -115,7 +115,7 @@ describe('OveragePricing', async () => { expect(getAllByText(`$${UNKNOWN_PRICE} per GB`)).toHaveLength(1); }); - it('Renders placeholder unknown pricing when prices are undefined', async () => { + it('Renders placeholder unknown pricing when prices are undefined', () => { queryMocks.useObjectStorageTypesQuery.mockReturnValue({ data: undefined, }); diff --git a/packages/manager/src/mocks/serverHandlers.ts b/packages/manager/src/mocks/serverHandlers.ts index 71425aa9b2a..d0cc05f9548 100644 --- a/packages/manager/src/mocks/serverHandlers.ts +++ b/packages/manager/src/mocks/serverHandlers.ts @@ -921,10 +921,6 @@ export const handlers = [ ]; return HttpResponse.json(makeResourcePage(configs)); }), - http.get('*/v4/nodebalancers/types', () => { - const nodeBalancerTypes = nodeBalancerTypeFactory.buildList(1); - return HttpResponse.json(makeResourcePage(nodeBalancerTypes)); - }), http.get('*/v4/object-storage/types', () => { const objectStorageTypes = [ objectStorageTypeFactory.build(), From 224d943078ac4058006fbc05fec43d02901d6b35 Mon Sep 17 00:00:00 2001 From: mjac0bs Date: Tue, 21 May 2024 12:21:19 -0700 Subject: [PATCH 17/17] Address feedback: @bnussman-akamai --- .../BucketLanding/CreateBucketDrawer.tsx | 4 ++-- packages/manager/src/queries/objectStorage.ts | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 4eb87584d83..001bdce16d3 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -27,11 +27,11 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendCreateBucketEvent } from 'src/utilities/analytics'; import { getErrorMap } from 'src/utilities/errorUtils'; import { getGDPRDetails } from 'src/utilities/formatRegion'; +import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; import { EnableObjectStorageModal } from '../EnableObjectStorageModal'; import ClusterSelect from './ClusterSelect'; import { OveragePricing } from './OveragePricing'; -import { PRICES_RELOAD_ERROR_NOTICE_TEXT } from 'src/utilities/pricing/constants'; interface Props { isOpen: boolean; @@ -80,7 +80,7 @@ export const CreateBucketDrawer = (props: Props) => { data: types, isError: isErrorTypes, isLoading: isLoadingTypes, - } = useObjectStorageTypesQuery(); + } = useObjectStorageTypesQuery(isOpen); const isInvalidPrice = !types || isErrorTypes; diff --git a/packages/manager/src/queries/objectStorage.ts b/packages/manager/src/queries/objectStorage.ts index 754ac007d4f..44de89ed1a9 100644 --- a/packages/manager/src/queries/objectStorage.ts +++ b/packages/manager/src/queries/objectStorage.ts @@ -419,11 +419,9 @@ const getAllObjectStorageTypes = () => ); export const useObjectStorageTypesQuery = (enabled = true) => - useQuery( - [`${queryKey}-types`], - getAllObjectStorageTypes, - { - ...queryPresets.oneTimeFetch, - enabled, - } - ); + useQuery({ + queryFn: getAllObjectStorageTypes, + queryKey: [queryKey, 'types'], + ...queryPresets.oneTimeFetch, + enabled, + });