+ This plan licenses Harper for the usage limits below, for the price listed above. The usage license expires in three months or when any usage
+ limit is reached. Usage blocks can be purchased as they are consumed.
{rows.map((row, index) =>
- {row.label}
diff --git a/src/features/clusters/upsert/index.tsx b/src/features/clusters/upsert/index.tsx
index d80ece13e..8da9b630b 100644
--- a/src/features/clusters/upsert/index.tsx
+++ b/src/features/clusters/upsert/index.tsx
@@ -14,6 +14,7 @@ import { UpsertClusterSchema } from '@/features/clusters/upsert/upsertClusterSch
import { getOrganizationQueryOptions } from '@/features/organization/queries/getOrganizationQuery';
import { LocalStorageKeys, useLocalStorage } from '@/hooks/useLocalStorage';
import { SchemaPlan, SchemaRegion } from '@/lib/api.gen';
+import { sortByField } from '@/lib/arrays/sort/byField';
import { groupThenKeyBy } from '@/lib/groupThenKeyBy';
import { useQuery } from '@tanstack/react-query';
import { useParams } from '@tanstack/react-router';
@@ -33,9 +34,9 @@ export function UpsertCluster() {
const { data: regionLocations } = useQuery(getRegionLocationsOptions(limitRegionParameters));
const deploymentToPerformanceToPlan = useMemo>>(() =>
- groupThenKeyBy(planTypes || [], 'deploymentDescription', 'performanceDescription'), [planTypes]);
+ groupThenKeyBy(planTypes?.sort(sortByField('priceUsd')) || [], 'deploymentDescription', 'performanceDescription'), [planTypes]);
const regionNameToLatencyToRegion = useMemo>>(() =>
- groupThenKeyBy(regionLocations || [], 'region', 'latencyDescription'), [regionLocations]);
+ groupThenKeyBy(regionLocations?.sort(sortByField('latencyDescription')) || [], 'region', 'latencyDescription'), [regionLocations]);
const defaultValues = useMemo>(() => {
if (savedClusterState) {
@@ -81,6 +82,7 @@ export function UpsertCluster() {
}
return {
+ autoRenew: cluster?.plans?.[0]?.autoRenew ?? true,
systemName: cluster?.name ?? '',
abbreviatedName: cluster?.abbreviatedName ?? '',
deploymentDescription: selectedPlan?.deploymentDescription ?? defaults?.deploymentDescription ?? '',
diff --git a/src/features/clusters/upsert/lib/calculateDefaultDeploymentPerformanceAndRegionPlans.ts b/src/features/clusters/upsert/lib/calculateDefaultDeploymentPerformanceAndRegionPlans.ts
index 01118cd94..4ece76bbd 100644
--- a/src/features/clusters/upsert/lib/calculateDefaultDeploymentPerformanceAndRegionPlans.ts
+++ b/src/features/clusters/upsert/lib/calculateDefaultDeploymentPerformanceAndRegionPlans.ts
@@ -6,20 +6,21 @@ export function calculateDefaultDeploymentPerformanceAndRegionPlans(
planTypes: SchemaPlan[],
regionLocations: SchemaRegion[],
): null | Pick, 'deploymentDescription' | 'performanceDescription' | 'regionPlans'> {
- const freeColocatedPlan = planTypes
- .find(planType => !planType.priceUsd && planType.deploymentType === 'colocated');
- const allowedRegionIds = freeColocatedPlan?.allowedRegionIds;
- if (freeColocatedPlan && allowedRegionIds?.length) {
- const allowedFreeRegions = regionLocations.filter(regionLocation => allowedRegionIds.includes(regionLocation.id));
- const allowedGlobalFreeRegion = allowedFreeRegions.find(regionLocation => regionLocation.region === 'Global');
- if (allowedGlobalFreeRegion) {
+ const planToSelect = planTypes.find(planType => !planType.priceUsd && planType.deploymentType === 'colocated')
+ || planTypes.find(planType => planType.deploymentType === 'colocated')
+ || planTypes[0];
+ const allowedRegionIds = planToSelect?.allowedRegionIds;
+ if (planToSelect) {
+ const allowedRegions = allowedRegionIds ? regionLocations.filter(regionLocation => allowedRegionIds.includes(regionLocation.id)) : regionLocations;
+ const regionToSelect = allowedRegions.find(regionLocation => regionLocation.region === 'Global') || allowedRegions[0];
+ if (regionToSelect) {
return {
- deploymentDescription: freeColocatedPlan.deploymentDescription,
- performanceDescription: freeColocatedPlan.performanceDescription,
+ deploymentDescription: planToSelect.deploymentDescription,
+ performanceDescription: planToSelect.performanceDescription,
regionPlans: [
{
- regionName: allowedGlobalFreeRegion.region,
- latencyDescription: allowedGlobalFreeRegion.latencyDescription,
+ regionName: regionToSelect.region,
+ latencyDescription: regionToSelect.latencyDescription,
},
],
};
diff --git a/src/features/clusters/upsert/upsertClusterSchema.tsx b/src/features/clusters/upsert/upsertClusterSchema.tsx
index 2386c372d..d3f5ff3a2 100644
--- a/src/features/clusters/upsert/upsertClusterSchema.tsx
+++ b/src/features/clusters/upsert/upsertClusterSchema.tsx
@@ -6,6 +6,7 @@ export const UpsertClusterSchema = z.object({
systemName: z.string()
.min(1, 'Must be at least 1 character long.')
.max(255, 'Must be at most 255 characters long.'),
+ autoRenew: z.boolean(),
abbreviatedName: z
.string()
.max(10, 'Must be at most 10 characters long.')
diff --git a/src/lib/arrays/sort/byField.test.ts b/src/lib/arrays/sort/byField.test.ts
new file mode 100644
index 000000000..447bfc20a
--- /dev/null
+++ b/src/lib/arrays/sort/byField.test.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest';
+import { sortByField } from './byField';
+
+describe('sortByField', () => {
+ it('sorts objects by a string field in ascending order', () => {
+ const items = [
+ { id: 3, name: 'Charlie' },
+ { id: 1, name: 'Alice' },
+ { id: 2, name: 'Bob' },
+ ];
+
+ const result = items.sort(sortByField('name'));
+
+ expect(result.map((i) => i.name)).toEqual(['Alice', 'Bob', 'Charlie']);
+ });
+
+ it('sorts objects by a numeric field in ascending order', () => {
+ const items = [
+ { label: 'c', value: 30 },
+ { label: 'a', value: 10 },
+ { label: 'b', value: 20 },
+ ];
+
+ const result = items.sort(sortByField('value'));
+
+ expect(result.map((i) => i.value)).toEqual([10, 20, 30]);
+ });
+
+ it('comparator returns 0 when the field values are equal', () => {
+ const cmp = sortByField<{ id: number }>('id');
+ expect(cmp({ id: 1 }, { id: 1 })).toBe(0);
+ });
+});
diff --git a/src/lib/arrays/sort/byField.ts b/src/lib/arrays/sort/byField.ts
new file mode 100644
index 000000000..c9f4269a7
--- /dev/null
+++ b/src/lib/arrays/sort/byField.ts
@@ -0,0 +1,8 @@
+export function sortByField(fieldName: keyof T): (a: T, b: T) => number {
+ return (a: T, b: T): number => {
+ if (a[fieldName] === b[fieldName]) {
+ return 0;
+ }
+ return a[fieldName] > b[fieldName] ? 1 : -1;
+ };
+}
diff --git a/src/lib/arrays/sort/byNumberPrefix.test.ts b/src/lib/arrays/sort/byNumberPrefix.test.ts
new file mode 100644
index 000000000..472f3d49a
--- /dev/null
+++ b/src/lib/arrays/sort/byNumberPrefix.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from 'vitest';
+import { sortByNumberPrefix } from './byNumberPrefix';
+
+describe('sortByNumberPrefix', () => {
+ it('sorts strings by their leading numeric prefix in ascending order', () => {
+ const items = [
+ '1000ms, small',
+ '230ms, medium',
+ '50ms, large',
+ '5ms, x-large',
+ ];
+
+ const result = items.sort(sortByNumberPrefix);
+
+ expect(result).toEqual([
+ '5ms, x-large',
+ '50ms, large',
+ '230ms, medium',
+ '1000ms, small',
+ ]);
+ });
+
+ it('returns 0 when the numeric prefixes are equal (current behavior)', () => {
+ expect(sortByNumberPrefix('10 apples', '10 bananas')).toBe(0);
+ });
+
+ it('treats strings without a numeric prefix as 0', () => {
+ // "apple" has no number prefix -> treated as 0; should come before positive numbers
+ const items = ['apple', '2 bananas', 'banana'];
+ const result = items.sort(sortByNumberPrefix);
+
+ // "apple" and "banana" both treated as 0 and keep their relative order (sort is stable in modern engines)
+ expect(result[0]).toBe('apple');
+ expect(result[1]).toBe('banana');
+ expect(result[2]).toBe('2 bananas');
+
+ // direct comparator checks
+ expect(sortByNumberPrefix('apple', '2 bananas')).toBe(-1);
+ expect(sortByNumberPrefix('apple', 'banana')).toBe(0);
+ });
+
+ it('handles leading zeros correctly', () => {
+ const items = ['007sec, tiny', '7sec, small', '0008sec, medium'];
+ const result = items.sort(sortByNumberPrefix);
+ expect(result).toEqual(['007sec, tiny', '7sec, small', '0008sec, medium']);
+ });
+});
diff --git a/src/lib/arrays/sort/byNumberPrefix.ts b/src/lib/arrays/sort/byNumberPrefix.ts
new file mode 100644
index 000000000..f5fa2f46e
--- /dev/null
+++ b/src/lib/arrays/sort/byNumberPrefix.ts
@@ -0,0 +1,10 @@
+const numberPrefix = new RegExp(/^\d+/);
+
+export function sortByNumberPrefix(a: string, b: string): number {
+ const aPrefix = parseInt(a.match(numberPrefix)?.[0] || '0', 10);
+ const bPrefix = parseInt(b.match(numberPrefix)?.[0] || '0', 10);
+ if (aPrefix === bPrefix) {
+ return 0;
+ }
+ return aPrefix > bPrefix ? 1 : -1;
+}