diff --git a/packages/api-v4/.changeset/pr-10566-added-1718123757956.md b/packages/api-v4/.changeset/pr-10566-added-1718123757956.md new file mode 100644 index 00000000000..33f278dce49 --- /dev/null +++ b/packages/api-v4/.changeset/pr-10566-added-1718123757956.md @@ -0,0 +1,5 @@ +--- +"@linode/api-v4": Added +--- + +New endpoint for `network-transfer/prices`` ([#10566](https://github.com/linode/manager/pull/10566)) diff --git a/packages/api-v4/src/index.ts b/packages/api-v4/src/index.ts index b62ab50d817..8de3cbfcf6f 100644 --- a/packages/api-v4/src/index.ts +++ b/packages/api-v4/src/index.ts @@ -22,6 +22,8 @@ export * from './managed'; export * from './networking'; +export * from './network-transfer'; + export * from './nodebalancers'; export * from './object-storage'; diff --git a/packages/api-v4/src/network-transfer/index.ts b/packages/api-v4/src/network-transfer/index.ts new file mode 100644 index 00000000000..19729308f7c --- /dev/null +++ b/packages/api-v4/src/network-transfer/index.ts @@ -0,0 +1 @@ +export * from './prices'; diff --git a/packages/api-v4/src/network-transfer/prices.ts b/packages/api-v4/src/network-transfer/prices.ts new file mode 100644 index 00000000000..ccb0233bb75 --- /dev/null +++ b/packages/api-v4/src/network-transfer/prices.ts @@ -0,0 +1,10 @@ +import { API_ROOT } from 'src/constants'; +import Request, { setMethod, setURL, setParams } from 'src/request'; +import { Params, PriceType, ResourcePage } from 'src/types'; + +export const getNetworkTransferPrices = (params?: Params) => + Request>( + setURL(`${API_ROOT}/network-transfer/prices`), + setMethod('GET'), + setParams(params) + ); diff --git a/packages/manager/.changeset/pr-10566-changed-1718123674587.md b/packages/manager/.changeset/pr-10566-changed-1718123674587.md new file mode 100644 index 00000000000..4d7abc2ec23 --- /dev/null +++ b/packages/manager/.changeset/pr-10566-changed-1718123674587.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +Use dynamic outbound transfer pricing with `network-transfer/prices` endpoint ([#10566](https://github.com/linode/manager/pull/10566)) diff --git a/packages/manager/src/factories/types.ts b/packages/manager/src/factories/types.ts index 5229d085b50..b184dcbbbf5 100644 --- a/packages/manager/src/factories/types.ts +++ b/packages/manager/src/factories/types.ts @@ -218,3 +218,40 @@ export const objectStorageOverageTypeFactory = Factory.Sync.makeFactory( + { + id: 'distributed_network_transfer', + label: 'Distributed Network Transfer', + price: { + hourly: 0.01, + monthly: null, + }, + region_prices: [], + transfer: 0, + } +); + +export const networkTransferPriceTypeFactory = Factory.Sync.makeFactory( + { + id: 'network_transfer', + label: 'Network Transfer', + price: { + hourly: 0.005, + monthly: null, + }, + region_prices: [ + { + hourly: 0.015, + id: 'id-cgk', + monthly: null, + }, + { + hourly: 0.007, + id: 'br-gru', + monthly: null, + }, + ], + transfer: 0, + } +); diff --git a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx index dc745704d43..41ece50f4f1 100644 --- a/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx +++ b/packages/manager/src/features/CloudPulse/shared/CloudPulseResourcesSelect.test.tsx @@ -20,7 +20,7 @@ vi.mock('src/queries/cloudpulse/resources', async () => { const mockResourceHandler = vi.fn(); const SELECT_ALL = 'Select All'; -const ARIA_SELECTED = 'aria-selected' +const ARIA_SELECTED = 'aria-selected'; describe('CloudPulseResourcesSelect component tests', () => { it('should render disabled component if the the props are undefined or regions and service type does not have any resources', () => { diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx index 11c89b01580..71fb9116ef1 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/CreateBucketDrawer.tsx @@ -15,6 +15,7 @@ import { useMutateAccountAgreements, } from 'src/queries/account/agreements'; import { useAccountSettings } from 'src/queries/account/settings'; +import { useNetworkTransferPricesQuery } from 'src/queries/networkTransfer'; import { useCreateBucketMutation, useObjectStorageBuckets, @@ -77,12 +78,20 @@ export const CreateBucketDrawer = (props: Props) => { }); const { - data: types, - isError: isErrorTypes, - isLoading: isLoadingTypes, + data: objTypes, + isError: isErrorObjTypes, + isInitialLoading: isLoadingObjTypes, } = useObjectStorageTypesQuery(isOpen); - - const isInvalidPrice = !types || isErrorTypes; + const { + data: transferTypes, + isError: isErrorTransferTypes, + isInitialLoading: isLoadingTransferTypes, + } = useNetworkTransferPricesQuery(isOpen); + + const isErrorTypes = isErrorTransferTypes || isErrorObjTypes; + const isLoadingTypes = isLoadingTransferTypes || isLoadingObjTypes; + const isInvalidPrice = + !objTypes || !transferTypes || isErrorTypes || isErrorTransferTypes; const { error, diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx index 1438e669c81..4340b64e5f6 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OMC_CreateBucketDrawer.tsx @@ -13,9 +13,11 @@ import { useMutateAccountAgreements, } from 'src/queries/account/agreements'; import { useAccountSettings } from 'src/queries/account/settings'; +import { useNetworkTransferPricesQuery } from 'src/queries/networkTransfer'; import { useCreateBucketMutation, useObjectStorageBuckets, + useObjectStorageTypesQuery, } from 'src/queries/objectStorage'; import { useProfile } from 'src/queries/profile/profile'; import { useRegionsQuery } from 'src/queries/regions/regions'; @@ -23,6 +25,7 @@ import { isFeatureEnabled } from 'src/utilities/accountCapabilities'; import { sendCreateBucketEvent } from 'src/utilities/analytics/customEventAnalytics'; 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 { BucketRegions } from './BucketRegions'; @@ -58,6 +61,22 @@ export const OMC_CreateBucketDrawer = (props: Props) => { regions: regionsSupportingObjectStorage, }); + const { + data: objTypes, + isError: isErrorObjTypes, + isInitialLoading: isLoadingObjTypes, + } = useObjectStorageTypesQuery(isOpen); + const { + data: transferTypes, + isError: isErrorTransferTypes, + isInitialLoading: isLoadingTransferTypes, + } = useNetworkTransferPricesQuery(isOpen); + + const isErrorTypes = isErrorTransferTypes || isErrorObjTypes; + const isLoadingTypes = isLoadingTransferTypes || isLoadingObjTypes; + const isInvalidPrice = + !objTypes || !transferTypes || isErrorTypes || isErrorTransferTypes; + const { error, isLoading, @@ -176,9 +195,15 @@ export const OMC_CreateBucketDrawer = (props: Props) => { 'data-testid': 'create-bucket-button', disabled: !formik.values.region || - (showGDPRCheckbox && !hasSignedAgreement), + (showGDPRCheckbox && !hasSignedAgreement) || + isErrorTypes, label: 'Create Bucket', - loading: isLoading, + loading: + isLoading || Boolean(formik.values.region && isLoadingTypes), + tooltipText: + !isLoadingTypes && isInvalidPrice + ? PRICES_RELOAD_ERROR_NOTICE_TEXT + : '', type: 'submit', }} secondaryButtonProps={{ label: 'Cancel', onClick: onClose }} diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx index 43330dcaa49..c7d0c2eba23 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.test.tsx @@ -2,14 +2,12 @@ import { fireEvent } from '@testing-library/react'; import React from 'react'; import { + distributedNetworkTransferPriceTypeFactory, + networkTransferPriceTypeFactory, objectStorageOverageTypeFactory, objectStorageTypeFactory, } from 'src/factories'; -import { - OBJ_STORAGE_PRICE, - UNKNOWN_PRICE, -} from 'src/utilities/pricing/constants'; -import { objectStoragePriceIncreaseMap } from 'src/utilities/pricing/dynamicPricing'; +import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; import { renderWithTheme } from 'src/utilities/testHelpers'; import { @@ -23,7 +21,13 @@ const mockObjectStorageTypes = [ objectStorageOverageTypeFactory.build(), ]; +const mockNetworkTransferTypes = [ + distributedNetworkTransferPriceTypeFactory.build(), + networkTransferPriceTypeFactory.build(), +]; + const queryMocks = vi.hoisted(() => ({ + useNetworkTransferPricesQuery: vi.fn().mockReturnValue({}), useObjectStorageTypesQuery: vi.fn().mockReturnValue({}), })); @@ -35,11 +39,22 @@ vi.mock('src/queries/objectStorage', async () => { }; }); +vi.mock('src/queries/networkTransfer', async () => { + const actual = await vi.importActual('src/queries/networkTransfer'); + return { + ...actual, + useNetworkTransferPricesQuery: queryMocks.useNetworkTransferPricesQuery, + }; +}); + describe('OveragePricing', async () => { beforeAll(() => { queryMocks.useObjectStorageTypesQuery.mockReturnValue({ data: mockObjectStorageTypes, }); + queryMocks.useNetworkTransferPricesQuery.mockReturnValue({ + data: mockNetworkTransferTypes, + }); }); it('Renders base overage pricing for a region without price increases', () => { @@ -49,7 +64,7 @@ describe('OveragePricing', async () => { getByText(`$${mockObjectStorageTypes[1].price.hourly?.toFixed(2)} per GB`, { exact: false, }); - getByText(`$${OBJ_STORAGE_PRICE.transfer_overage} per GB`, { + getByText(`$${mockNetworkTransferTypes[1].price.hourly} per GB`, { exact: false, }); }); @@ -60,7 +75,7 @@ describe('OveragePricing', async () => { exact: false, }); getByText( - `$${objectStoragePriceIncreaseMap['br-gru'].transfer_overage} per GB`, + `$${mockNetworkTransferTypes[1].region_prices[1].hourly} per GB`, { exact: false } ); }); diff --git a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx index 7a277db53ec..fa139dac2a0 100644 --- a/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx +++ b/packages/manager/src/features/ObjectStorage/BucketLanding/OveragePricing.tsx @@ -1,4 +1,3 @@ -import { Region } from '@linode/api-v4'; import { styled } from '@mui/material/styles'; import React from 'react'; @@ -6,15 +5,12 @@ import { Box } from 'src/components/Box'; import { CircleProgress } from 'src/components/CircleProgress'; import { TextTooltip } from 'src/components/TextTooltip'; import { Typography } from 'src/components/Typography'; +import { useNetworkTransferPricesQuery } from 'src/queries/networkTransfer'; import { useObjectStorageTypesQuery } from 'src/queries/objectStorage'; -import { - OBJ_STORAGE_PRICE, - UNKNOWN_PRICE, -} from 'src/utilities/pricing/constants'; -import { - getDCSpecificPriceByType, - objectStoragePriceIncreaseMap, -} from 'src/utilities/pricing/dynamicPricing'; +import { UNKNOWN_PRICE } from 'src/utilities/pricing/constants'; +import { getDCSpecificPriceByType } from 'src/utilities/pricing/dynamicPricing'; + +import type { Region } from '@linode/api-v4'; interface Props { regionId: Region['id']; @@ -28,24 +24,44 @@ export const GLOBAL_TRANSFER_POOL_TOOLTIP_TEXT = export const OveragePricing = (props: Props) => { const { regionId } = props; - const { data: types, isError, isLoading } = useObjectStorageTypesQuery(); + const { + data: objTypes, + isError: isErrorObjTypes, + isLoading: isLoadingObjTypes, + } = useObjectStorageTypesQuery(); + const { + data: transferTypes, + isError: isErrorTransferTypes, + isLoading: isLoadingTransferTypes, + } = useNetworkTransferPricesQuery(); - const overageType = types?.find( + const storageOverageType = objTypes?.find( (type) => type.id === 'objectstorage-overage' ); + const transferOverageType = transferTypes?.find( + (type) => type.id === 'network_transfer' + ); const storageOveragePrice = getDCSpecificPriceByType({ decimalPrecision: 3, interval: 'hourly', regionId, - type: overageType, + type: storageOverageType, + }); + const transferOveragePrice = getDCSpecificPriceByType({ + decimalPrecision: 3, + interval: 'hourly', + regionId, + type: transferOverageType, }); - const isDcSpecificPricingRegion = objectStoragePriceIncreaseMap.hasOwnProperty( - regionId + const isDcSpecificPricingRegion = Boolean( + transferOverageType?.region_prices.find( + (region_price) => region_price.id === regionId + ) ); - return isLoading ? ( + return isLoadingObjTypes || isLoadingTransferTypes ? ( @@ -55,7 +71,7 @@ export const OveragePricing = (props: Props) => { For this region, additional storage costs{' '} $ - {storageOveragePrice && !isError + {storageOveragePrice && !isErrorObjTypes ? parseFloat(storageOveragePrice) : UNKNOWN_PRICE}{' '} per GB @@ -67,9 +83,9 @@ export const OveragePricing = (props: Props) => { Outbound transfer will cost{' '} $ - {isDcSpecificPricingRegion - ? objectStoragePriceIncreaseMap[regionId].transfer_overage - : OBJ_STORAGE_PRICE.transfer_overage}{' '} + {transferOveragePrice && !isErrorTransferTypes + ? parseFloat(transferOveragePrice) + : UNKNOWN_PRICE}{' '} per GB {' '} if it exceeds{' '} diff --git a/packages/manager/src/queries/networkTransfer.ts b/packages/manager/src/queries/networkTransfer.ts new file mode 100644 index 00000000000..29f9ed3b6ae --- /dev/null +++ b/packages/manager/src/queries/networkTransfer.ts @@ -0,0 +1,23 @@ +import { getNetworkTransferPrices } from '@linode/api-v4'; +import { useQuery } from '@tanstack/react-query'; + +import { getAll } from 'src/utilities/getAll'; + +import { queryPresets } from './base'; + +import type { APIError, PriceType } from '@linode/api-v4'; + +export const queryKey = 'network-transfer'; + +const getAllNetworkTransferPrices = () => + getAll((params) => getNetworkTransferPrices(params))().then( + (data) => data.data + ); + +export const useNetworkTransferPricesQuery = (enabled = true) => + useQuery({ + queryFn: getAllNetworkTransferPrices, + queryKey: [queryKey, 'prices'], + ...queryPresets.oneTimeFetch, + enabled, + }); diff --git a/packages/manager/src/utilities/pricing/constants.ts b/packages/manager/src/utilities/pricing/constants.ts index 190b1b7d824..9dbd49f171e 100644 --- a/packages/manager/src/utilities/pricing/constants.ts +++ b/packages/manager/src/utilities/pricing/constants.ts @@ -1,17 +1,7 @@ -export interface ObjStoragePriceObject { - monthly: number; - storage_overage: number; - transfer_overage: number; -} - // These values will eventually come from the API, but for now they are hardcoded and // used to generate the region based dynamic pricing. export const LKE_HA_PRICE = 60; -export const OBJ_STORAGE_PRICE: ObjStoragePriceObject = { - monthly: 5.0, - storage_overage: 0.02, - transfer_overage: 0.005, -}; + export const UNKNOWN_PRICE = '--.--'; export const PRICE_ERROR_TOOLTIP_TEXT = 'There was an error loading the price.'; export const PRICES_RELOAD_ERROR_NOTICE_TEXT = diff --git a/packages/manager/src/utilities/pricing/dynamicPricing.ts b/packages/manager/src/utilities/pricing/dynamicPricing.ts index b190b0c5d64..b6e17da0b60 100644 --- a/packages/manager/src/utilities/pricing/dynamicPricing.ts +++ b/packages/manager/src/utilities/pricing/dynamicPricing.ts @@ -53,17 +53,6 @@ export const priceIncreaseMap = { 'id-cgk': 0.2, // Jakarta }; -export const objectStoragePriceIncreaseMap = { - 'br-gru': { - storage_overage: 0.028, - transfer_overage: 0.007, - }, - 'id-cgk': { - storage_overage: 0.024, - transfer_overage: 0.015, - }, -}; - /** * This function is used to calculate the dynamic pricing for a given entity, based on potential region increased costs. * @example