Skip to content

Commit b2f1caf

Browse files
committed
fix(pci-kubernetes): calculate multiple node price
ref: #TAPC-3341 Signed-off-by: Pierre-Philippe <[email protected]>
1 parent 3bfb7e5 commit b2f1caf

File tree

16 files changed

+407
-193
lines changed

16 files changed

+407
-193
lines changed

packages/manager/apps/pci-kubernetes/src/api/data/flavors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ export const getFlavors = async (
7171
return data;
7272
};
7373

74+
export const getFlavorDetails = async (
75+
projectId: string,
76+
region: string,
77+
): Promise<TFlavor[]> => {
78+
const { data } = await v6.get<TFlavor[]>(
79+
`/cloud/project/${projectId}/flavor?region=${region}`,
80+
);
81+
return data;
82+
};
83+
7484
export type TKubeFlavor = {
7585
category: string;
7686
gpus: number;

packages/manager/apps/pci-kubernetes/src/api/data/kubernetes.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,22 @@ export const getAllKube = async (projectId: string): Promise<TKube[]> => {
1919
return data;
2020
};
2121

22-
export interface NodePool {
22+
export type NodePool = {
2323
name: string;
2424
antiAffinity: boolean;
2525
autoscale: boolean;
26-
availabilityZones: string[];
26+
availabilityZones?: string[];
2727
desiredNodes: number;
2828
minNodes: number;
29-
localisation: string; // will change with 3AZ
29+
localisation: string;
3030
flavorName: string;
3131
maxNodes: number;
3232
monthlyBilled: boolean;
33-
}
33+
};
3434

3535
export type NodePoolPrice = NodePool & { monthlyPrice: number };
3636

37-
export interface KubeClusterCreationParams {
37+
export type KubeClusterCreationParams = {
3838
name: string;
3939
region: string;
4040
version: string;
@@ -48,7 +48,7 @@ export interface KubeClusterCreationParams {
4848
defaultVrackGateway?: string;
4949
privateNetworkRoutingAsDefault?: boolean;
5050
};
51-
}
51+
};
5252

5353
export const createKubernetesCluster = async (
5454
projectId: string,

packages/manager/apps/pci-kubernetes/src/components/flavor-selector/FlavorSelector.component.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ import {
1414
} from '@/hooks/useFlavors';
1515
import { FlavorTile } from './FlavorTile.component';
1616

17-
export type KubeFlavor = ReturnType<
17+
export type TComputedKubeFlavor = ReturnType<
1818
typeof useMergedKubeFlavors
1919
>['mergedFlavors'][0];
2020

2121
interface FlavorSelectorProps {
2222
projectId: string;
2323
region: string;
24-
onSelect?: (flavor: KubeFlavor) => void;
24+
onSelect?: (flavor: TComputedKubeFlavor) => void;
2525
}
2626

2727
export function FlavorSelector({

packages/manager/apps/pci-kubernetes/src/helpers/index.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest';
22
import { z } from 'zod';
3+
34
import {
45
compareFunction,
56
formatIP,
@@ -13,7 +14,7 @@ import {
1314
parseCommaSeparated,
1415
generateUniqueName,
1516
} from '@/helpers/index';
16-
import { NodePool } from '@/api/data/kubernetes';
17+
import { NodePoolPrice } from '@/api/data/kubernetes';
1718

1819
describe('helper', () => {
1920
it('compares two objects based on a key', () => {
@@ -200,7 +201,7 @@ describe('generateUniqueName', () => {
200201
(baseName, existingNodePools, expectedResult) => {
201202
const result = generateUniqueName(
202203
baseName,
203-
existingNodePools as NodePool[],
204+
existingNodePools as NodePoolPrice[],
204205
);
205206
expect(result).toBe(expectedResult);
206207
},

packages/manager/apps/pci-kubernetes/src/helpers/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ZodObject, ZodRawShape } from 'zod';
44
import { ClassValue, clsx } from 'clsx';
55
import { twMerge } from 'tailwind-merge';
66
import { DeploymentMode, SigningAlgorithms, TOidcProvider } from '@/types';
7-
import { NodePool } from '@/api/data/kubernetes';
7+
import { NodePool, NodePoolPrice } from '@/api/data/kubernetes';
88

99
export const REFETCH_INTERVAL_DURATION = 15_000;
1010
export const QUOTA_ERROR_URL =
@@ -164,7 +164,7 @@ export const transformKey = (key: string): string =>
164164
key.replace(/([A-Z])/g, '_$1').toLowerCase();
165165

166166
const isNotEmptyString = (str: string): boolean => str.trim() !== '';
167-
const isNotEmptyArray = (arr: any[]): boolean => arr.length > 0;
167+
const isNotEmptyArray = (arr: unknown[]): boolean => arr.length > 0;
168168

169169
export const isOptionalValue = (
170170
value: string | string[] | SigningAlgorithms[] | null | undefined,
@@ -175,7 +175,7 @@ export const isOptionalValue = (
175175
return true;
176176
};
177177
export const getValidOptionalKeys = (oidcProvider: TOidcProvider) =>
178-
Object.entries(oidcProvider ?? {}).reduce((acc, [key, value]) => {
178+
Object.entries(oidcProvider ?? {}).reduce<string[]>((acc, [key, value]) => {
179179
if (isOptionalValue(value) && key !== 'issuerUrl' && key !== 'clientId') {
180180
acc.push(key);
181181
}
@@ -191,7 +191,7 @@ export const getValidOptionalKeys = (oidcProvider: TOidcProvider) =>
191191
*/
192192
export function generateUniqueName(
193193
baseName: string,
194-
existingNodePools: NodePool[],
194+
existingNodePools: NodePoolPrice[],
195195
) {
196196
let newName = baseName;
197197
let copyNumber = 1;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { describe, it, vi, beforeEach, afterEach, expect, test } from 'vitest';
3+
4+
import useFlavorDetails, { getPrice } from './useMergedFlavorById';
5+
6+
import { TComputedKubeFlavor } from '@/components/flavor-selector/FlavorSelector.component';
7+
import { useMergedKubeFlavors } from './useFlavors';
8+
import { wrapper } from '@/wrapperRenders';
9+
10+
vi.mock('./useFlavors', () => ({
11+
...vi.importActual('./useFlavors'),
12+
useMergedKubeFlavors: vi.fn(),
13+
}));
14+
15+
describe('getPrice', () => {
16+
test.each([
17+
[undefined, undefined, 0, undefined],
18+
[0, 100, 2, undefined],
19+
[1, undefined, 2, { hour: 2, month: undefined }],
20+
[1, 10, 2, { hour: 2, month: 20 }],
21+
[5, 50, 3, { hour: 15, month: 150 }],
22+
])(
23+
'getPrice(%p, %p, %p) should return %p',
24+
(hour, month, desiredScaling, expected) => {
25+
expect(getPrice(hour, month, desiredScaling)).toEqual(expected);
26+
},
27+
);
28+
});
29+
30+
describe('useFlavorDetails', () => {
31+
const mockFlavors = [
32+
{
33+
id: 'flavor-1',
34+
name: 'Flavor 1',
35+
vcpus: 2,
36+
ram: 4096,
37+
pricingsHourly: { price: 0.05 },
38+
pricingsMonthly: { price: 30 },
39+
},
40+
{
41+
id: 'flavor-2',
42+
name: 'Flavor 2',
43+
vcpus: 4,
44+
ram: 8192,
45+
pricingsHourly: { price: 0.1 },
46+
pricingsMonthly: { price: 60 },
47+
},
48+
] as TComputedKubeFlavor[];
49+
50+
beforeEach(() => {
51+
vi.mocked(useMergedKubeFlavors).mockReturnValue({
52+
mergedFlavors: mockFlavors,
53+
isPending: false,
54+
} as { mergedFlavors: TComputedKubeFlavor[]; isPending: boolean });
55+
});
56+
57+
afterEach(() => {
58+
vi.clearAllMocks();
59+
});
60+
61+
it('should return flavor details with computed price', async () => {
62+
const { result } = renderHook(
63+
() =>
64+
useFlavorDetails('project-1', 'GRA', 'flavor-1', { desiredScaling: 3 }),
65+
{ wrapper },
66+
);
67+
68+
expect(result.current).toEqual({
69+
id: 'flavor-1',
70+
name: 'Flavor 1',
71+
vcpus: 2,
72+
ram: 4096,
73+
pricingsHourly: { price: 0.05 },
74+
pricingsMonthly: { price: 30 },
75+
price: {
76+
hour: 0.05 * 3,
77+
month: 30 * 3,
78+
},
79+
});
80+
});
81+
82+
it('should return undefined price if desiredScaling is 0', () => {
83+
const { result } = renderHook(
84+
() =>
85+
useFlavorDetails('project-1', 'GRA', 'flavor-2', { desiredScaling: 0 }),
86+
{ wrapper },
87+
);
88+
89+
expect(result.current).toEqual({
90+
id: 'flavor-2',
91+
name: 'Flavor 2',
92+
vcpus: 4,
93+
ram: 8192,
94+
pricingsHourly: { price: 0.1 },
95+
pricingsMonthly: { price: 60 },
96+
price: undefined,
97+
});
98+
});
99+
100+
it('should return undefined flavor if not found', () => {
101+
const { result } = renderHook(
102+
() =>
103+
useFlavorDetails('project-1', 'GRA', 'unknown-flavor', {
104+
desiredScaling: 2,
105+
}),
106+
{ wrapper },
107+
);
108+
109+
expect(result.current).toEqual({
110+
price: undefined,
111+
});
112+
});
113+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useMemo } from 'react';
2+
import { useMergedKubeFlavors } from './useFlavors';
3+
import { TComputedKubeFlavor } from '@/components/flavor-selector/FlavorSelector.component';
4+
5+
export const getPriceByDesiredScale = (
6+
hour?: number,
7+
month?: number,
8+
desiredScaling = 0,
9+
) => {
10+
if (!hour || !desiredScaling) return undefined;
11+
return {
12+
hour: hour * desiredScaling,
13+
month: month ? month * desiredScaling : undefined,
14+
};
15+
};
16+
17+
const useMergedFlavorById = <T = TComputedKubeFlavor>(
18+
projectId: string,
19+
region: string,
20+
flavorId: string,
21+
opts?: { select?: (flavor: TComputedKubeFlavor) => T },
22+
): T | null => {
23+
const { mergedFlavors } = useMergedKubeFlavors(projectId, region);
24+
25+
const flavor = useMemo(() => mergedFlavors?.find((f) => f.id === flavorId), [
26+
mergedFlavors,
27+
flavorId,
28+
]);
29+
if (!flavor) return null;
30+
31+
return opts?.select ? opts?.select(flavor) : (flavor as T);
32+
};
33+
34+
export default useMergedFlavorById;

0 commit comments

Comments
 (0)