mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-24 21:30:26 +00:00
Compare commits
1 Commits
refactor/c
...
fix/hosts-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df66e72b4 |
@@ -13,7 +13,9 @@ export interface HostListPayload {
|
||||
orderBy?: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
} | null;
|
||||
start?: number;
|
||||
end?: number;
|
||||
}
|
||||
|
||||
export interface TimeSeriesValue {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useEffect } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { refreshIntervalOptions } from 'container/TopNav/AutoRefreshV2/constants';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { createCustomTimeRange } from '../../store/globalTime/utils';
|
||||
|
||||
/**
|
||||
* Adapter component that syncs Redux global time state to Zustand store.
|
||||
* This component should be rendered once at the app level.
|
||||
*
|
||||
* It reads from the Redux globalTime reducer and updates the Zustand store
|
||||
* to provide a migration path from Redux to Zustand.
|
||||
*/
|
||||
export function GlobalTimeStoreAdapter(): null {
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const setSelectedTime = useGlobalTimeStore((s) => s.setSelectedTime);
|
||||
|
||||
useEffect(() => {
|
||||
// Convert the selectedTime to the new format
|
||||
// If it's 'custom', store the min/max times in the custom format
|
||||
const selectedTime =
|
||||
globalTime.selectedTime === 'custom'
|
||||
? createCustomTimeRange(globalTime.minTime, globalTime.maxTime)
|
||||
: globalTime.selectedTime;
|
||||
|
||||
// Find refresh interval from Redux state
|
||||
const refreshOption = refreshIntervalOptions.find(
|
||||
(option) => option.key === globalTime.selectedAutoRefreshInterval,
|
||||
);
|
||||
|
||||
const refreshInterval =
|
||||
!globalTime.isAutoRefreshDisabled && refreshOption ? refreshOption.value : 0;
|
||||
|
||||
setSelectedTime(selectedTime, refreshInterval);
|
||||
}, [
|
||||
globalTime.selectedTime,
|
||||
globalTime.isAutoRefreshDisabled,
|
||||
globalTime.selectedAutoRefreshInterval,
|
||||
globalTime.minTime,
|
||||
globalTime.maxTime,
|
||||
setSelectedTime,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { act, render, renderHook } from '@testing-library/react';
|
||||
import configureStore, { MockStoreEnhanced } from 'redux-mock-store';
|
||||
import { useGlobalTimeStore } from 'store/globalTime/globalTimeStore';
|
||||
import { createCustomTimeRange } from 'store/globalTime/utils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { GlobalTimeStoreAdapter } from '../GlobalTimeStoreAdapter';
|
||||
|
||||
const mockStore = configureStore<Partial<AppState>>([]);
|
||||
|
||||
describe('GlobalTimeStoreAdapter', () => {
|
||||
let store: MockStoreEnhanced<Partial<AppState>>;
|
||||
|
||||
const createGlobalTimeState = (
|
||||
overrides: Partial<GlobalReducer> = {},
|
||||
): GlobalReducer => ({
|
||||
minTime: 1700000000000000000,
|
||||
maxTime: 1700000001000000000,
|
||||
loading: false,
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset Zustand store before each test
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime('30s', 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render null because it just an adapter', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState(),
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('should sync relative time from Redux to Zustand store', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: 'off',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync custom time from Redux to Zustand store', () => {
|
||||
const minTime = 1700000000000000000;
|
||||
const maxTime = 1700000001000000000;
|
||||
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: 'custom',
|
||||
minTime,
|
||||
maxTime,
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(
|
||||
createCustomTimeRange(minTime, maxTime),
|
||||
);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should sync refresh interval when auto refresh is enabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '5s',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
expect(result.current.refreshInterval).toBe(5000); // 5s = 5000ms
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should set refreshInterval to 0 when auto refresh is disabled', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
selectedAutoRefreshInterval: '5s', // Even with interval set, should be 0 when disabled
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should update Zustand store when Redux state changes', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: true,
|
||||
}),
|
||||
});
|
||||
|
||||
const { rerender } = render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify initial state
|
||||
let zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('15m');
|
||||
|
||||
// Update Redux store
|
||||
const newStore = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '1h',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: '30s',
|
||||
}),
|
||||
});
|
||||
|
||||
rerender(
|
||||
<Provider store={newStore}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
// Verify updated state
|
||||
zustandState = renderHook(() => useGlobalTimeStore());
|
||||
expect(zustandState.result.current.selectedTime).toBe('1h');
|
||||
expect(zustandState.result.current.refreshInterval).toBe(30000); // 30s = 30000ms
|
||||
expect(zustandState.result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle various refresh interval options', () => {
|
||||
const testCases = [
|
||||
{ key: '5s', expectedValue: 5000 },
|
||||
{ key: '10s', expectedValue: 10000 },
|
||||
{ key: '30s', expectedValue: 30000 },
|
||||
{ key: '1m', expectedValue: 60000 },
|
||||
{ key: '5m', expectedValue: 300000 },
|
||||
];
|
||||
|
||||
testCases.forEach(({ key, expectedValue }) => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: key,
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(expectedValue);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown refresh interval by setting 0', () => {
|
||||
store = mockStore({
|
||||
globalTime: createGlobalTimeState({
|
||||
selectedTime: '15m',
|
||||
isAutoRefreshDisabled: false,
|
||||
selectedAutoRefreshInterval: 'unknown-interval',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,11 @@
|
||||
export const REACT_QUERY_KEY = {
|
||||
/**
|
||||
* For any query that should support AutoRefresh and min/max time is from DateTimeSelectionV2
|
||||
* You can prefix the query with this KEY, it will allow the queries to be automatically refreshed
|
||||
* when the user clicks in the refresh button, or alert the user when the data is being refreshed.
|
||||
*/
|
||||
AUTO_REFRESH_QUERY: 'AUTO_REFRESH_QUERY',
|
||||
|
||||
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
|
||||
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
|
||||
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
|
||||
import {
|
||||
getHostLists,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import HostMetricDetail from 'components/HostMetricsDetail';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
@@ -16,40 +19,40 @@ import {
|
||||
} from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { FeatureKeys } from '../../constants/features';
|
||||
import { REACT_QUERY_KEY } from '../../constants/reactQueryKeys';
|
||||
import { useAppContext } from '../../providers/App/App';
|
||||
import HostsListControls from './HostsListControls';
|
||||
import HostsListTable from './HostsListTable';
|
||||
import { getHostListsQuery, GetHostsQuickFiltersConfig } from './utils';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
const defaultFilters: TagFilter = { items: [], op: 'and' };
|
||||
const baseQuery = getHostListsQuery();
|
||||
|
||||
function HostsList(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
const filters = getFiltersFromParams(
|
||||
const filtersFromParams = getFiltersFromParams(
|
||||
searchParams,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
|
||||
);
|
||||
if (!filters) {
|
||||
return {
|
||||
items: [],
|
||||
op: 'and',
|
||||
};
|
||||
}
|
||||
return filters;
|
||||
return filtersFromParams ?? defaultFilters;
|
||||
});
|
||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||
|
||||
@@ -83,57 +86,48 @@ function HostsList(): JSX.Element {
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize('hosts');
|
||||
|
||||
const query = useMemo(() => {
|
||||
const baseQuery = getHostListsQuery();
|
||||
return {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy,
|
||||
};
|
||||
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
|
||||
const selectedTime = useGlobalTimeStore((store) => store.selectedTime);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedHostName) {
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'hostList',
|
||||
const queryKey = useMemo(
|
||||
() => [
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
REACT_QUERY_KEY.GET_HOST_LIST,
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
pageSize,
|
||||
currentPage,
|
||||
filters,
|
||||
orderBy,
|
||||
selectedHostName,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetHostList(
|
||||
query as HostListPayload,
|
||||
{
|
||||
queryKey,
|
||||
enabled: !!query,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
selectedTime,
|
||||
],
|
||||
[pageSize, currentPage, filters, orderBy, selectedTime],
|
||||
);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery<
|
||||
SuccessResponse<HostListResponse> | ErrorResponse,
|
||||
Error
|
||||
>({
|
||||
queryKey,
|
||||
queryFn: ({ signal }) => {
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
const payload: HostListPayload = {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters: filters ?? defaultFilters,
|
||||
orderBy,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
};
|
||||
|
||||
return getHostLists(payload, signal);
|
||||
},
|
||||
enabled: true,
|
||||
refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
});
|
||||
|
||||
const hostMetricsData = useMemo(() => data?.payload?.data?.records || [], [
|
||||
data,
|
||||
]);
|
||||
@@ -253,7 +247,7 @@ function HostsList(): JSX.Element {
|
||||
isError={isError}
|
||||
tableData={data}
|
||||
hostMetricsData={hostMetricsData}
|
||||
filters={filters || { items: [], op: 'AND' }}
|
||||
filters={filters ?? defaultFilters}
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
onHostClick={handleHostClick}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import * as getHostListsApi from 'api/infraMonitoring/getHostLists';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
@@ -18,6 +18,10 @@ jest.mock('lib/getMinMax', () => ({
|
||||
maxTime: 1713738000000,
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
getMinMaxForSelectedTime: jest.fn().mockReturnValue({
|
||||
minTime: 1713734400000000000,
|
||||
maxTime: 1713738000000000000,
|
||||
}),
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
@@ -30,7 +34,13 @@ jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
@@ -82,27 +92,40 @@ jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(useGetHostListHooks, 'useGetHostList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
},
|
||||
],
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
|
||||
jest.spyOn(getHostListsApi, 'getHostLists').mockResolvedValue({
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'Success',
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
os: 'linux',
|
||||
cpu: 0.75,
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memory: 0.65,
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
wait: 0.03,
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15: 0.5,
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
},
|
||||
],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
params: {} as any,
|
||||
});
|
||||
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
@@ -128,20 +151,11 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
} as any);
|
||||
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
it('renders hosts list table', async () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
@@ -151,6 +165,23 @@ describe('HostsList', () => {
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders filters', async () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
|
||||
import { Progress, Tag, Tooltip, Typography } from 'antd';
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
import {
|
||||
HostData,
|
||||
@@ -12,16 +12,12 @@ import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import TabLabel from 'components/TabLabel';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { TriangleAlert } from 'lucide-react';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import HostsList from './HostsList';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
|
||||
export interface HostRowData {
|
||||
@@ -127,14 +123,6 @@ export const getHostListsQuery = (): HostListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
export const getTabsItems = (): TabsProps['items'] => [
|
||||
{
|
||||
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
||||
key: PANEL_TYPES.LIST,
|
||||
children: <HostsList />,
|
||||
},
|
||||
];
|
||||
|
||||
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
{
|
||||
title: <div className="hostname-column-header">Hostname</div>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useIsFetching, useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
@@ -34,6 +35,7 @@ import { persistTimeDurationForRoute } from 'utils/metricsTimeStorageUtils';
|
||||
import { normalizeTimeToMs } from 'utils/timeUtils';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { REACT_QUERY_KEY } from '../../../constants/reactQueryKeys';
|
||||
import AutoRefresh from '../AutoRefreshV2';
|
||||
import { DateTimeRangeType } from '../CustomDateTimeModal';
|
||||
import {
|
||||
@@ -352,7 +354,14 @@ function DateTimeSelection({
|
||||
],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const isRefreshingQueries = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
});
|
||||
const onRefreshHandler = (): void => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
});
|
||||
onSelectHandler(selectedTime);
|
||||
onLastRefreshHandler();
|
||||
};
|
||||
@@ -732,7 +741,13 @@ function DateTimeSelection({
|
||||
{showAutoRefresh && selectedTime !== 'custom' && (
|
||||
<div className="refresh-actions">
|
||||
<FormItem hidden={refreshButtonHidden} className="refresh-btn">
|
||||
<Button icon={<SyncOutlined />} onClick={onRefreshHandler} />
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={!!isRefreshingQueries}
|
||||
onClick={(): void => {
|
||||
onRefreshHandler();
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getHostLists,
|
||||
HostListPayload,
|
||||
HostListResponse,
|
||||
} from 'api/infraMonitoring/getHostLists';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type UseGetHostList = (
|
||||
requestData: HostListPayload,
|
||||
options?: UseQueryOptions<
|
||||
SuccessResponse<HostListResponse> | ErrorResponse,
|
||||
Error
|
||||
>,
|
||||
headers?: Record<string, string>,
|
||||
) => UseQueryResult<SuccessResponse<HostListResponse> | ErrorResponse, Error>;
|
||||
|
||||
export const useGetHostList: UseGetHostList = (
|
||||
requestData,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queryKey = useMemo(() => {
|
||||
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||
return [...options.queryKey];
|
||||
}
|
||||
|
||||
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||
return options.queryKey;
|
||||
}
|
||||
|
||||
return [REACT_QUERY_KEY.GET_HOST_LIST, requestData];
|
||||
}, [options?.queryKey, requestData]);
|
||||
|
||||
return useQuery<SuccessResponse<HostListResponse> | ErrorResponse, Error>({
|
||||
queryFn: ({ signal }) => getHostLists(requestData, signal, headers),
|
||||
...options,
|
||||
queryKey,
|
||||
});
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query/devtools';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import AppRoutes from 'AppRoutes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
@@ -51,6 +53,8 @@ if (container) {
|
||||
<TimezoneProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Provider store={store}>
|
||||
<GlobalTimeStoreAdapter />
|
||||
<ReactQueryDevtools />
|
||||
<AppProvider>
|
||||
<AppRoutes />
|
||||
</AppProvider>
|
||||
|
||||
193
frontend/src/store/globalTime/__tests__/globalTimeStore.test.ts
Normal file
193
frontend/src/store/globalTime/__tests__/globalTimeStore.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import { useGlobalTimeStore } from '../globalTimeStore';
|
||||
import { createCustomTimeRange } from '../utils';
|
||||
|
||||
describe('globalTimeStore', () => {
|
||||
beforeEach(() => {
|
||||
// Reset the store state before each test
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime('30s', 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should have default selectedTime of 30s', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe('30s');
|
||||
});
|
||||
|
||||
it('should have isRefreshEnabled as false by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should have refreshInterval as 0 by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTime', () => {
|
||||
it('should update selectedTime', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update refreshInterval when provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should keep existing refreshInterval when not provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should enable refresh for relative time with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh for relative time with refreshInterval = 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable refresh for custom time range even with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime, 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should handle various relative time formats', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const timeFormats = ['1m', '5m', '15m', '30m', '1h', '3h', '6h', '1d', '1w'];
|
||||
|
||||
timeFormats.forEach((time) => {
|
||||
act(() => {
|
||||
result.current.setSelectedTime(time, 10000);
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe(time);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMaxTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return min/max time for custom time range', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
const {
|
||||
minTime: resultMin,
|
||||
maxTime: resultMax,
|
||||
} = result.current.getMinMaxTime();
|
||||
expect(resultMin).toBe(minTime);
|
||||
expect(resultMax).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should compute fresh min/max time for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
const now = Date.now() * 1000000;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * 1000000;
|
||||
|
||||
expect(maxTime).toBe(now);
|
||||
expect(minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should return different values on subsequent calls for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const first = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time by 1 second
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const second = result.current.getMinMaxTime();
|
||||
|
||||
// maxTime should be different (1 second later)
|
||||
expect(second.maxTime).toBe(first.maxTime + 1000 * 1000000);
|
||||
expect(second.minTime).toBe(first.minTime + 1000 * 1000000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should share state between multiple hook instances', () => {
|
||||
const { result: result1 } = renderHook(() => useGlobalTimeStore());
|
||||
const { result: result2 } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result1.current.setSelectedTime('1h', 10000);
|
||||
});
|
||||
|
||||
expect(result2.current.selectedTime).toBe('1h');
|
||||
expect(result2.current.refreshInterval).toBe(10000);
|
||||
expect(result2.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
138
frontend/src/store/globalTime/__tests__/utils.test.ts
Normal file
138
frontend/src/store/globalTime/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
isCustomTimeRange,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from '../utils';
|
||||
|
||||
describe('globalTime/utils', () => {
|
||||
describe('CUSTOM_TIME_SEPARATOR', () => {
|
||||
it('should be defined as ||_||', () => {
|
||||
expect(CUSTOM_TIME_SEPARATOR).toBe('||_||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomTimeRange', () => {
|
||||
it('should return true for custom time range strings', () => {
|
||||
expect(isCustomTimeRange('1000000000||_||2000000000')).toBe(true);
|
||||
expect(isCustomTimeRange('0||_||0')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for relative time strings', () => {
|
||||
expect(isCustomTimeRange('15m')).toBe(false);
|
||||
expect(isCustomTimeRange('1h')).toBe(false);
|
||||
expect(isCustomTimeRange('1d')).toBe(false);
|
||||
expect(isCustomTimeRange('30s')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for empty string', () => {
|
||||
expect(isCustomTimeRange('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCustomTimeRange', () => {
|
||||
it('should create a custom time range string from min and max times', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const result = createCustomTimeRange(minTime, maxTime);
|
||||
expect(result).toBe(`${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`);
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
const result = createCustomTimeRange(0, 0);
|
||||
expect(result).toBe(`0${CUSTOM_TIME_SEPARATOR}0`);
|
||||
});
|
||||
|
||||
it('should handle large nanosecond timestamps', () => {
|
||||
const minTime = 1700000000000000000;
|
||||
const maxTime = 1700000001000000000;
|
||||
const result = createCustomTimeRange(minTime, maxTime);
|
||||
expect(result).toBe(`${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseCustomTimeRange', () => {
|
||||
it('should parse a valid custom time range string', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const timeString = `${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`;
|
||||
const result = parseCustomTimeRange(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return null for non-custom time range strings', () => {
|
||||
expect(parseCustomTimeRange('15m')).toBeNull();
|
||||
expect(parseCustomTimeRange('1h')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for invalid numeric values', () => {
|
||||
expect(parseCustomTimeRange(`abc${CUSTOM_TIME_SEPARATOR}def`)).toBeNull();
|
||||
expect(parseCustomTimeRange(`123${CUSTOM_TIME_SEPARATOR}def`)).toBeNull();
|
||||
expect(parseCustomTimeRange(`abc${CUSTOM_TIME_SEPARATOR}456`)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle zero values', () => {
|
||||
const result = parseCustomTimeRange(`0${CUSTOM_TIME_SEPARATOR}0`);
|
||||
expect(result).toEqual({ minTime: 0, maxTime: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSelectedTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should parse custom time range and return min/max values', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const timeString = createCustomTimeRange(minTime, maxTime);
|
||||
const result = parseSelectedTime(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return fallback for invalid custom time range', () => {
|
||||
const invalidCustom = `invalid${CUSTOM_TIME_SEPARATOR}values`;
|
||||
const result = parseSelectedTime(invalidCustom);
|
||||
const now = Date.now() * 1000000;
|
||||
const fallbackDuration = 30 * 1000 * 1000000; // 30s in nanoseconds
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - fallbackDuration);
|
||||
});
|
||||
|
||||
it('should parse relative time strings using getMinMaxForSelectedTime', () => {
|
||||
const result = parseSelectedTime('15m');
|
||||
const now = Date.now() * 1000000;
|
||||
// 15 minutes in nanoseconds
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * 1000000;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should parse 1h relative time', () => {
|
||||
const result = parseSelectedTime('1h');
|
||||
const now = Date.now() * 1000000;
|
||||
// 1 hour in nanoseconds
|
||||
const oneHourNs = 60 * 60 * 1000 * 1000000;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - oneHourNs);
|
||||
});
|
||||
|
||||
it('should parse 1d relative time', () => {
|
||||
const result = parseSelectedTime('1d');
|
||||
const now = Date.now() * 1000000;
|
||||
// 1 day in nanoseconds
|
||||
const oneDayNs = 24 * 60 * 60 * 1000 * 1000000;
|
||||
|
||||
expect(result.maxTime).toBe(now);
|
||||
expect(result.minTime).toBe(now - oneDayNs);
|
||||
});
|
||||
});
|
||||
});
|
||||
32
frontend/src/store/globalTime/globalTimeStore.ts
Normal file
32
frontend/src/store/globalTime/globalTimeStore.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { IGlobalTimeStoreActions, IGlobalTimeStoreState } from './types';
|
||||
import { isCustomTimeRange, parseSelectedTime } from './utils';
|
||||
|
||||
export type IGlobalTimeStore = IGlobalTimeStoreState & IGlobalTimeStoreActions;
|
||||
|
||||
export const useGlobalTimeStore = create<IGlobalTimeStore>((set, get) => ({
|
||||
// Initial state
|
||||
selectedTime: '30s',
|
||||
isRefreshEnabled: false,
|
||||
refreshInterval: 0,
|
||||
|
||||
// Actions
|
||||
setSelectedTime: (selectedTime, refreshInterval): void => {
|
||||
set((state) => {
|
||||
const newRefreshInterval = refreshInterval ?? state.refreshInterval;
|
||||
const isCustom = isCustomTimeRange(selectedTime);
|
||||
|
||||
return {
|
||||
selectedTime,
|
||||
refreshInterval: newRefreshInterval,
|
||||
isRefreshEnabled: !isCustom && newRefreshInterval > 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getMinMaxTime: (): { minTime: number; maxTime: number } => {
|
||||
const { selectedTime } = get();
|
||||
return parseSelectedTime(selectedTime);
|
||||
},
|
||||
}));
|
||||
9
frontend/src/store/globalTime/index.ts
Normal file
9
frontend/src/store/globalTime/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export { useGlobalTimeStore } from './globalTimeStore';
|
||||
export type { IGlobalTimeStoreState, ParsedTimeRange } from './types';
|
||||
export {
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
isCustomTimeRange,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from './utils';
|
||||
43
frontend/src/store/globalTime/types.ts
Normal file
43
frontend/src/store/globalTime/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export interface IGlobalTimeStoreState {
|
||||
/**
|
||||
* The selected time range, can be:
|
||||
* - Relative duration: '1m', '5m', '15m', '1h', '1d', etc.
|
||||
* - Custom range: '<minTimeUnixNano>||_||<maxTimeUnixNano>' format
|
||||
*/
|
||||
selectedTime: string;
|
||||
|
||||
/**
|
||||
* Whether auto-refresh is enabled.
|
||||
* Automatically computed: true for duration-based times, false for custom ranges.
|
||||
*/
|
||||
isRefreshEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The refresh interval in milliseconds (e.g., 5000 for 5s, 30000 for 30s)
|
||||
* Only used when isRefreshEnabled is true
|
||||
*/
|
||||
refreshInterval: number;
|
||||
}
|
||||
|
||||
export interface ParsedTimeRange {
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
}
|
||||
|
||||
export interface IGlobalTimeStoreActions {
|
||||
/**
|
||||
* Set the selected time and optionally the refresh interval.
|
||||
* isRefreshEnabled is automatically computed:
|
||||
* - Custom time ranges: always false
|
||||
* - Duration times with refreshInterval > 0: true
|
||||
* - Duration times with refreshInterval = 0: false
|
||||
*/
|
||||
setSelectedTime: (selectedTime: string, refreshInterval?: number) => void;
|
||||
|
||||
/**
|
||||
* Get the current min/max time values parsed from selectedTime.
|
||||
* For durations, computes fresh values based on Date.now().
|
||||
* For custom ranges, extracts the stored values.
|
||||
*/
|
||||
getMinMaxTime: () => { minTime: number; maxTime: number };
|
||||
}
|
||||
70
frontend/src/store/globalTime/utils.ts
Normal file
70
frontend/src/store/globalTime/utils.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
|
||||
import { ParsedTimeRange } from './types';
|
||||
|
||||
/**
|
||||
* Custom time range separator used in the selectedTime string
|
||||
*/
|
||||
export const CUSTOM_TIME_SEPARATOR = '||_||';
|
||||
|
||||
/**
|
||||
* Check if selectedTime represents a custom time range
|
||||
*/
|
||||
export function isCustomTimeRange(selectedTime: string): boolean {
|
||||
return selectedTime.includes(CUSTOM_TIME_SEPARATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom time range string from min/max times (in nanoseconds)
|
||||
*/
|
||||
export function createCustomTimeRange(
|
||||
minTime: number,
|
||||
maxTime: number,
|
||||
): string {
|
||||
return `${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the custom time range string to get min/max times (in nanoseconds)
|
||||
*/
|
||||
export function parseCustomTimeRange(
|
||||
selectedTime: string,
|
||||
): ParsedTimeRange | null {
|
||||
if (!isCustomTimeRange(selectedTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [minStr, maxStr] = selectedTime.split(CUSTOM_TIME_SEPARATOR);
|
||||
const minTime = parseInt(minStr, 10);
|
||||
const maxTime = parseInt(maxStr, 10);
|
||||
|
||||
if (Number.isNaN(minTime) || Number.isNaN(maxTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { minTime, maxTime };
|
||||
}
|
||||
|
||||
const fallbackDuration = 30 * 1000 * 1000000; // 30s
|
||||
|
||||
/**
|
||||
* Parse the selectedTime string to get min/max time values.
|
||||
* For relative times, computes fresh values based on Date.now().
|
||||
* For custom times, extracts the stored min/max values.
|
||||
*/
|
||||
export function parseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
if (isCustomTimeRange(selectedTime)) {
|
||||
const parsed = parseCustomTimeRange(selectedTime);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
// Fallback to current time if parsing fails
|
||||
const now = Date.now() * 1000000;
|
||||
return { minTime: now - fallbackDuration, maxTime: now };
|
||||
}
|
||||
|
||||
// It's a relative time like '15m', '1h', etc.
|
||||
// Use getMinMaxForSelectedTime which computes from Date.now()
|
||||
return getMinMaxForSelectedTime(selectedTime as Time, 0, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user