Logs: Feature flag logsInfiniteScrolling removed (#113585)
* Remove feature flag definition * Remove feature flag from code * Update imports * LogsNavigationPages: remove deprecated component * Finally deprecate navigation pages * Update tests * Update translations * Remove unused code * More cleanup * Test cleanup * Remove deprecated props * More props removal * Update feature flags * Revert changes
This commit is contained in:
@@ -41,7 +41,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes |
|
||||
| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes |
|
||||
| `dashboardScene` | Enables dashboard rendering using scenes for all roles | Yes |
|
||||
| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | Yes |
|
||||
| `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | |
|
||||
| `onPremToCloudMigrations` | Enable the Grafana Migration Assistant, which helps you easily migrate various on-prem resources to your Grafana Cloud stack. | Yes |
|
||||
| `cloudWatchNewLabelParsing` | Updates CloudWatch label parsing to be more accurate | Yes |
|
||||
|
||||
@@ -381,11 +381,6 @@ export interface FeatureToggles {
|
||||
*/
|
||||
timeComparison?: boolean;
|
||||
/**
|
||||
* Enables infinite scrolling for the Logs panel in Explore and Dashboards
|
||||
* @default true
|
||||
*/
|
||||
logsInfiniteScrolling?: boolean;
|
||||
/**
|
||||
* Enables shared crosshair in table panel
|
||||
*/
|
||||
tableSharedCrosshair?: boolean;
|
||||
|
||||
@@ -615,14 +615,6 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDatavizSquad,
|
||||
},
|
||||
{
|
||||
Name: "logsInfiniteScrolling",
|
||||
Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards",
|
||||
Stage: FeatureStageGeneralAvailability,
|
||||
Expression: "true",
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaObservabilityLogsSquad,
|
||||
},
|
||||
{
|
||||
Name: "tableSharedCrosshair",
|
||||
Description: "Enables shared crosshair in table panel",
|
||||
|
||||
Generated
-1
@@ -85,7 +85,6 @@ panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
|
||||
pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
|
||||
timeComparison,experimental,@grafana/dataviz-squad,false,false,true
|
||||
logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true
|
||||
tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true
|
||||
kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true
|
||||
cloudRBACRoles,preview,@grafana/identity-access-team,false,true,false
|
||||
|
||||
|
+2
-1
@@ -2545,7 +2545,8 @@
|
||||
"metadata": {
|
||||
"name": "logsInfiniteScrolling",
|
||||
"resourceVersion": "1753448760331",
|
||||
"creationTimestamp": "2023-11-09T10:54:03Z"
|
||||
"creationTimestamp": "2023-11-09T10:54:03Z",
|
||||
"deletionTimestamp": "2025-11-07T10:59:01Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards",
|
||||
|
||||
@@ -164,7 +164,6 @@ describe('buildShortUrl', () => {
|
||||
describe('getLogsPermalinkRange', () => {
|
||||
let row: LogRowModel, rows: LogRowModel[];
|
||||
beforeEach(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = true;
|
||||
row = createLogRow({
|
||||
timeEpochMs: 1111112222222,
|
||||
});
|
||||
@@ -175,22 +174,6 @@ describe('getLogsPermalinkRange', () => {
|
||||
row,
|
||||
];
|
||||
});
|
||||
afterAll(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = false;
|
||||
});
|
||||
|
||||
it('returns the original range if infinite scrolling is not enabled', () => {
|
||||
config.featureToggles.logsInfiniteScrolling = false;
|
||||
const range = {
|
||||
from: 1111111111111,
|
||||
to: 1111112222222,
|
||||
};
|
||||
const expectedRange = {
|
||||
from: new Date(1111111111111).toISOString(),
|
||||
to: new Date(1111112222222).toISOString(),
|
||||
};
|
||||
expect(getLogsPermalinkRange(row, [row], range)).toEqual(expectedRange);
|
||||
});
|
||||
|
||||
it('returns the range relative to the previous log line', () => {
|
||||
const range = {
|
||||
|
||||
@@ -170,14 +170,6 @@ function getPreviousLog(row: LogRowModel, allLogs: LogRowModel[]): LogRowModel |
|
||||
}
|
||||
|
||||
export function getLogsPermalinkRange(row: LogRowModel, rows: LogRowModel[], absoluteRange: AbsoluteTimeRange) {
|
||||
const range = {
|
||||
from: new Date(absoluteRange.from).toISOString(),
|
||||
to: new Date(absoluteRange.to).toISOString(),
|
||||
};
|
||||
if (!config.featureToggles.logsInfiniteScrolling) {
|
||||
return range;
|
||||
}
|
||||
|
||||
// With infinite scrolling, the time range of the log line can be after the absolute range or beyond the request line limit, so we need to adjust
|
||||
// Look for the previous sibling log, and use its timestamp
|
||||
const allLogs = rows.filter((logRow) => logRow.dataFrame.refId === row.dataFrame.refId);
|
||||
|
||||
@@ -475,7 +475,6 @@ export class Explore extends PureComponent<Props, ExploreState> {
|
||||
onStopScanning={this.onStopScanning}
|
||||
eventBus={this.logsEventBus}
|
||||
splitOpenFn={this.splitOpenFnLogs}
|
||||
scrollElement={this.scrollElement}
|
||||
isFilterLabelActive={this.isFilterLabelActive}
|
||||
onClickFilterString={this.onClickFilterString}
|
||||
onClickFilterOutString={this.onClickFilterOutString}
|
||||
|
||||
@@ -65,6 +65,8 @@ describe('Logs', () => {
|
||||
let originalHref = window.location.href;
|
||||
|
||||
beforeEach(() => {
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
window.HTMLElement.prototype.scroll = jest.fn();
|
||||
localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -128,9 +130,7 @@ describe('Logs', () => {
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
}}
|
||||
addResultsToCache={() => {}}
|
||||
onChangeTime={() => {}}
|
||||
clearCache={() => {}}
|
||||
getFieldLinks={() => {
|
||||
return [];
|
||||
}}
|
||||
@@ -160,39 +160,6 @@ describe('Logs', () => {
|
||||
return { ...rendered, store: fakeStore };
|
||||
};
|
||||
|
||||
describe('scrolling behavior', () => {
|
||||
let originalInnerHeight: number;
|
||||
beforeEach(() => {
|
||||
originalInnerHeight = window.innerHeight;
|
||||
window.innerHeight = 1000;
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
window.HTMLElement.prototype.scroll = jest.fn();
|
||||
});
|
||||
afterEach(() => {
|
||||
window.innerHeight = originalInnerHeight;
|
||||
});
|
||||
|
||||
it('should call `scrollElement.scroll`', () => {
|
||||
const logs = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
logs.push(makeLog({ uid: `uid${i}`, rowId: `id${i}`, timeEpochMs: i }));
|
||||
}
|
||||
const scrollElementMock = {
|
||||
scroll: jest.fn(),
|
||||
scrollTop: 920,
|
||||
};
|
||||
setup(
|
||||
{ scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState: { logs: { id: 'uid47' } } },
|
||||
undefined,
|
||||
logs
|
||||
);
|
||||
|
||||
// element.getBoundingClientRect().top will always be 0 for jsdom
|
||||
// calc will be `scrollElement.scrollTop - window.innerHeight / 2` -> 920 - 500 = 420
|
||||
expect(scrollElementMock.scroll).toBeCalledWith({ behavior: 'smooth', top: 420 });
|
||||
});
|
||||
});
|
||||
|
||||
it('should render logs', () => {
|
||||
setup();
|
||||
const logsSection = screen.getByTestId('logRows');
|
||||
@@ -246,9 +213,7 @@ describe('Logs', () => {
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
}}
|
||||
addResultsToCache={() => {}}
|
||||
onChangeTime={() => {}}
|
||||
clearCache={() => {}}
|
||||
getFieldLinks={() => {
|
||||
return [];
|
||||
}}
|
||||
@@ -296,9 +261,7 @@ describe('Logs', () => {
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
}}
|
||||
addResultsToCache={() => {}}
|
||||
onChangeTime={() => {}}
|
||||
clearCache={() => {}}
|
||||
getFieldLinks={() => {
|
||||
return [];
|
||||
}}
|
||||
@@ -349,9 +312,7 @@ describe('Logs', () => {
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: { from: 'now-1h', to: 'now' },
|
||||
}}
|
||||
addResultsToCache={() => {}}
|
||||
onChangeTime={() => {}}
|
||||
clearCache={() => {}}
|
||||
getFieldLinks={() => {
|
||||
return [];
|
||||
}}
|
||||
@@ -412,22 +373,6 @@ describe('Logs', () => {
|
||||
expect(fakeChangePanelState).toHaveBeenCalledWith('right', 'logs', { logs: {} });
|
||||
});
|
||||
|
||||
it('should scroll the scrollElement into view if rows contain id', () => {
|
||||
const panelState = { logs: { id: '3' } };
|
||||
const scrollElementMock = { scroll: jest.fn() };
|
||||
setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState });
|
||||
|
||||
expect(scrollElementMock.scroll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not scroll the scrollElement into view if rows does not contain id', () => {
|
||||
const panelState = { logs: { id: 'not-included' } };
|
||||
const scrollElementMock = { scroll: jest.fn() };
|
||||
setup({ loading: false, scrollElement: scrollElementMock as unknown as HTMLDivElement, panelState });
|
||||
|
||||
expect(scrollElementMock.scroll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call reportInteraction on permalinkClick', async () => {
|
||||
const panelState = { logs: { id: 'not-included' } };
|
||||
const rows = [
|
||||
@@ -479,8 +424,6 @@ describe('Logs', () => {
|
||||
});
|
||||
|
||||
it('should call createAndCopyShortLink on permalinkClick - with infinite scrolling', async () => {
|
||||
const featureToggleValue = config.featureToggles.logsInfiniteScrolling;
|
||||
config.featureToggles.logsInfiniteScrolling = true;
|
||||
const rows = [
|
||||
makeLog({ uid: '1', rowId: 'id1', timeEpochMs: 1 }),
|
||||
makeLog({ uid: '2', rowId: 'id2', timeEpochMs: 1 }),
|
||||
@@ -503,7 +446,6 @@ describe('Logs', () => {
|
||||
)
|
||||
);
|
||||
expect(createAndCopyShortLink).toHaveBeenCalledWith(expect.stringMatching('visualisationType%22:%22logs'));
|
||||
config.featureToggles.logsInfiniteScrolling = featureToggleValue;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -119,11 +119,8 @@ interface Props extends Themeable2 {
|
||||
) => Promise<DataQuery | null>;
|
||||
getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode;
|
||||
getFieldLinks: GetFieldLinksFn;
|
||||
addResultsToCache: () => void;
|
||||
clearCache: () => void;
|
||||
eventBus: EventBus;
|
||||
panelState?: ExplorePanelsState;
|
||||
scrollElement?: HTMLDivElement;
|
||||
isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
logsFrames?: DataFrame[];
|
||||
range: TimeRange;
|
||||
@@ -183,8 +180,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
getFieldLinks,
|
||||
theme,
|
||||
logsQueries,
|
||||
clearCache,
|
||||
addResultsToCache,
|
||||
exploreId,
|
||||
getRowContext,
|
||||
getLogRowContextUi,
|
||||
@@ -193,7 +188,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
panelState,
|
||||
eventBus,
|
||||
onPinLineCallback,
|
||||
scrollElement,
|
||||
} = props;
|
||||
const [showLabels, setShowLabels] = useState<boolean>(store.getBool(SETTINGS_KEYS.showLabels, false));
|
||||
const [showTime, setShowTime] = useState<boolean>(store.getBool(SETTINGS_KEYS.showTime, true));
|
||||
@@ -365,29 +359,17 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
[props.eventBus]
|
||||
);
|
||||
|
||||
const scrollIntoView = useCallback(
|
||||
(element: HTMLElement) => {
|
||||
if (config.featureToggles.logsInfiniteScrolling) {
|
||||
if (logsContainerRef.current) {
|
||||
topLogsRef.current?.scrollIntoView();
|
||||
logsContainerRef.current.scroll({
|
||||
behavior: 'smooth',
|
||||
top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2,
|
||||
});
|
||||
}
|
||||
const scrollIntoView = useCallback((element: HTMLElement) => {
|
||||
if (logsContainerRef.current) {
|
||||
topLogsRef.current?.scrollIntoView?.();
|
||||
logsContainerRef.current.scroll({
|
||||
behavior: 'smooth',
|
||||
top: logsContainerRef.current.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollElement) {
|
||||
scrollElement.scroll({
|
||||
behavior: 'smooth',
|
||||
top: scrollElement.scrollTop + element.getBoundingClientRect().top - window.innerHeight / 2,
|
||||
});
|
||||
}
|
||||
},
|
||||
[scrollElement]
|
||||
);
|
||||
return;
|
||||
}, []);
|
||||
|
||||
const sortOrderChanged = useCallback(
|
||||
(newSortOrder: LogsSortOrder) => {
|
||||
@@ -626,13 +608,11 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
);
|
||||
|
||||
const scrollToTopLogs = useCallback(() => {
|
||||
if (config.featureToggles.logsInfiniteScrolling) {
|
||||
if (logsContainerRef.current) {
|
||||
logsContainerRef.current.scroll({
|
||||
behavior: 'auto',
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
if (logsContainerRef.current) {
|
||||
logsContainerRef.current.scroll({
|
||||
behavior: 'auto',
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
topLogsRef.current?.scrollIntoView();
|
||||
}, []);
|
||||
@@ -684,7 +664,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
);
|
||||
|
||||
const { dedupedRows, dedupCount } = useMemo(() => dedupRows(logRows, dedupStrategy), [dedupStrategy, logRows]);
|
||||
const navigationRange = useMemo(() => createNavigationRange(logRows), [logRows]);
|
||||
const infiniteScrollAvailable = useMemo(
|
||||
() => !logsQueries?.some((query) => 'direction' in query && query.direction === LokiQueryDirection.Scan),
|
||||
[logsQueries]
|
||||
@@ -1060,11 +1039,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
visualisationType === 'logs' &&
|
||||
hasData && (
|
||||
<>
|
||||
<div
|
||||
className={config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows}
|
||||
data-testid="logRows"
|
||||
ref={logsContainerRef}
|
||||
>
|
||||
<div className={styles.scrollableLogRows} data-testid="logRows" ref={logsContainerRef}>
|
||||
<InfiniteScroll
|
||||
loading={loading}
|
||||
loadMoreLogs={infiniteScrollAvailable ? loadMoreLogs : undefined}
|
||||
@@ -1113,18 +1088,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
<LogsNavigation
|
||||
logsSortOrder={logsSortOrder}
|
||||
visibleRange={navigationRange ?? absoluteRange}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
onChangeTime={onChangeTime}
|
||||
loading={loading}
|
||||
queries={logsQueries ?? []}
|
||||
scrollToTopLogs={scrollToTopLogs}
|
||||
addResultsToCache={addResultsToCache}
|
||||
clearCache={clearCache}
|
||||
/>
|
||||
<LogsNavigation logsSortOrder={logsSortOrder} scrollToTopLogs={scrollToTopLogs} />
|
||||
</>
|
||||
)}
|
||||
{config.featureToggles.newLogsPanel && visualisationType === 'logs' && (
|
||||
@@ -1277,7 +1241,7 @@ const getStyles = (theme: GrafanaTheme2, wrapLogMessage: boolean, tableHeight: n
|
||||
}),
|
||||
stickyNavigation: css({
|
||||
overflow: 'visible',
|
||||
...(config.featureToggles.logsInfiniteScrolling && { marginBottom: '0px' }),
|
||||
marginBottom: '0px',
|
||||
}),
|
||||
logsVolumePanel: css({
|
||||
marginBottom: theme.spacing(1.5),
|
||||
@@ -1290,17 +1254,3 @@ const dedupRows = (logRows: LogRowModel[], dedupStrategy: LogsDedupStrategy) =>
|
||||
const dedupCount = dedupedRows.reduce((sum, row) => (row.duplicates ? sum + row.duplicates : sum), 0);
|
||||
return { dedupedRows, dedupCount };
|
||||
};
|
||||
|
||||
const createNavigationRange = (logRows: LogRowModel[]): { from: number; to: number } | undefined => {
|
||||
if (!logRows || logRows.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const firstTimeStamp = logRows[0].timeEpochMs;
|
||||
const lastTimeStamp = logRows[logRows.length - 1].timeEpochMs;
|
||||
|
||||
if (lastTimeStamp < firstTimeStamp) {
|
||||
return { from: lastTimeStamp, to: firstTimeStamp };
|
||||
}
|
||||
|
||||
return { from: firstTimeStamp, to: lastTimeStamp };
|
||||
};
|
||||
|
||||
@@ -31,13 +31,7 @@ import { ExploreItemState } from 'app/types/explore';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { getTimeZone } from '../../profile/state/selectors';
|
||||
import {
|
||||
addResultsToCache,
|
||||
clearCache,
|
||||
loadSupplementaryQueryData,
|
||||
selectIsWaitingForData,
|
||||
setSupplementaryQueryEnabled,
|
||||
} from '../state/query';
|
||||
import { loadSupplementaryQueryData, selectIsWaitingForData, setSupplementaryQueryEnabled } from '../state/query';
|
||||
import { updateTimeRange, loadMoreLogs } from '../state/time';
|
||||
import { LiveTailControls } from '../useLiveTailControls';
|
||||
import { getFieldLinksForExplore } from '../utils/links';
|
||||
@@ -58,7 +52,6 @@ interface LogsContainerProps extends PropsFromRedux {
|
||||
onStopScanning: () => void;
|
||||
eventBus: EventBus;
|
||||
splitOpenFn: SplitOpen;
|
||||
scrollElement?: HTMLDivElement;
|
||||
isFilterLabelActive: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||
onClickFilterString: (value: string, refId?: string) => void;
|
||||
onClickFilterOutString: (value: string, refId?: string) => void;
|
||||
@@ -260,14 +253,6 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
);
|
||||
};
|
||||
|
||||
addResultsToCache = () => {
|
||||
this.props.addResultsToCache(this.props.exploreId);
|
||||
};
|
||||
|
||||
clearCache = () => {
|
||||
this.props.clearCache(this.props.exploreId);
|
||||
};
|
||||
|
||||
loadLogsVolumeData = () => {
|
||||
this.props.loadSupplementaryQueryData(this.props.exploreId, SupplementaryQueryType.LogsVolume);
|
||||
};
|
||||
@@ -298,7 +283,6 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
isLive,
|
||||
exploreId,
|
||||
logsVolume,
|
||||
scrollElement,
|
||||
onPinLineCallback,
|
||||
} = this.props;
|
||||
|
||||
@@ -358,12 +342,9 @@ class LogsContainer extends PureComponent<LogsContainerProps, LogsContainerState
|
||||
getRowContextQuery={this.getLogRowContextQuery}
|
||||
getLogRowContextUi={this.getLogRowContextUi}
|
||||
getFieldLinks={this.getFieldLinks}
|
||||
addResultsToCache={this.addResultsToCache}
|
||||
clearCache={this.clearCache}
|
||||
eventBus={this.props.eventBus}
|
||||
panelState={this.props.panelState}
|
||||
logsFrames={this.props.logsFrames}
|
||||
scrollElement={scrollElement}
|
||||
isFilterLabelActive={this.logDetailsFilterAvailable() ? this.props.isFilterLabelActive : undefined}
|
||||
range={range}
|
||||
onPinLineCallback={onPinLineCallback}
|
||||
@@ -419,8 +400,6 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string }
|
||||
const mapDispatchToProps = {
|
||||
updateTimeRange,
|
||||
loadMoreLogs,
|
||||
addResultsToCache,
|
||||
clearCache,
|
||||
loadSupplementaryQueryData,
|
||||
setSupplementaryQueryEnabled,
|
||||
};
|
||||
|
||||
@@ -2,9 +2,6 @@ import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { LogsSortOrder } from '@grafana/data';
|
||||
import { DataQuery } from '@grafana/schema';
|
||||
|
||||
import LogsNavigation from './LogsNavigation';
|
||||
|
||||
// we have to mock out reportInteraction, otherwise it crashes the test.
|
||||
@@ -15,16 +12,8 @@ jest.mock('@grafana/runtime', () => ({
|
||||
|
||||
type LogsNavigationProps = ComponentProps<typeof LogsNavigation>;
|
||||
const defaultProps: LogsNavigationProps = {
|
||||
absoluteRange: { from: 1637319381811, to: 1637322981811 },
|
||||
timeZone: 'local',
|
||||
queries: [],
|
||||
loading: false,
|
||||
logsSortOrder: undefined,
|
||||
visibleRange: { from: 1637322959000, to: 1637322981811 },
|
||||
onChangeTime: jest.fn(),
|
||||
scrollToTopLogs: jest.fn(),
|
||||
addResultsToCache: jest.fn(),
|
||||
clearCache: jest.fn(),
|
||||
};
|
||||
|
||||
const setup = (propOverrides?: Partial<LogsNavigationProps>) => {
|
||||
@@ -37,132 +26,13 @@ const setup = (propOverrides?: Partial<LogsNavigationProps>) => {
|
||||
};
|
||||
|
||||
describe('LogsNavigation', () => {
|
||||
it('should always render 3 navigation buttons', () => {
|
||||
it('should render scroll to top with default logs order', async () => {
|
||||
setup();
|
||||
expect(screen.getByTestId('newerLogsButton')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('olderLogsButton')).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId('scrollToTop')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render 3 navigation buttons in correct order when default logs order', () => {
|
||||
const { container } = setup();
|
||||
const expectedOrder = ['newerLogsButton', 'olderLogsButton', 'scrollToTop'];
|
||||
const elements = container.querySelectorAll(
|
||||
'[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]'
|
||||
);
|
||||
expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder);
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('scrollToTop'));
|
||||
|
||||
it('should render 3 navigation buttons in correct order when flipped logs order', () => {
|
||||
const { container } = setup({ logsSortOrder: LogsSortOrder.Ascending });
|
||||
const expectedOrder = ['olderLogsButton', 'newerLogsButton', 'scrollToTop'];
|
||||
const elements = container.querySelectorAll(
|
||||
'[data-testid=newerLogsButton],[data-testid=olderLogsButton],[data-testid=scrollToTop]'
|
||||
);
|
||||
expect(Array.from(elements).map((el) => el.getAttribute('data-testid'))).toMatchObject(expectedOrder);
|
||||
});
|
||||
|
||||
it('should disable fetch buttons when logs are loading', () => {
|
||||
setup({ loading: true });
|
||||
const olderLogsButton = screen.getByTestId('olderLogsButton');
|
||||
const newerLogsButton = screen.getByTestId('newerLogsButton');
|
||||
expect(olderLogsButton).toBeDisabled();
|
||||
expect(newerLogsButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should render logs navigation pages section', () => {
|
||||
setup();
|
||||
expect(screen.getByTestId('logsNavigationPages')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should correctly request older logs when flipped order', async () => {
|
||||
const onChangeTimeMock = jest.fn();
|
||||
const { rerender } = setup({ onChangeTime: onChangeTimeMock });
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319359000, to: 1637322959000 });
|
||||
|
||||
rerender(
|
||||
<LogsNavigation
|
||||
{...defaultProps}
|
||||
absoluteRange={{ from: 1637319359000, to: 1637322959000 }}
|
||||
visibleRange={{ from: 1637322938000, to: 1637322959000 }}
|
||||
onChangeTime={onChangeTimeMock}
|
||||
logsSortOrder={LogsSortOrder.Ascending}
|
||||
/>
|
||||
);
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
expect(onChangeTimeMock).toHaveBeenCalledWith({ from: 1637319338000, to: 1637322938000 });
|
||||
});
|
||||
|
||||
it('should correctly display the active page', async () => {
|
||||
const queries: DataQuery[] = [];
|
||||
const { rerender } = setup({
|
||||
absoluteRange: { from: 1704737384139, to: 1704737684139 },
|
||||
visibleRange: { from: 1704737384207, to: 1704737683316 },
|
||||
queries,
|
||||
logsSortOrder: LogsSortOrder.Descending,
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('page1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('page1').firstChild).toHaveClass('selectedBg');
|
||||
|
||||
expect(screen.queryByTestId('page2')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
|
||||
rerender(
|
||||
<LogsNavigation
|
||||
{...defaultProps}
|
||||
absoluteRange={{ from: 1704737084207, to: 1704737384207 }}
|
||||
visibleRange={{ from: 1704737084627, to: 1704737383765 }}
|
||||
onChangeTime={jest.fn()}
|
||||
logsSortOrder={LogsSortOrder.Descending}
|
||||
queries={queries}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('page1')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('page1').firstChild).not.toHaveClass('selectedBg');
|
||||
|
||||
expect(await screen.findByTestId('page2')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('page2').firstChild).toHaveClass('selectedBg');
|
||||
|
||||
expect(screen.queryByTestId('page3')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should reset the scroll when pagination is clicked', async () => {
|
||||
const scrollToTopLogsMock = jest.fn();
|
||||
setup({ scrollToTopLogs: scrollToTopLogsMock });
|
||||
|
||||
expect(scrollToTopLogsMock).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
expect(scrollToTopLogsMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not trigger actions while loading', async () => {
|
||||
const scrollToTopLogs = jest.fn();
|
||||
const changeTimeMock = jest.fn();
|
||||
setup({ scrollToTopLogs, onChangeTime: changeTimeMock, loading: true });
|
||||
|
||||
expect(scrollToTopLogs).not.toHaveBeenCalled();
|
||||
expect(changeTimeMock).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
await userEvent.click(screen.getByTestId('newerLogsButton'));
|
||||
expect(scrollToTopLogs).not.toHaveBeenCalled();
|
||||
expect(changeTimeMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not add results to cache unless pagination is used', async () => {
|
||||
const addResultsToCache = jest.fn();
|
||||
setup({ addResultsToCache });
|
||||
|
||||
expect(addResultsToCache).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('olderLogsButton')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('newerLogsButton')).toBeDisabled();
|
||||
|
||||
await userEvent.click(screen.getByTestId('olderLogsButton'));
|
||||
await userEvent.click(screen.getByTestId('newerLogsButton'));
|
||||
|
||||
expect(addResultsToCache).toHaveBeenCalledTimes(1);
|
||||
expect(defaultProps.scrollToTopLogs).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,220 +1,30 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { isEqual } from 'lodash';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { AbsoluteTimeRange, GrafanaTheme2, LogsSortOrder } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { GrafanaTheme2, LogsSortOrder } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { DataQuery, TimeZone } from '@grafana/schema';
|
||||
import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui';
|
||||
import { Button, Icon, useTheme2 } from '@grafana/ui';
|
||||
import { getChromeHeaderLevelHeight } from 'app/core/components/AppChrome/TopBar/useChromeHeaderHeight';
|
||||
|
||||
import { LogsNavigationPages } from './LogsNavigationPages';
|
||||
|
||||
type Props = {
|
||||
absoluteRange: AbsoluteTimeRange;
|
||||
timeZone: TimeZone;
|
||||
queries: DataQuery[];
|
||||
loading: boolean;
|
||||
visibleRange: AbsoluteTimeRange;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
scrollToTopLogs: () => void;
|
||||
scrollToBottomLogs?: () => void;
|
||||
addResultsToCache: () => void;
|
||||
clearCache: () => void;
|
||||
};
|
||||
|
||||
export type LogsPage = {
|
||||
logsRange: AbsoluteTimeRange;
|
||||
queryRange: AbsoluteTimeRange;
|
||||
};
|
||||
|
||||
function LogsNavigation({
|
||||
absoluteRange,
|
||||
logsSortOrder,
|
||||
timeZone,
|
||||
loading,
|
||||
onChangeTime,
|
||||
scrollToTopLogs,
|
||||
scrollToBottomLogs,
|
||||
visibleRange,
|
||||
queries,
|
||||
clearCache,
|
||||
addResultsToCache,
|
||||
}: Props) {
|
||||
const [pages, setPages] = useState<LogsPage[]>([]);
|
||||
|
||||
// These refs are to determine, if we want to clear up logs navigation when totally new query is run
|
||||
const expectedQueriesRef = useRef<DataQuery[]>();
|
||||
const expectedRangeRef = useRef<AbsoluteTimeRange>();
|
||||
// This ref is to store range span for future queres based on firstly selected time range
|
||||
// e.g. if last 5 min selected, always run 5 min range
|
||||
const rangeSpanRef = useRef(0);
|
||||
|
||||
const currentPageIndex = useMemo(
|
||||
() =>
|
||||
pages.findIndex((page) => {
|
||||
return page.queryRange.to === absoluteRange.to;
|
||||
}),
|
||||
[absoluteRange.to, pages]
|
||||
);
|
||||
|
||||
function LogsNavigation({ logsSortOrder, scrollToTopLogs }: Props) {
|
||||
const oldestLogsFirst = logsSortOrder === LogsSortOrder.Ascending;
|
||||
const onFirstPage = oldestLogsFirst ? currentPageIndex === pages.length - 1 : currentPageIndex === 0;
|
||||
const onLastPage = oldestLogsFirst ? currentPageIndex === 0 : currentPageIndex === pages.length - 1;
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, oldestLogsFirst);
|
||||
|
||||
// Main effect to set pages and index
|
||||
useEffect(() => {
|
||||
const newPage = { logsRange: visibleRange, queryRange: absoluteRange };
|
||||
let newPages: LogsPage[] = [];
|
||||
// We want to start new pagination if queries change or if absolute range is different than expected
|
||||
if (!isEqual(expectedRangeRef.current, absoluteRange) || !isEqual(expectedQueriesRef.current, queries)) {
|
||||
clearCache();
|
||||
setPages([newPage]);
|
||||
expectedQueriesRef.current = queries;
|
||||
rangeSpanRef.current = absoluteRange.to - absoluteRange.from;
|
||||
} else {
|
||||
setPages((pages) => {
|
||||
// Remove duplicates with new query
|
||||
newPages = pages.filter((page) => !isEqual(newPage.queryRange, page.queryRange));
|
||||
// Sort pages based on logsOrder so they visually align with displayed logs
|
||||
newPages = [...newPages, newPage].sort((a, b) => sortPages(a, b, logsSortOrder));
|
||||
return newPages;
|
||||
});
|
||||
}
|
||||
}, [visibleRange, absoluteRange, logsSortOrder, queries, clearCache, addResultsToCache]);
|
||||
|
||||
const changeTime = useCallback(
|
||||
({ from, to }: AbsoluteTimeRange) => {
|
||||
addResultsToCache();
|
||||
expectedRangeRef.current = { from, to };
|
||||
onChangeTime({ from, to });
|
||||
},
|
||||
[onChangeTime, addResultsToCache]
|
||||
);
|
||||
|
||||
const sortPages = (a: LogsPage, b: LogsPage, logsSortOrder?: LogsSortOrder | null) => {
|
||||
if (logsSortOrder === LogsSortOrder.Ascending) {
|
||||
return a.queryRange.to > b.queryRange.to ? 1 : -1;
|
||||
}
|
||||
return a.queryRange.to > b.queryRange.to ? -1 : 1;
|
||||
};
|
||||
|
||||
const olderLogsButton = (
|
||||
<Button
|
||||
data-testid="olderLogsButton"
|
||||
className={styles.navButton}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
//If we are not on the last page, use next page's range
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'olderLogsButton',
|
||||
});
|
||||
if (!onLastPage) {
|
||||
const indexChange = oldestLogsFirst ? -1 : 1;
|
||||
changeTime({
|
||||
from: pages[currentPageIndex + indexChange].queryRange.from,
|
||||
to: pages[currentPageIndex + indexChange].queryRange.to,
|
||||
});
|
||||
} else {
|
||||
//If we are on the last page, create new range
|
||||
changeTime({ from: visibleRange.from - rangeSpanRef.current, to: visibleRange.from });
|
||||
}
|
||||
scrollToTopLogs();
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className={styles.navButtonContent}>
|
||||
{loading ? <Spinner /> : <Icon name={oldestLogsFirst ? 'angle-up' : 'angle-down'} size="lg" />}
|
||||
<Trans i18nKey={'logs.logs-navigation.older-logs'}>Older logs</Trans>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const newerLogsButton = (
|
||||
<Button
|
||||
data-testid="newerLogsButton"
|
||||
className={styles.navButton}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'newerLogsButton',
|
||||
});
|
||||
//If we are not on the first page, use previous page's range
|
||||
if (!onFirstPage) {
|
||||
const indexChange = oldestLogsFirst ? 1 : -1;
|
||||
changeTime({
|
||||
from: pages[currentPageIndex + indexChange].queryRange.from,
|
||||
to: pages[currentPageIndex + indexChange].queryRange.to,
|
||||
});
|
||||
}
|
||||
scrollToTopLogs();
|
||||
//If we are on the first page, button is disabled and we do nothing
|
||||
}}
|
||||
disabled={loading || onFirstPage}
|
||||
>
|
||||
<div className={styles.navButtonContent}>
|
||||
{loading && <Spinner />}
|
||||
{onFirstPage || loading ? null : <Icon name={oldestLogsFirst ? 'angle-down' : 'angle-up'} size="lg" />}
|
||||
{onFirstPage
|
||||
? t('logs.logs-navigation.start-of-range', 'Start of range')
|
||||
: t('logs.logs-navigation.newer-logs', 'Newer logs')}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const onPageClick = useCallback(
|
||||
(page: LogsPage, pageNumber: number) => {
|
||||
reportInteraction('grafana_explore_logs_pagination_clicked', {
|
||||
pageType: 'page',
|
||||
pageNumber,
|
||||
});
|
||||
changeTime({ from: page.queryRange.from, to: page.queryRange.to });
|
||||
scrollToTopLogs();
|
||||
},
|
||||
[changeTime, scrollToTopLogs]
|
||||
);
|
||||
|
||||
const onScrollToTopClick = useCallback(() => {
|
||||
reportInteraction('grafana_explore_logs_scroll_top_clicked');
|
||||
scrollToTopLogs();
|
||||
}, [scrollToTopLogs]);
|
||||
|
||||
const onScrollToBottomClick = useCallback(() => {
|
||||
reportInteraction('grafana_explore_logs_scroll_bottom_clicked');
|
||||
scrollToBottomLogs?.();
|
||||
}, [scrollToBottomLogs]);
|
||||
|
||||
return (
|
||||
<div className={styles.navContainer}>
|
||||
{!config.featureToggles.logsInfiniteScrolling && (
|
||||
<>
|
||||
{oldestLogsFirst ? olderLogsButton : newerLogsButton}
|
||||
<LogsNavigationPages
|
||||
pages={pages}
|
||||
currentPageIndex={currentPageIndex}
|
||||
oldestLogsFirst={oldestLogsFirst}
|
||||
timeZone={timeZone}
|
||||
loading={loading}
|
||||
onClick={onPageClick}
|
||||
/>
|
||||
{oldestLogsFirst ? newerLogsButton : olderLogsButton}
|
||||
</>
|
||||
)}
|
||||
{scrollToBottomLogs && (
|
||||
<Button
|
||||
data-testid="scrollToBottom"
|
||||
className={styles.scrollToBottomButton}
|
||||
variant="secondary"
|
||||
onClick={onScrollToBottomClick}
|
||||
title={t('logs.logs-navigation.scroll-bottom', 'Scroll to bottom')}
|
||||
>
|
||||
<Icon name="arrow-down" size="lg" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
data-testid="scrollToTop"
|
||||
className={styles.scrollToTopButton}
|
||||
@@ -239,33 +49,11 @@ const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
|
||||
width: oldestLogsFirst && !config.featureToggles.newLogsPanel ? '58px' : 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: config.featureToggles.logsInfiniteScrolling
|
||||
? 'flex-end'
|
||||
: oldestLogsFirst
|
||||
? 'flex-start'
|
||||
: 'space-between',
|
||||
justifyContent: 'flex-end',
|
||||
position: 'sticky',
|
||||
top: theme.spacing(2),
|
||||
right: 0,
|
||||
}),
|
||||
navButton: css({
|
||||
width: '58px',
|
||||
height: '68px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
lineHeight: 1,
|
||||
}),
|
||||
navButtonContent: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
whiteSpace: 'normal',
|
||||
}),
|
||||
scrollToBottomButton: css({
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { LogsNavigationPages } from './LogsNavigationPages';
|
||||
|
||||
type LogsNavigationPagesProps = ComponentProps<typeof LogsNavigationPages>;
|
||||
|
||||
const setup = (propOverrides?: Partial<LogsNavigationPagesProps>) => {
|
||||
const props: LogsNavigationPagesProps = {
|
||||
pages: [
|
||||
{
|
||||
logsRange: { from: 1619081941000, to: 1619081945930 },
|
||||
queryRange: { from: 1619081645930, to: 1619081945930 },
|
||||
},
|
||||
{
|
||||
logsRange: { from: 1619081951000, to: 1619081955930 },
|
||||
queryRange: { from: 1619081655930, to: 1619081955930 },
|
||||
},
|
||||
],
|
||||
currentPageIndex: 0,
|
||||
oldestLogsFirst: false,
|
||||
timeZone: 'local',
|
||||
loading: false,
|
||||
onClick: jest.fn(),
|
||||
...propOverrides,
|
||||
};
|
||||
|
||||
return render(<LogsNavigationPages {...props} />);
|
||||
};
|
||||
|
||||
describe('LogsNavigationPages', () => {
|
||||
it('should render logs navigation pages', () => {
|
||||
setup();
|
||||
expect(screen.getByTestId('logsNavigationPages')).toBeInTheDocument();
|
||||
});
|
||||
it('should render logs pages with correct range if normal order', () => {
|
||||
setup();
|
||||
expect(screen.getByText(/02:59:05 — 02:59:01/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/02:59:15 — 02:59:11/i)).toBeInTheDocument();
|
||||
});
|
||||
it('should render logs pages with correct range if flipped order', () => {
|
||||
setup({ oldestLogsFirst: true });
|
||||
expect(screen.getByText(/02:59:11 — 02:59:15/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/02:59:01 — 02:59:05/i)).toBeInTheDocument();
|
||||
});
|
||||
it('should invoke the callback when clicked', async () => {
|
||||
const onPageClicked = jest.fn();
|
||||
setup({ onClick: onPageClicked });
|
||||
expect(onPageClicked).not.toHaveBeenCalled();
|
||||
await userEvent.click(screen.getByText(/02:59:05 — 02:59:01/i));
|
||||
expect(onPageClicked).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { dateTimeFormat, systemDateFormats, GrafanaTheme2 } from '@grafana/data';
|
||||
import { TimeZone } from '@grafana/schema';
|
||||
import { ScrollContainer, Spinner, useTheme2, clearButtonStyles } from '@grafana/ui';
|
||||
|
||||
import { LogsPage } from './LogsNavigation';
|
||||
|
||||
type Props = {
|
||||
pages: LogsPage[];
|
||||
currentPageIndex: number;
|
||||
oldestLogsFirst: boolean;
|
||||
timeZone: TimeZone;
|
||||
loading: boolean;
|
||||
onClick: (page: LogsPage, pageNumber: number) => void;
|
||||
};
|
||||
|
||||
export function LogsNavigationPages({ pages, currentPageIndex, oldestLogsFirst, timeZone, loading, onClick }: Props) {
|
||||
const formatTime = (time: number) => {
|
||||
return `${dateTimeFormat(time, {
|
||||
format: systemDateFormats.interval.second,
|
||||
timeZone: timeZone,
|
||||
})}`;
|
||||
};
|
||||
|
||||
const createPageContent = (page: LogsPage, index: number) => {
|
||||
if (currentPageIndex === index && loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
const topContent = formatTime(oldestLogsFirst ? page.logsRange.from : page.logsRange.to);
|
||||
const bottomContent = formatTime(oldestLogsFirst ? page.logsRange.to : page.logsRange.from);
|
||||
return `${topContent} — ${bottomContent}`;
|
||||
};
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, loading);
|
||||
|
||||
return (
|
||||
<ScrollContainer>
|
||||
<div className={styles.pagesWrapper} data-testid="logsNavigationPages">
|
||||
<div className={styles.pagesContainer}>
|
||||
{pages.map((page: LogsPage, index: number) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`page${index + 1}`}
|
||||
className={cx(clearButtonStyles(theme), styles.page)}
|
||||
key={page.queryRange.to}
|
||||
onClick={() => {
|
||||
onClick(page, index + 1);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className={cx(styles.line, { selectedBg: currentPageIndex === index })} />
|
||||
<div className={cx(styles.time, { selectedText: currentPageIndex === index })}>
|
||||
{createPageContent(page, index)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, loading: boolean) => {
|
||||
return {
|
||||
pagesWrapper: css({
|
||||
height: '100%',
|
||||
paddingLeft: theme.spacing(0.5),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'&::after': {
|
||||
content: "''",
|
||||
display: 'block',
|
||||
background: `repeating-linear-gradient(135deg, ${theme.colors.background.primary}, ${theme.colors.background.primary} 5px, ${theme.colors.background.secondary} 5px, ${theme.colors.background.secondary} 15px)`,
|
||||
width: '3px',
|
||||
height: 'inherit',
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}),
|
||||
pagesContainer: css({
|
||||
display: 'flex',
|
||||
padding: 0,
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
page: css({
|
||||
display: 'flex',
|
||||
margin: theme.spacing(2, 0),
|
||||
cursor: loading ? 'auto' : 'pointer',
|
||||
whiteSpace: 'normal',
|
||||
'.selectedBg': {
|
||||
background: theme.colors.primary.main,
|
||||
},
|
||||
'.selectedText': {
|
||||
color: theme.colors.primary.main,
|
||||
},
|
||||
}),
|
||||
line: css({
|
||||
width: '3px',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
background: theme.colors.text.secondary,
|
||||
}),
|
||||
time: css({
|
||||
width: '60px',
|
||||
minHeight: '80px',
|
||||
fontSize: theme.v1.typography.size.sm,
|
||||
paddingLeft: theme.spacing(0.5),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
};
|
||||
};
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { LogsVisualisationType } from '../../explore/Logs/Logs';
|
||||
|
||||
@@ -150,7 +149,7 @@ const LogRowsComponent = forwardRef<HTMLDivElement | null, LogRowsComponentProps
|
||||
if (ref) {
|
||||
return styles.forwardedScrollableLogRows;
|
||||
}
|
||||
return config.featureToggles.logsInfiniteScrolling ? styles.scrollableLogRows : styles.logRows;
|
||||
return styles.scrollableLogRows;
|
||||
}, [ref]);
|
||||
|
||||
const scrollIntoView = useCallback(
|
||||
|
||||
@@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { CoreApp, LogRowModel, dateTimeForTimeZone, rangeUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LogsSortOrder } from '@grafana/schema';
|
||||
|
||||
import { InfiniteScroll, Props, SCROLLING_THRESHOLD } from './InfiniteScroll';
|
||||
@@ -106,14 +105,6 @@ function setup(
|
||||
return { element, events, scrollTo, wheel };
|
||||
}
|
||||
|
||||
const originalState = config.featureToggles.logsInfiniteScrolling;
|
||||
beforeAll(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = true;
|
||||
});
|
||||
afterAll(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = originalState;
|
||||
});
|
||||
|
||||
describe('InfiniteScroll', () => {
|
||||
test('Wraps components without adding DOM elements', async () => {
|
||||
const { container } = render(
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ReactNode, MutableRefObject, useCallback, useEffect, useRef, useState }
|
||||
import { AbsoluteTimeRange, CoreApp, LogRowModel, TimeRange, rangeUtil } from '@grafana/data';
|
||||
// import { convertRawToRange, isRelativeTime, isRelativeTimeRange } from '@grafana/data/internal';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { LogsSortOrder, TimeZone } from '@grafana/schema';
|
||||
import { Button, Icon } from '@grafana/ui';
|
||||
|
||||
@@ -86,7 +86,7 @@ export const InfiniteScroll = ({
|
||||
}
|
||||
|
||||
function handleScroll(event: Event | WheelEvent) {
|
||||
if (!scrollElement || !loadMoreLogs || !rows.length || loading || !config.featureToggles.logsInfiniteScrolling) {
|
||||
if (!scrollElement || !loadMoreLogs || !rows.length || loading) {
|
||||
return;
|
||||
}
|
||||
const scrollDirection = shouldLoadMore(event, lastEvent.current, countRef, scrollElement, lastScroll.current);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { act, render, screen } from '@testing-library/react';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
|
||||
import { createTheme, dateTimeForTimeZone, rangeUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LogsSortOrder } from '@grafana/schema';
|
||||
|
||||
import { ScrollDirection, SCROLLING_THRESHOLD } from '../InfiniteScroll';
|
||||
@@ -110,14 +109,6 @@ function setup(
|
||||
return { element, events, scrollTo, wheel };
|
||||
}
|
||||
|
||||
const originalState = config.featureToggles.logsInfiniteScrolling;
|
||||
beforeAll(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = true;
|
||||
});
|
||||
afterAll(() => {
|
||||
config.featureToggles.logsInfiniteScrolling = originalState;
|
||||
});
|
||||
|
||||
describe('InfiniteScroll', () => {
|
||||
describe.each([LogsSortOrder.Descending, LogsSortOrder.Ascending])(
|
||||
'When the sort order is descending',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ListChildComponentProps, ListOnItemsRenderedProps } from 'react-window'
|
||||
|
||||
import { AbsoluteTimeRange, LogsSortOrder, TimeRange } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Spinner, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { canScrollBottom, canScrollTop, getVisibleRange, ScrollDirection, shouldLoadMore } from '../InfiniteScroll';
|
||||
@@ -139,7 +139,7 @@ export const InfiniteScroll = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scrollElement || !loadMore || !config.featureToggles.logsInfiniteScrolling) {
|
||||
if (!scrollElement || !loadMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
sortDataFrame,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
|
||||
import { getMockFrames } from 'app/plugins/datasource/loki/mocks/frames';
|
||||
|
||||
@@ -292,7 +291,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
value: `2 lines displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
@@ -374,7 +373,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
value: `2 lines displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
@@ -386,9 +385,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('with infinite scrolling enabled it should return expected logs model', () => {
|
||||
config.featureToggles.logsInfiniteScrolling = true;
|
||||
|
||||
it('it should return expected logs model', () => {
|
||||
const series: DataFrame[] = [
|
||||
createDataFrame({
|
||||
fields: [
|
||||
@@ -421,8 +418,6 @@ describe('dataFrameToLogsModel', () => {
|
||||
value: `1 line displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
|
||||
config.featureToggles.logsInfiniteScrolling = false;
|
||||
});
|
||||
|
||||
it('given one series with limit as custom meta property should return correct limit', () => {
|
||||
@@ -430,7 +425,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
const logsModel = dataFrameToLogsModel(series, 1);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
value: `2 lines displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
});
|
||||
@@ -639,7 +634,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
value: `2 lines displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
@@ -758,7 +753,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
expect(logsModel.meta).toHaveLength(3);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
value: `2 lines displayed`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { SIPrefix } from '@grafana/data/internal';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { BarAlignment, GraphDrawStyle, StackingMode } from '@grafana/schema';
|
||||
import { colors } from '@grafana/ui';
|
||||
import { getThemeColor } from 'app/core/utils/colors';
|
||||
@@ -576,8 +575,7 @@ function adjustMetaInfo(logsModel: LogsModel, visibleRangeMs?: number, requested
|
||||
metaLimitValue = `${limit} lines shown — ${coverage}% (${rangeUtil.msRangeToTimeString(visibleRangeMs)}) of ${rangeUtil.msRangeToTimeString(requestedRangeMs)}`;
|
||||
}
|
||||
} else {
|
||||
const description = config.featureToggles.logsInfiniteScrolling ? 'displayed' : 'returned';
|
||||
metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} ${description}`;
|
||||
metaLimitValue = `${logsModel.rows.length} ${logsModel.rows.length > 1 ? 'lines' : 'line'} displayed`;
|
||||
}
|
||||
|
||||
logsModelMeta[limitIndex] = {
|
||||
|
||||
@@ -464,7 +464,7 @@ export const LogsPanel = ({
|
||||
|
||||
const loadMoreLogs = useCallback(
|
||||
async (scrollRange: AbsoluteTimeRange) => {
|
||||
if (!data.request || !config.featureToggles.logsInfiniteScrolling || loadingRef.current) {
|
||||
if (!data.request || loadingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -9973,11 +9973,7 @@
|
||||
"wrap-lines": "Wrap lines"
|
||||
},
|
||||
"logs-navigation": {
|
||||
"newer-logs": "Newer logs",
|
||||
"older-logs": "Older logs",
|
||||
"scroll-bottom": "Scroll to bottom",
|
||||
"scroll-top": "Scroll to top",
|
||||
"start-of-range": "Start of range"
|
||||
"scroll-top": "Scroll to top"
|
||||
},
|
||||
"logs-panel": {
|
||||
"render-common-labels": {
|
||||
|
||||
Reference in New Issue
Block a user