common handler for versions

This commit is contained in:
Nate Kelley 2025-09-24 15:34:07 -06:00
parent 522e3ed500
commit 0d34f18eb4
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
10 changed files with 476 additions and 88 deletions

View File

@ -121,7 +121,6 @@ export const getDashboardAndInitializeMetrics = async ({
prefetchMetricsData?: boolean;
}) => {
const chosenVersionNumber = version_number === 'LATEST' ? undefined : version_number;
console.log('chosenVersionNumber', chosenVersionNumber);
return getDashboardById({
id: id || '',
password,

View File

@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearch } from '@tanstack/react-router';
import { useCallback, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useGetAssetVersionNumber } from '@/api/response-helpers/common-version-number';
import type { BusterDashboardResponse } from '../../asset_interfaces/dashboard/interfaces';
import { dashboardQueryKeys } from '../../query_keys/dashboard';
@ -10,33 +10,13 @@ const stableVersionSearchSelector = (state: { dashboard_version_number?: number
export const useGetDashboardVersionNumber = (
dashboardId: string,
versionNumber: number | 'LATEST' = 'LATEST'
versionNumber: number | 'LATEST' | undefined
) => {
const { data: latestVersionNumber } = useQuery({
...dashboardQueryKeys.dashboardGetDashboard(dashboardId, 'LATEST'),
enabled: false,
select: stableVersionDataSelector,
});
// Get the dashboard_version_number query param from the route
const paramVersionNumber = useSearch({
select: stableVersionSearchSelector,
strict: false,
});
const isLatest = versionNumber === 'LATEST' || latestVersionNumber === versionNumber;
const selectedVersionNumber = isLatest
? ('LATEST' as const)
: (versionNumber ?? paramVersionNumber ?? 'LATEST');
return useMemo(
() => ({
selectedVersionNumber,
latestVersionNumber,
paramVersionNumber,
}),
[latestVersionNumber, selectedVersionNumber, paramVersionNumber]
return useGetAssetVersionNumber(
dashboardQueryKeys.dashboardGetDashboard(dashboardId, 'LATEST'),
versionNumber,
stableVersionDataSelector,
stableVersionSearchSelector
);
};

View File

@ -12,6 +12,7 @@ import {
setProtectedAssetPasswordError,
useProtectedAssetPassword,
} from '@/context/BusterAssets/useProtectedAssetStore';
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
import { isQueryStale } from '@/lib/query';
import { hasOrganizationId } from '../../users/userQueryHelpers';
import {
@ -79,8 +80,8 @@ export const usePrefetchGetDashboardClient = <TData = GetDashboardResponse>(
params?: Omit<UseQueryOptions<GetDashboardResponse, RustApiError, TData>, 'queryKey' | 'queryFn'>
) => {
const queryClient = useQueryClient();
const queryFn = useGetDashboardAndInitializeMetrics({ prefetchData: false });
return (id: string, versionNumber: number | 'LATEST' = 'LATEST') => {
return useMemoizedFn((id: string, versionNumber: number | 'LATEST') => {
const queryFn = useGetDashboardAndInitializeMetrics({ prefetchData: false });
const getDashboardQueryKey = dashboardQueryKeys.dashboardGetDashboard(id, versionNumber);
const isStale = isQueryStale(getDashboardQueryKey, queryClient) || params?.staleTime === 0;
if (!isStale) return;
@ -95,7 +96,7 @@ export const usePrefetchGetDashboardClient = <TData = GetDashboardResponse>(
}),
...params,
});
};
});
};
/**

View File

@ -13,7 +13,7 @@ import type {
BusterMetricDataExtended,
} from '@/api/asset_interfaces/metric';
import { metricsQueryKeys } from '@/api/query_keys/metric';
import { silenceAssetErrors } from '@/api/repsonse-helpers/silenece-asset-errors';
import { silenceAssetErrors } from '@/api/response-helpers/silenece-asset-errors';
import {
setProtectedAssetPasswordError,
useProtectedAssetPassword,

View File

@ -1,6 +1,6 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSearch } from '@tanstack/react-router';
import { useCallback, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
import { useGetAssetVersionNumber } from '@/api/response-helpers/common-version-number';
import type { BusterMetric } from '../../asset_interfaces/metric';
import { metricsQueryKeys } from '../../query_keys/metric';
@ -10,33 +10,13 @@ const stableVersionSearchSelector = (state: { metric_version_number?: number | u
export const useGetMetricVersionNumber = (
metricId: string,
versionNumber: number | 'LATEST' = 'LATEST'
versionNumber: number | 'LATEST' | undefined
) => {
const { data: latestVersionNumber } = useQuery({
...metricsQueryKeys.metricsGetMetric(metricId, 'LATEST'),
enabled: false,
select: stableVersionDataSelector,
});
// Get the metric_version_number query param from the route
const paramVersionNumber = useSearch({
select: stableVersionSearchSelector,
strict: false,
});
const isLatest = versionNumber === 'LATEST' || latestVersionNumber === versionNumber;
const selectedVersionNumber = isLatest
? ('LATEST' as const)
: (versionNumber ?? paramVersionNumber ?? 'LATEST');
return useMemo(
() => ({
paramVersionNumber,
latestVersionNumber,
selectedVersionNumber,
}),
[paramVersionNumber, latestVersionNumber, selectedVersionNumber]
return useGetAssetVersionNumber(
metricsQueryKeys.metricsGetMetric(metricId, 'LATEST'),
versionNumber,
stableVersionDataSelector,
stableVersionSearchSelector
);
};

View File

@ -9,7 +9,7 @@ import {
import { create } from 'mutative';
import { collectionQueryKeys } from '@/api/query_keys/collection';
import { reportsQueryKeys } from '@/api/query_keys/reports';
import { silenceAssetErrors } from '@/api/repsonse-helpers/silenece-asset-errors';
import { silenceAssetErrors } from '@/api/response-helpers/silenece-asset-errors';
import { useProtectedAssetPassword } from '@/context/BusterAssets/useProtectedAssetStore';
import type { RustApiError } from '../../errors';
import {

View File

@ -1,8 +1,6 @@
import type { GetReportResponse } from '@buster/server-shared/reports';
import { useQuery } from '@tanstack/react-query';
import { useSearch } from '@tanstack/react-router';
import { useMemo } from 'react';
import { reportsQueryKeys } from '@/api/query_keys/reports';
import { useGetAssetVersionNumber } from '@/api/response-helpers/common-version-number';
const stableVersionDataSelector = (data: GetReportResponse) => data.version_number;
const stableVersionSearchSelector = (state: { report_version_number?: number | undefined }) =>
@ -10,27 +8,12 @@ const stableVersionSearchSelector = (state: { report_version_number?: number | u
export const useGetReportVersionNumber = (
reportId: string,
versionNumber: number | 'LATEST' = 'LATEST'
versionNumber: number | 'LATEST' | undefined
) => {
const { data: latestVersionNumber } = useQuery({
...reportsQueryKeys.reportsGetReport(reportId, 'LATEST'),
enabled: false,
select: stableVersionDataSelector,
});
const paramVersionNumber = useSearch({
select: stableVersionSearchSelector,
strict: false,
});
const isLatest = versionNumber === 'LATEST' || latestVersionNumber === versionNumber;
const selectedVersionNumber = isLatest
? ('LATEST' as const)
: (versionNumber ?? paramVersionNumber ?? 'LATEST');
return useMemo(
() => ({ paramVersionNumber, selectedVersionNumber, latestVersionNumber }),
[paramVersionNumber, selectedVersionNumber, latestVersionNumber]
return useGetAssetVersionNumber(
reportsQueryKeys.reportsGetReport(reportId, 'LATEST'),
versionNumber,
stableVersionDataSelector,
stableVersionSearchSelector
);
};

View File

@ -0,0 +1,412 @@
import { renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useGetAssetVersionNumber } from './common-version-number';
// Mock dependencies
vi.mock('@tanstack/react-query', () => ({
useQuery: vi.fn(),
}));
vi.mock('@tanstack/react-router', () => ({
useSearch: vi.fn(),
}));
// Import mocked modules
import { useQuery } from '@tanstack/react-query';
import { useSearch } from '@tanstack/react-router';
const mockedUseQuery = vi.mocked(useQuery);
const mockedUseSearch = vi.mocked(useSearch);
describe('useGetAssetVersionNumber', () => {
// Mock query options and selector functions
const mockQueryOptions = {
queryKey: ['test-asset'],
queryFn: () => Promise.resolve({ version: 5 }),
};
const mockStableVersionDataSelector = vi.fn((data: { version: number }) => data.version);
const mockStableVersionSearchSelector = vi.fn((search: any) => search.version);
beforeEach(() => {
vi.clearAllMocks();
// Default mock implementations
mockedUseQuery.mockReturnValue({
data: undefined,
isLoading: false,
error: null,
} as any);
mockedUseSearch.mockReturnValue(undefined);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('basic functionality', () => {
it('should return correct structure with default values', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current).toEqual({
paramVersionNumber: undefined,
selectedVersionNumber: 'LATEST',
latestVersionNumber: undefined,
});
});
it('should call useQuery with correct parameters', () => {
renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
3,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(mockedUseQuery).toHaveBeenCalledWith({
...mockQueryOptions,
enabled: false,
select: mockStableVersionDataSelector,
});
});
it('should call useSearch with correct parameters', () => {
renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
3,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(mockedUseSearch).toHaveBeenCalledWith({
select: mockStableVersionSearchSelector,
strict: false,
});
});
});
describe('version number handling', () => {
it('should return LATEST when versionNumber is undefined', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
it('should return LATEST when versionNumber is explicitly LATEST', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
'LATEST',
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
it('should return specific version number when provided', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
3,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe(3);
});
});
describe('isLatest logic', () => {
it('should consider version as latest when it matches latestVersionNumber', () => {
mockedUseQuery.mockReturnValue({
data: 5,
isLoading: false,
error: null,
} as any);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
5, // matches latest
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
it('should not consider version as latest when it does not match latestVersionNumber', () => {
mockedUseQuery.mockReturnValue({
data: 5,
isLoading: false,
error: null,
} as any);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
3, // does not match latest (5)
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe(3);
});
it('should return LATEST when no versionNumber but latestVersionNumber exists', () => {
mockedUseQuery.mockReturnValue({
data: 5,
isLoading: false,
error: null,
} as any);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
});
describe('paramVersionNumber from search', () => {
it('should use paramVersionNumber when versionNumber is undefined', () => {
mockedUseSearch.mockReturnValue(7);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.paramVersionNumber).toBe(7);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
it('should prefer explicit versionNumber over paramVersionNumber', () => {
mockedUseSearch.mockReturnValue(7);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
3, // explicit version
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.paramVersionNumber).toBe(7);
expect(result.current.selectedVersionNumber).toBe(3); // uses explicit version
});
it('should fall back to LATEST when no versionNumber and no paramVersionNumber', () => {
mockedUseSearch.mockReturnValue(undefined);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.paramVersionNumber).toBe(undefined);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
});
describe('edge cases', () => {
it('should handle zero as a valid version number', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
0,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe('LATEST');
});
it('should handle negative version numbers', () => {
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
-1,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.selectedVersionNumber).toBe(-1);
});
it('should handle when latestVersionNumber is undefined', () => {
mockedUseQuery.mockReturnValue({
data: undefined,
isLoading: false,
error: null,
} as any);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
5,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
expect(result.current.latestVersionNumber).toBe(undefined);
expect(result.current.selectedVersionNumber).toBe(5);
});
});
describe('memoization', () => {
it('should return same object reference when dependencies do not change', () => {
mockedUseQuery.mockReturnValue({
data: 5,
isLoading: false,
error: null,
} as any);
mockedUseSearch.mockReturnValue(3);
const { result, rerender } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
2,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
const firstResult = result.current;
// Rerender without changing dependencies
rerender();
expect(result.current).toBe(firstResult); // Same reference due to useMemo
});
it('should return new object when latestVersionNumber changes', () => {
let latestVersion = 5;
mockedUseQuery.mockImplementation(
() =>
({
data: latestVersion,
isLoading: false,
error: null,
}) as any
);
mockedUseSearch.mockReturnValue(3);
const { result, rerender } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
2,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
const firstResult = result.current;
// Change the latest version
latestVersion = 6;
rerender();
expect(result.current).not.toBe(firstResult); // Different reference
expect(result.current.latestVersionNumber).toBe(6);
});
});
describe('selector function integration', () => {
it('should call stableVersionDataSelector with query data', () => {
const mockData = { version: 10, name: 'test' };
mockedUseQuery.mockReturnValue({
data: mockData,
isLoading: false,
error: null,
} as any);
renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
5,
mockStableVersionDataSelector,
mockStableVersionSearchSelector
)
);
// The selector should be passed to useQuery, but not called directly in our hook
expect(mockStableVersionDataSelector).not.toHaveBeenCalled();
});
it('should work with custom selector functions', () => {
const customDataSelector = vi.fn((data: any) => data?.customVersion || 0);
const customSearchSelector = vi.fn((search: any) => search?.customParam);
mockedUseQuery.mockReturnValue({
data: 15, // This would be the result after selector is applied
isLoading: false,
error: null,
} as any);
mockedUseSearch.mockReturnValue(8);
const { result } = renderHook(() =>
useGetAssetVersionNumber(
mockQueryOptions,
undefined,
customDataSelector,
customSearchSelector
)
);
expect(result.current.latestVersionNumber).toBe(15);
expect(result.current.paramVersionNumber).toBe(8);
expect(mockedUseQuery).toHaveBeenCalledWith({
...mockQueryOptions,
enabled: false,
select: customDataSelector,
});
expect(mockedUseSearch).toHaveBeenCalledWith({
select: customSearchSelector,
strict: false,
});
});
});
});

View File

@ -0,0 +1,33 @@
import { type UseQueryOptions, useQuery } from '@tanstack/react-query';
import { useSearch } from '@tanstack/react-router';
import { useMemo } from 'react';
export const useGetAssetVersionNumber = <TQueryData, TError = unknown>(
query: UseQueryOptions<TQueryData, TError>,
versionNumber: number | 'LATEST' | undefined,
stableVersionDataSelector: (data: TQueryData) => number,
stableVersionSearchSelector: (state: ReturnType<typeof useSearch>['select']) => number | undefined
) => {
const { data: latestVersionNumber } = useQuery({
...query,
enabled: false,
select: stableVersionDataSelector,
});
const paramVersionNumber = useSearch({
select: stableVersionSearchSelector,
strict: false,
});
const isLatest =
!versionNumber || versionNumber === 'LATEST' || latestVersionNumber === versionNumber;
const selectedVersionNumber = isLatest
? ('LATEST' as const)
: (versionNumber ?? paramVersionNumber ?? 'LATEST');
return useMemo(
() => ({ paramVersionNumber, selectedVersionNumber, latestVersionNumber }),
[paramVersionNumber, selectedVersionNumber, latestVersionNumber]
);
};