fix: resolve ui full reload on auto-refresh (#8383)

This commit is contained in:
Amlan Kumar Nandy 2025-07-07 23:51:06 +07:00 committed by GitHub
parent 26d55875f5
commit 06ef9ff384
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 921 additions and 225 deletions

View File

@ -194,7 +194,7 @@ function HostMetricTraces({
{!isError && traces.length > 0 && ( {!isError && traces.length > 0 && (
<div className="host-metric-traces-table"> <div className="host-metric-traces-table">
<TraceExplorerControls <TraceExplorerControls
isLoading={isFetching} isLoading={isFetching && traces.length === 0}
totalCount={totalCount} totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS} perPageOptions={PER_PAGE_OPTIONS}
showSizeChanger={false} showSizeChanger={false}
@ -203,7 +203,7 @@ function HostMetricTraces({
tableLayout="fixed" tableLayout="fixed"
pagination={false} pagination={false}
scroll={{ x: true }} scroll={{ x: true }}
loading={isFetching} loading={isFetching && traces.length === 0}
dataSource={traces} dataSource={traces}
columns={traceListColumns} columns={traceListColumns}
onRow={(): Record<string, unknown> => ({ onRow={(): Record<string, unknown> => ({

View File

@ -37,7 +37,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -86,8 +86,12 @@ function HostMetricsDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [selectedView, setSelectedView] = useState<VIEWS>( const [selectedView, setSelectedView] = useState<VIEWS>(
@ -150,10 +154,11 @@ function HostMetricsDetails({
}, [initialFilters]); }, [initialFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -181,6 +186,7 @@ function HostMetricsDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -356,6 +362,7 @@ function HostMetricsDetails({
const handleClose = (): void => { const handleClose = (): void => {
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
lastSelectedInterval.current = null;
setSearchParams({}); setSearchParams({});
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -15,11 +15,12 @@ import {
} from 'container/TopNav/DateTimeSelectionV2/config'; } from 'container/TopNav/DateTimeSelectionV2/config';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions'; import { useResizeObserver } from 'hooks/useDimensions';
import { useMultiIntersectionObserver } from 'hooks/useMultiIntersectionObserver';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueries, UseQueryResult } from 'react-query'; import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
@ -53,6 +54,11 @@ function Metrics({
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED) featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false; ?.active || false;
const {
visibilities,
setElement,
} = useMultiIntersectionObserver(hostWidgetInfo.length, { threshold: 0.1 });
const queryPayloads = useMemo( const queryPayloads = useMemo(
() => () =>
getHostQueryPayload( getHostQueryPayload(
@ -65,11 +71,15 @@ function Metrics({
); );
const queries = useQueries( const queries = useQueries(
queryPayloads.map((payload) => ({ queryPayloads.map((payload, index) => ({
queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'], queryKey: ['host-metrics', payload, ENTITY_VERSION_V4, 'HOST'],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> => queryFn: ({
GetMetricQueryRange(payload, ENTITY_VERSION_V4), signal,
enabled: !!payload, }: QueryFunctionContext): Promise<
SuccessResponse<MetricRangePayloadProps>
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
enabled: !!payload && visibilities[index],
keepPreviousData: true,
})), })),
); );
@ -143,7 +153,7 @@ function Metrics({
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>, query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number, idx: number,
): JSX.Element => { ): JSX.Element => {
if (query.isLoading) { if ((!query.data && query.isLoading) || !visibilities[idx]) {
return <Skeleton />; return <Skeleton />;
} }
@ -181,7 +191,7 @@ function Metrics({
</div> </div>
<Row gutter={24} className="host-metrics-container"> <Row gutter={24} className="host-metrics-container">
{queries.map((query, idx) => ( {queries.map((query, idx) => (
<Col span={12} key={hostWidgetInfo[idx].title}> <Col ref={setElement(idx)} span={12} key={hostWidgetInfo[idx].title}>
<Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text> <Typography.Text>{hostWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="host-metrics-card" ref={graphRef}> <Card bordered className="host-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)} {renderCardContent(query, idx)}

View File

@ -96,11 +96,41 @@ function HostsList(): JSX.Element {
}; };
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]); }, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
const queryKey = useMemo(() => {
if (selectedHostName) {
return [
'hostList',
String(pageSize),
String(currentPage),
JSON.stringify(filters),
JSON.stringify(orderBy),
];
}
return [
'hostList',
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( const { data, isFetching, isLoading, isError } = useGetHostList(
query as HostListPayload, query as HostListPayload,
{ {
queryKey: ['hostList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
); );
@ -212,6 +242,7 @@ function HostsList(): JSX.Element {
<HostsListControls <HostsListControls
filters={filters} filters={filters}
handleFiltersChange={handleFiltersChange} handleFiltersChange={handleFiltersChange}
showAutoRefresh={!selectedHostData}
/> />
</div> </div>
<HostsListTable <HostsListTable

View File

@ -11,9 +11,11 @@ import { DataSource } from 'types/common/queryBuilder';
function HostsListControls({ function HostsListControls({
handleFiltersChange, handleFiltersChange,
filters, filters,
showAutoRefresh,
}: { }: {
handleFiltersChange: (value: IBuilderQuery['filters']) => void; handleFiltersChange: (value: IBuilderQuery['filters']) => void;
filters: IBuilderQuery['filters']; filters: IBuilderQuery['filters'];
showAutoRefresh: boolean;
}): JSX.Element { }): JSX.Element {
const currentQuery = initialQueriesMap[DataSource.METRICS]; const currentQuery = initialQueriesMap[DataSource.METRICS];
const updatedCurrentQuery = useMemo( const updatedCurrentQuery = useMemo(
@ -58,7 +60,7 @@ function HostsListControls({
<div className="time-selector"> <div className="time-selector">
<DateTimeSelectionV2 <DateTimeSelectionV2
showAutoRefresh showAutoRefresh={showAutoRefresh}
showRefreshText={false} showRefreshText={false}
hideShareModal hideShareModal
/> />

View File

@ -93,9 +93,13 @@ export default function HostsListTable({
const showHostsEmptyState = const showHostsEmptyState =
!isFetching && !isFetching &&
!isLoading && !isLoading &&
formattedHostMetricsData.length === 0 &&
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) && (!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
!filters.items.length; !filters.items.length;
const showTableLoadingState =
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
if (isError) { if (isError) {
return <Typography>{data?.error || 'Something went wrong'}</Typography>; return <Typography>{data?.error || 'Something went wrong'}</Typography>;
} }
@ -127,7 +131,7 @@ export default function HostsListTable({
); );
} }
if (isLoading || isFetching) { if (showTableLoadingState) {
return ( return (
<div className="hosts-list-loading-state"> <div className="hosts-list-loading-state">
<Skeleton.Input <Skeleton.Input
@ -155,7 +159,7 @@ export default function HostsListTable({
return ( return (
<Table <Table
className="hosts-list-table" className="hosts-list-table"
dataSource={isLoading || isFetching ? [] : formattedHostMetricsData} dataSource={showTableLoadingState ? [] : formattedHostMetricsData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -170,7 +174,7 @@ export default function HostsListTable({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
tableLayout="fixed" tableLayout="fixed"

View File

@ -28,6 +28,7 @@ describe('HostsListControls', () => {
<HostsListControls <HostsListControls
handleFiltersChange={mockHandleFiltersChange} handleFiltersChange={mockHandleFiltersChange}
filters={mockFilters} filters={mockFilters}
showAutoRefresh={false}
/>, />,
); );

View File

@ -59,13 +59,27 @@ describe('HostsListTable', () => {
setPageSize: mockSetPageSize, setPageSize: mockSetPageSize,
} as any; } as any;
it('renders loading state if isLoading is true', () => { it('renders loading state if isLoading is true and tableData is empty', () => {
const { container } = render(<HostsListTable {...mockProps} isLoading />); const { container } = render(
<HostsListTable
{...mockProps}
isLoading
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy(); expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
}); });
it('renders loading state if isFetching is true', () => { it('renders loading state if isFetching is true and tableData is empty', () => {
const { container } = render(<HostsListTable {...mockProps} isFetching />); const { container } = render(
<HostsListTable
{...mockProps}
isFetching
hostMetricsData={[]}
tableData={{ payload: { data: { hosts: [] } } }}
/>,
);
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy(); expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
}); });
@ -75,7 +89,17 @@ describe('HostsListTable', () => {
}); });
it('renders empty state if no hosts are found', () => { it('renders empty state if no hosts are found', () => {
const { container } = render(<HostsListTable {...mockProps} />); const { container } = render(
<HostsListTable
{...mockProps}
hostMetricsData={[]}
tableData={{
payload: {
data: { hosts: [] },
},
}}
/>,
);
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy(); expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
}); });
@ -83,6 +107,7 @@ describe('HostsListTable', () => {
const { container } = render( const { container } = render(
<HostsListTable <HostsListTable
{...mockProps} {...mockProps}
hostMetricsData={[]}
tableData={{ tableData={{
...mockTableData, ...mockTableData,
payload: { payload: {
@ -90,6 +115,7 @@ describe('HostsListTable', () => {
data: { data: {
...mockTableData.payload.data, ...mockTableData.payload.data,
sentAnyHostMetricsData: false, sentAnyHostMetricsData: false,
hosts: [],
}, },
}, },
}} }}
@ -102,6 +128,7 @@ describe('HostsListTable', () => {
const { container } = render( const { container } = render(
<HostsListTable <HostsListTable
{...mockProps} {...mockProps}
hostMetricsData={[]}
tableData={{ tableData={{
...mockTableData, ...mockTableData,
payload: { payload: {
@ -109,6 +136,7 @@ describe('HostsListTable', () => {
data: { data: {
...mockTableData.payload.data, ...mockTableData.payload.data,
isSendingIncorrectK8SAgentMetrics: true, isSendingIncorrectK8SAgentMetrics: true,
hosts: [],
}, },
}, },
}} }}

View File

@ -38,7 +38,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -85,8 +85,12 @@ function ClusterDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -195,10 +199,11 @@ function ClusterDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -226,6 +231,7 @@ function ClusterDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -462,6 +468,7 @@ function ClusterDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -189,6 +189,32 @@ function K8sClustersList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedClusterName) {
return [
'clusterList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'clusterList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedClusterName,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -198,7 +224,7 @@ function K8sClustersList({
} = useGetK8sClustersList( } = useGetK8sClustersList(
fetchGroupedByRowDataQuery as K8sClustersListPayload, fetchGroupedByRowDataQuery as K8sClustersListPayload,
{ {
queryKey: ['clusterList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -254,11 +280,44 @@ function K8sClustersList({
return groupedByRowData?.payload?.data?.records || []; return groupedByRowData?.payload?.data?.records || [];
}, [groupedByRowData, selectedRowData]); }, [groupedByRowData, selectedRowData]);
const queryKey = useMemo(() => {
if (selectedClusterName) {
return [
'clusterList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'clusterList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedClusterName,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sClustersList( const { data, isFetching, isLoading, isError } = useGetK8sClustersList(
query as K8sClustersListPayload, query as K8sClustersListPayload,
{ {
queryKey: ['clusterList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -583,6 +642,9 @@ function K8sClustersList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedClustersData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -595,12 +657,13 @@ function K8sClustersList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.NODES} entity={K8sCategory.NODES}
showAutoRefresh={!selectedClusterData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table <Table
className="k8s-list-table clusters-list-table" className="k8s-list-table clusters-list-table"
dataSource={isFetching || isLoading ? [] : formattedClustersData} dataSource={showTableLoadingState ? [] : formattedClustersData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -612,26 +675,25 @@ function K8sClustersList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -33,7 +33,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -84,8 +84,12 @@ function DaemonSetDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -211,10 +215,11 @@ function DaemonSetDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -242,6 +247,7 @@ function DaemonSetDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -476,6 +482,7 @@ function DaemonSetDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -191,6 +191,32 @@ function K8sDaemonSetsList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedDaemonSetUID) {
return [
'daemonSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'daemonSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedDaemonSetUID,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -200,7 +226,7 @@ function K8sDaemonSetsList({
} = useGetK8sDaemonSetsList( } = useGetK8sDaemonSetsList(
fetchGroupedByRowDataQuery as K8sDaemonSetsListPayload, fetchGroupedByRowDataQuery as K8sDaemonSetsListPayload,
{ {
queryKey: ['daemonSetList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -251,11 +277,44 @@ function K8sDaemonSetsList({
[groupedByRowData, groupBy], [groupedByRowData, groupBy],
); );
const queryKey = useMemo(() => {
if (selectedDaemonSetUID) {
return [
'daemonSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'daemonSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedDaemonSetUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sDaemonSetsList( const { data, isFetching, isLoading, isError } = useGetK8sDaemonSetsList(
query as K8sDaemonSetsListPayload, query as K8sDaemonSetsListPayload,
{ {
queryKey: ['daemonSetList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -591,6 +650,9 @@ function K8sDaemonSetsList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedDaemonSetsData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -603,6 +665,7 @@ function K8sDaemonSetsList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.DAEMONSETS} entity={K8sCategory.DAEMONSETS}
showAutoRefresh={!selectedDaemonSetData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
@ -610,7 +673,7 @@ function K8sDaemonSetsList({
className={classNames('k8s-list-table', 'daemonSets-list-table', { className={classNames('k8s-list-table', 'daemonSets-list-table', {
'expanded-daemonsets-list-table': isGroupedByAttribute, 'expanded-daemonsets-list-table': isGroupedByAttribute,
})} })}
dataSource={isFetching || isLoading ? [] : formattedDaemonSetsData} dataSource={showTableLoadingState ? [] : formattedDaemonSetsData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -622,26 +685,25 @@ function K8sDaemonSetsList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -38,7 +38,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -88,8 +88,12 @@ function DeploymentDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -215,10 +219,11 @@ function DeploymentDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -246,6 +251,7 @@ function DeploymentDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -487,6 +493,7 @@ function DeploymentDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -192,6 +192,32 @@ function K8sDeploymentsList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedDeploymentUID) {
return [
'deploymentList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'deploymentList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedDeploymentUID,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -201,7 +227,7 @@ function K8sDeploymentsList({
} = useGetK8sDeploymentsList( } = useGetK8sDeploymentsList(
fetchGroupedByRowDataQuery as K8sDeploymentsListPayload, fetchGroupedByRowDataQuery as K8sDeploymentsListPayload,
{ {
queryKey: ['deploymentList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -252,11 +278,44 @@ function K8sDeploymentsList({
[groupedByRowData, groupBy], [groupedByRowData, groupBy],
); );
const queryKey = useMemo(() => {
if (selectedDeploymentUID) {
return [
'deploymentList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'deploymentList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedDeploymentUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList( const { data, isFetching, isLoading, isError } = useGetK8sDeploymentsList(
query as K8sDeploymentsListPayload, query as K8sDeploymentsListPayload,
{ {
queryKey: ['deploymentList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -596,6 +655,9 @@ function K8sDeploymentsList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedDeploymentsData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -608,6 +670,7 @@ function K8sDeploymentsList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.NODES} entity={K8sCategory.NODES}
showAutoRefresh={!selectedDeploymentData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
@ -615,7 +678,7 @@ function K8sDeploymentsList({
className={classNames('k8s-list-table', 'deployments-list-table', { className={classNames('k8s-list-table', 'deployments-list-table', {
'expanded-deployments-list-table': isGroupedByAttribute, 'expanded-deployments-list-table': isGroupedByAttribute,
})} })}
dataSource={isFetching || isLoading ? [] : formattedDeploymentsData} dataSource={showTableLoadingState ? [] : formattedDeploymentsData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -627,26 +690,25 @@ function K8sDeploymentsList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -270,7 +270,7 @@ export default function Events({
</div> </div>
</div> </div>
{isLoading && <LoadingContainer />} {isLoading && formattedEntityEvents.length === 0 && <LoadingContainer />}
{!isLoading && !isError && formattedEntityEvents.length === 0 && ( {!isLoading && !isError && formattedEntityEvents.length === 0 && (
<EntityDetailsEmptyContainer category={category} view="events" /> <EntityDetailsEmptyContainer category={category} view="events" />

View File

@ -24,12 +24,13 @@ import {
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions'; import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData'; import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useQueries, UseQueryResult } from 'react-query'; import { QueryFunctionContext, useQueries, UseQueryResult } from 'react-query';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Options } from 'uplot'; import { Options } from 'uplot';
import { FeatureKeys } from '../../../../constants/features'; import { FeatureKeys } from '../../../../constants/features';
import { useMultiIntersectionObserver } from '../../../../hooks/useMultiIntersectionObserver';
import { useAppContext } from '../../../../providers/App/App'; import { useAppContext } from '../../../../providers/App/App';
interface EntityMetricsProps<T> { interface EntityMetricsProps<T> {
@ -73,6 +74,12 @@ function EntityMetrics<T>({
const dotMetricsEnabled = const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED) featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false; ?.active || false;
const {
visibilities,
setElement,
} = useMultiIntersectionObserver(entityWidgetInfo.length, { threshold: 0.1 });
const queryPayloads = useMemo( const queryPayloads = useMemo(
() => () =>
getEntityQueryPayload( getEntityQueryPayload(
@ -91,11 +98,15 @@ function EntityMetrics<T>({
); );
const queries = useQueries( const queries = useQueries(
queryPayloads.map((payload) => ({ queryPayloads.map((payload, index) => ({
queryKey: [queryKey, payload, ENTITY_VERSION_V4, category], queryKey: [queryKey, payload, ENTITY_VERSION_V4, category],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> => queryFn: ({
GetMetricQueryRange(payload, ENTITY_VERSION_V4), signal,
enabled: !!payload, }: QueryFunctionContext): Promise<
SuccessResponse<MetricRangePayloadProps>
> => GetMetricQueryRange(payload, ENTITY_VERSION_V4, signal),
enabled: !!payload && visibilities[index],
keepPreviousData: true,
})), })),
); );
@ -186,7 +197,7 @@ function EntityMetrics<T>({
query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>, query: UseQueryResult<SuccessResponse<MetricRangePayloadProps>, unknown>,
idx: number, idx: number,
): JSX.Element => { ): JSX.Element => {
if (query.isLoading) { if ((!query.data && query.isLoading) || !visibilities[idx]) {
return <Skeleton />; return <Skeleton />;
} }
@ -196,7 +207,7 @@ function EntityMetrics<T>({
return <div>{errorMessage}</div>; return <div>{errorMessage}</div>;
} }
const { panelType } = (query.data?.params as any).compositeQuery; const panelType = (query.data?.params as any)?.compositeQuery?.panelType;
return ( return (
<div <div
@ -234,7 +245,7 @@ function EntityMetrics<T>({
</div> </div>
<Row gutter={24} className="entity-metrics-container"> <Row gutter={24} className="entity-metrics-container">
{queries.map((query, idx) => ( {queries.map((query, idx) => (
<Col span={12} key={entityWidgetInfo[idx].title}> <Col ref={setElement(idx)} span={12} key={entityWidgetInfo[idx].title}>
<Typography.Text>{entityWidgetInfo[idx].title}</Typography.Text> <Typography.Text>{entityWidgetInfo[idx].title}</Typography.Text>
<Card bordered className="entity-metrics-card" ref={graphRef}> <Card bordered className="entity-metrics-card" ref={graphRef}>
{renderCardContent(query, idx)} {renderCardContent(query, idx)}

View File

@ -203,7 +203,7 @@ function EntityTraces({
{!isError && traces.length > 0 && ( {!isError && traces.length > 0 && (
<div className="entity-traces-table"> <div className="entity-traces-table">
<TraceExplorerControls <TraceExplorerControls
isLoading={isFetching} isLoading={isFetching && traces.length === 0}
totalCount={totalCount} totalCount={totalCount}
perPageOptions={PER_PAGE_OPTIONS} perPageOptions={PER_PAGE_OPTIONS}
showSizeChanger={false} showSizeChanger={false}
@ -212,7 +212,7 @@ function EntityTraces({
tableLayout="fixed" tableLayout="fixed"
pagination={false} pagination={false}
scroll={{ x: true }} scroll={{ x: true }}
loading={isFetching} loading={isFetching && traces.length === 0}
dataSource={traces} dataSource={traces}
columns={traceListColumns} columns={traceListColumns}
onRow={(): Record<string, unknown> => ({ onRow={(): Record<string, unknown> => ({

View File

@ -33,7 +33,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -81,8 +81,12 @@ function JobDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -204,10 +208,11 @@ function JobDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -235,6 +240,7 @@ function JobDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -469,6 +475,7 @@ function JobDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -186,6 +186,25 @@ function K8sJobsList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedJobUID) {
return [
'jobList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'jobList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [queryFilters, orderBy, selectedJobUID, minTime, maxTime, selectedRowData]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -195,7 +214,7 @@ function K8sJobsList({
} = useGetK8sJobsList( } = useGetK8sJobsList(
fetchGroupedByRowDataQuery as K8sJobsListPayload, fetchGroupedByRowDataQuery as K8sJobsListPayload,
{ {
queryKey: ['jobList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -251,11 +270,44 @@ function K8sJobsList({
return groupedByRowData?.payload?.data?.records || []; return groupedByRowData?.payload?.data?.records || [];
}, [groupedByRowData, selectedRowData]); }, [groupedByRowData, selectedRowData]);
const queryKey = useMemo(() => {
if (selectedJobUID) {
return [
'jobList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'jobList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedJobUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sJobsList( const { data, isFetching, isLoading, isError } = useGetK8sJobsList(
query as K8sJobsListPayload, query as K8sJobsListPayload,
{ {
queryKey: ['jobList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -581,6 +633,7 @@ function K8sJobsList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.JOBS} entity={K8sCategory.JOBS}
showAutoRefresh={!selectedJobData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}

View File

@ -30,6 +30,7 @@ interface K8sHeaderProps {
handleFilterVisibilityChange: () => void; handleFilterVisibilityChange: () => void;
isFiltersVisible: boolean; isFiltersVisible: boolean;
entity: K8sCategory; entity: K8sCategory;
showAutoRefresh: boolean;
} }
function K8sHeader({ function K8sHeader({
@ -46,6 +47,7 @@ function K8sHeader({
handleFilterVisibilityChange, handleFilterVisibilityChange,
isFiltersVisible, isFiltersVisible,
entity, entity,
showAutoRefresh,
}: K8sHeaderProps): JSX.Element { }: K8sHeaderProps): JSX.Element {
const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false); const [isFiltersSidePanelOpen, setIsFiltersSidePanelOpen] = useState(false);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -136,7 +138,7 @@ function K8sHeader({
<div className="k8s-list-controls-right"> <div className="k8s-list-controls-right">
<DateTimeSelectionV2 <DateTimeSelectionV2
showAutoRefresh showAutoRefresh={showAutoRefresh}
showRefreshText={false} showRefreshText={false}
hideShareModal hideShareModal
/> />

View File

@ -190,6 +190,32 @@ function K8sNamespacesList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedNamespaceUID) {
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'namespaceList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedNamespaceUID,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -199,7 +225,7 @@ function K8sNamespacesList({
} = useGetK8sNamespacesList( } = useGetK8sNamespacesList(
fetchGroupedByRowDataQuery as K8sNamespacesListPayload, fetchGroupedByRowDataQuery as K8sNamespacesListPayload,
{ {
queryKey: ['namespaceList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -250,11 +276,44 @@ function K8sNamespacesList({
[groupedByRowData, groupBy], [groupedByRowData, groupBy],
); );
const queryKey = useMemo(() => {
if (selectedNamespaceUID) {
return [
'namespaceList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'namespaceList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedNamespaceUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList( const { data, isFetching, isLoading, isError } = useGetK8sNamespacesList(
query as K8sNamespacesListPayload, query as K8sNamespacesListPayload,
{ {
queryKey: ['namespaceList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -592,6 +651,9 @@ function K8sNamespacesList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedNamespacesData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -604,12 +666,13 @@ function K8sNamespacesList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.NODES} entity={K8sCategory.NODES}
showAutoRefresh={!selectedNamespaceData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table <Table
className="k8s-list-table namespaces-list-table" className="k8s-list-table namespaces-list-table"
dataSource={isFetching || isLoading ? [] : formattedNamespacesData} dataSource={showTableLoadingState ? [] : formattedNamespacesData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -621,26 +684,25 @@ function K8sNamespacesList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -35,7 +35,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -85,8 +85,12 @@ function NamespaceDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -195,10 +199,11 @@ function NamespaceDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -226,6 +231,7 @@ function NamespaceDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -461,6 +467,7 @@ function NamespaceDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -184,6 +184,32 @@ function K8sNodesList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedNodeUID) {
return [
'nodeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'nodeList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedNodeUID,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -193,7 +219,7 @@ function K8sNodesList({
} = useGetK8sNodesList( } = useGetK8sNodesList(
fetchGroupedByRowDataQuery as K8sNodesListPayload, fetchGroupedByRowDataQuery as K8sNodesListPayload,
{ {
queryKey: ['nodeList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -249,11 +275,44 @@ function K8sNodesList({
[groupedByRowData, groupBy], [groupedByRowData, groupBy],
); );
const queryKey = useMemo(() => {
if (selectedNodeUID) {
return [
'nodeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'nodeList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedNodeUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sNodesList( const { data, isFetching, isLoading, isError } = useGetK8sNodesList(
query as K8sNodesListPayload, query as K8sNodesListPayload,
{ {
queryKey: ['nodeList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -571,6 +630,9 @@ function K8sNodesList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedNodesData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -583,12 +645,13 @@ function K8sNodesList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.NODES} entity={K8sCategory.NODES}
showAutoRefresh={!selectedNodeData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
<Table <Table
className="k8s-list-table nodes-list-table" className="k8s-list-table nodes-list-table"
dataSource={isFetching || isLoading ? [] : formattedNodesData} dataSource={showTableLoadingState ? [] : formattedNodesData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -600,26 +663,25 @@ function K8sNodesList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -38,7 +38,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -85,8 +85,12 @@ function NodeDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -195,10 +199,11 @@ function NodeDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -226,6 +231,7 @@ function NodeDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -464,6 +470,7 @@ function NodeDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -205,11 +205,44 @@ function K8sPodsList({
return queryPayload; return queryPayload;
}, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]); }, [pageSize, currentPage, queryFilters, minTime, maxTime, orderBy, groupBy]);
const queryKey = useMemo(() => {
if (selectedPodUID) {
return [
'podList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'podList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedPodUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sPodsList( const { data, isFetching, isLoading, isError } = useGetK8sPodsList(
query as K8sPodsListPayload, query as K8sPodsListPayload,
{ {
queryKey: ['hostList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -261,6 +294,25 @@ function K8sPodsList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData]); }, [minTime, maxTime, orderBy, selectedRowData]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedPodUID) {
return [
'podList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'podList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [queryFilters, orderBy, selectedPodUID, minTime, maxTime, selectedRowData]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -270,7 +322,7 @@ function K8sPodsList({
} = useGetK8sPodsList( } = useGetK8sPodsList(
fetchGroupedByRowDataQuery as K8sPodsListPayload, fetchGroupedByRowDataQuery as K8sPodsListPayload,
{ {
queryKey: ['hostList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -629,6 +681,9 @@ function K8sPodsList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedPodsData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -645,6 +700,7 @@ function K8sPodsList({
onAddColumn={handleAddColumn} onAddColumn={handleAddColumn}
onRemoveColumn={handleRemoveColumn} onRemoveColumn={handleRemoveColumn}
entity={K8sCategory.PODS} entity={K8sCategory.PODS}
showAutoRefresh={!selectedPodData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
@ -652,7 +708,7 @@ function K8sPodsList({
className={classNames('k8s-list-table', { className={classNames('k8s-list-table', {
'expanded-k8s-list-table': isGroupedByAttribute, 'expanded-k8s-list-table': isGroupedByAttribute,
})} })}
dataSource={isFetching || isLoading ? [] : formattedPodsData} dataSource={showTableLoadingState ? [] : formattedPodsData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -663,26 +719,25 @@ function K8sPodsList({
onChange: onPaginationChange, onChange: onPaginationChange,
}} }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
scroll={{ x: true }} scroll={{ x: true }}
tableLayout="fixed" tableLayout="fixed"

View File

@ -39,7 +39,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -89,8 +89,12 @@ function PodDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -212,10 +216,11 @@ function PodDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / TimeRangeOffset), startTime: Math.floor(minTime / TimeRangeOffset),
@ -243,6 +248,7 @@ function PodDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -485,6 +491,7 @@ function PodDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -191,6 +191,32 @@ function K8sStatefulSetsList({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [minTime, maxTime, orderBy, selectedRowData, groupBy]); }, [minTime, maxTime, orderBy, selectedRowData, groupBy]);
const groupedByRowDataQueryKey = useMemo(() => {
if (selectedStatefulSetUID) {
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
];
}
return [
'statefulSetList',
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(selectedRowData),
String(minTime),
String(maxTime),
];
}, [
queryFilters,
orderBy,
selectedStatefulSetUID,
minTime,
maxTime,
selectedRowData,
]);
const { const {
data: groupedByRowData, data: groupedByRowData,
isFetching: isFetchingGroupedByRowData, isFetching: isFetchingGroupedByRowData,
@ -200,7 +226,7 @@ function K8sStatefulSetsList({
} = useGetK8sStatefulSetsList( } = useGetK8sStatefulSetsList(
fetchGroupedByRowDataQuery as K8sStatefulSetsListPayload, fetchGroupedByRowDataQuery as K8sStatefulSetsListPayload,
{ {
queryKey: ['statefulSetList', fetchGroupedByRowDataQuery], queryKey: groupedByRowDataQueryKey,
enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData, enabled: !!fetchGroupedByRowDataQuery && !!selectedRowData,
}, },
undefined, undefined,
@ -256,11 +282,44 @@ function K8sStatefulSetsList({
return groupedByRowData?.payload?.data?.records || []; return groupedByRowData?.payload?.data?.records || [];
}, [groupedByRowData, selectedRowData]); }, [groupedByRowData, selectedRowData]);
const queryKey = useMemo(() => {
if (selectedStatefulSetUID) {
return [
'statefulSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
];
}
return [
'statefulSetList',
String(pageSize),
String(currentPage),
JSON.stringify(queryFilters),
JSON.stringify(orderBy),
JSON.stringify(groupBy),
String(minTime),
String(maxTime),
];
}, [
selectedStatefulSetUID,
pageSize,
currentPage,
queryFilters,
orderBy,
groupBy,
minTime,
maxTime,
]);
const { data, isFetching, isLoading, isError } = useGetK8sStatefulSetsList( const { data, isFetching, isLoading, isError } = useGetK8sStatefulSetsList(
query as K8sStatefulSetsListPayload, query as K8sStatefulSetsListPayload,
{ {
queryKey: ['statefulSetList', query], queryKey,
enabled: !!query, enabled: !!query,
keepPreviousData: true,
}, },
undefined, undefined,
dotMetricsEnabled, dotMetricsEnabled,
@ -592,6 +651,9 @@ function K8sStatefulSetsList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedStatefulSetsData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -604,6 +666,7 @@ function K8sStatefulSetsList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.STATEFULSETS} entity={K8sCategory.STATEFULSETS}
showAutoRefresh={!selectedStatefulSetData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
@ -611,7 +674,7 @@ function K8sStatefulSetsList({
className={classNames('k8s-list-table', 'statefulSets-list-table', { className={classNames('k8s-list-table', 'statefulSets-list-table', {
'expanded-statefulsets-list-table': isGroupedByAttribute, 'expanded-statefulsets-list-table': isGroupedByAttribute,
})} })}
dataSource={isFetching || isLoading ? [] : formattedStatefulSetsData} dataSource={showTableLoadingState ? [] : formattedStatefulSetsData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -623,26 +686,25 @@ function K8sStatefulSetsList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -38,7 +38,7 @@ import {
ScrollText, ScrollText,
X, X,
} from 'lucide-react'; } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat'; import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@ -84,8 +84,12 @@ function StatefulSetDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@ -211,10 +215,11 @@ function StatefulSetDetails({
}, [initialFilters, initialEventsFilters]); }, [initialFilters, initialEventsFilters]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -242,6 +247,7 @@ function StatefulSetDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -477,6 +483,7 @@ function StatefulSetDetails({
}; };
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -574,6 +574,9 @@ function K8sVolumesList({
}); });
}; };
const showTableLoadingState =
(isFetching || isLoading) && formattedVolumesData.length === 0;
return ( return (
<div className="k8s-list"> <div className="k8s-list">
<K8sHeader <K8sHeader
@ -586,6 +589,7 @@ function K8sVolumesList({
handleGroupByChange={handleGroupByChange} handleGroupByChange={handleGroupByChange}
selectedGroupBy={groupBy} selectedGroupBy={groupBy}
entity={K8sCategory.NODES} entity={K8sCategory.NODES}
showAutoRefresh={!selectedVolumeData}
/> />
{isError && <Typography>{data?.error || 'Something went wrong'}</Typography>} {isError && <Typography>{data?.error || 'Something went wrong'}</Typography>}
@ -593,7 +597,7 @@ function K8sVolumesList({
className={classNames('k8s-list-table', 'volumes-list-table', { className={classNames('k8s-list-table', 'volumes-list-table', {
'expanded-volumes-list-table': isGroupedByAttribute, 'expanded-volumes-list-table': isGroupedByAttribute,
})} })}
dataSource={isFetching || isLoading ? [] : formattedVolumesData} dataSource={showTableLoadingState ? [] : formattedVolumesData}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
@ -605,26 +609,25 @@ function K8sVolumesList({
}} }}
scroll={{ x: true }} scroll={{ x: true }}
loading={{ loading={{
spinning: isFetching || isLoading, spinning: showTableLoadingState,
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />, indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
}} }}
locale={{ locale={{
emptyText: emptyText: showTableLoadingState ? null : (
isFetching || isLoading ? null : ( <div className="no-filtered-hosts-message-container">
<div className="no-filtered-hosts-message-container"> <div className="no-filtered-hosts-message-content">
<div className="no-filtered-hosts-message-content"> <img
<img src="/Icons/emptyState.svg"
src="/Icons/emptyState.svg" alt="thinking-emoji"
alt="thinking-emoji" className="empty-state-svg"
className="empty-state-svg" />
/>
<Typography.Text className="no-filtered-hosts-message"> <Typography.Text className="no-filtered-hosts-message">
This query had no results. Edit your query and try again! This query had no results. Edit your query and try again!
</Typography.Text> </Typography.Text>
</div>
</div> </div>
), </div>
),
}} }}
tableLayout="fixed" tableLayout="fixed"
onChange={handleTableChange} onChange={handleTableChange}

View File

@ -13,7 +13,7 @@ import {
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import GetMinMax from 'lib/getMinMax'; import GetMinMax from 'lib/getMinMax';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
@ -44,8 +44,12 @@ function VolumeDetails({
endTime: endMs, endTime: endMs,
})); }));
const lastSelectedInterval = useRef<Time | null>(null);
const [selectedInterval, setSelectedInterval] = useState<Time>( const [selectedInterval, setSelectedInterval] = useState<Time>(
selectedTime as Time, lastSelectedInterval.current
? lastSelectedInterval.current
: (selectedTime as Time),
); );
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
@ -62,10 +66,11 @@ function VolumeDetails({
}, [volume]); }, [volume]);
useEffect(() => { useEffect(() => {
setSelectedInterval(selectedTime as Time); const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
setSelectedInterval(currentSelectedInterval as Time);
if (selectedTime !== 'custom') { if (currentSelectedInterval !== 'custom') {
const { maxTime, minTime } = GetMinMax(selectedTime); const { maxTime, minTime } = GetMinMax(currentSelectedInterval);
setModalTimeRange({ setModalTimeRange({
startTime: Math.floor(minTime / 1000000000), startTime: Math.floor(minTime / 1000000000),
@ -76,6 +81,7 @@ function VolumeDetails({
const handleTimeChange = useCallback( const handleTimeChange = useCallback(
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => { (interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
lastSelectedInterval.current = interval as Time;
setSelectedInterval(interval as Time); setSelectedInterval(interval as Time);
if (interval === 'custom' && dateTimeRange) { if (interval === 'custom' && dateTimeRange) {
@ -104,6 +110,7 @@ function VolumeDetails({
); );
const handleClose = (): void => { const handleClose = (): void => {
lastSelectedInterval.current = null;
setSelectedInterval(selectedTime as Time); setSelectedInterval(selectedTime as Time);
if (selectedTime !== 'custom') { if (selectedTime !== 'custom') {

View File

@ -0,0 +1,54 @@
import { useCallback, useEffect, useRef, useState } from 'react';
type SetElement = (el: Element | null) => void;
// To manage intersection observers for multiple items
export function useMultiIntersectionObserver(
itemCount: number,
options: IntersectionObserverInit = { threshold: 0.1 },
): {
visibilities: boolean[];
setElement: (index: number) => SetElement;
} {
const elementsRef = useRef<(Element | null)[]>([]);
const [everVisibles, setEverVisibles] = useState<boolean[]>(
new Array(itemCount).fill(false),
);
const setElement = useCallback<(index: number) => SetElement>(
(index) => (el): void => {
elementsRef.current[index] = el;
},
[],
);
useEffect(() => {
if (!elementsRef.current.length) return;
const observer = new IntersectionObserver((entries) => {
setEverVisibles((prev) => {
const newVis = [...prev];
let changed = false;
entries.forEach((entry) => {
const idx = elementsRef.current.indexOf(entry.target);
if (idx !== -1 && entry.isIntersecting && !newVis[idx]) {
newVis[idx] = true;
changed = true;
}
});
return changed ? newVis : prev;
});
}, options);
elementsRef.current.forEach((el) => {
if (el) observer.observe(el);
});
return (): void => {
observer.disconnect();
};
}, [itemCount, options]);
return { visibilities: everVisibles, setElement };
}