diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a4eca4f9ee7..ce5adca782c 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e5c59000c8d..5e9ad9ff8d4 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -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; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 28ff9e1809e..a557e32087b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 4ee6e57a9ed..99753233108 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index bb0e03295a0..d60ad45f64e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/core/utils/shortLinks.test.ts b/public/app/core/utils/shortLinks.test.ts index 39a43af6629..f88f302b4db 100644 --- a/public/app/core/utils/shortLinks.test.ts +++ b/public/app/core/utils/shortLinks.test.ts @@ -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 = { diff --git a/public/app/core/utils/shortLinks.ts b/public/app/core/utils/shortLinks.ts index 706857c122c..208f2ae0fcc 100644 --- a/public/app/core/utils/shortLinks.ts +++ b/public/app/core/utils/shortLinks.ts @@ -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); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index f8bd275930b..79678f4e15a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -475,7 +475,6 @@ export class Explore extends PureComponent { onStopScanning={this.onStopScanning} eventBus={this.logsEventBus} splitOpenFn={this.splitOpenFnLogs} - scrollElement={this.scrollElement} isFilterLabelActive={this.isFilterLabelActive} onClickFilterString={this.onClickFilterString} onClickFilterOutString={this.onClickFilterOutString} diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 958a8c589f8..93836280762 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -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; }); }); diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 7820e5eb9e9..055f4d454b7 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -119,11 +119,8 @@ interface Props extends Themeable2 { ) => Promise; 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; logsFrames?: DataFrame[]; range: TimeRange; @@ -183,8 +180,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { getFieldLinks, theme, logsQueries, - clearCache, - addResultsToCache, exploreId, getRowContext, getLogRowContextUi, @@ -193,7 +188,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { panelState, eventBus, onPinLineCallback, - scrollElement, } = props; const [showLabels, setShowLabels] = useState(store.getBool(SETTINGS_KEYS.showLabels, false)); const [showTime, setShowTime] = useState(store.getBool(SETTINGS_KEYS.showTime, true)); @@ -365,29 +359,17 @@ const UnthemedLogs: React.FunctionComponent = (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) => { ); 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) => { ); 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) => { visualisationType === 'logs' && hasData && ( <> -
+
= (props: Props) => { />
- + )} {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 }; -}; diff --git a/public/app/features/explore/Logs/LogsContainer.tsx b/public/app/features/explore/Logs/LogsContainer.tsx index 13bc78b5849..f93d66c6446 100644 --- a/public/app/features/explore/Logs/LogsContainer.tsx +++ b/public/app/features/explore/Logs/LogsContainer.tsx @@ -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; onClickFilterString: (value: string, refId?: string) => void; onClickFilterOutString: (value: string, refId?: string) => void; @@ -260,14 +253,6 @@ class LogsContainer extends PureComponent { - 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 ({ type LogsNavigationProps = ComponentProps; 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) => { @@ -37,132 +26,13 @@ const setup = (propOverrides?: Partial) => { }; 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( - - ); - 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( - - ); - - 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); }); }); diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx index 47a092b7138..b9b1c0c2c97 100644 --- a/public/app/features/explore/Logs/LogsNavigation.tsx +++ b/public/app/features/explore/Logs/LogsNavigation.tsx @@ -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([]); - - // These refs are to determine, if we want to clear up logs navigation when totally new query is run - const expectedQueriesRef = useRef(); - const expectedRangeRef = useRef(); - // 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 = ( - - ); - - const newerLogsButton = ( - - ); - - 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 (
- {!config.featureToggles.logsInfiniteScrolling && ( - <> - {oldestLogsFirst ? olderLogsButton : newerLogsButton} - - {oldestLogsFirst ? newerLogsButton : olderLogsButton} - - )} - {scrollToBottomLogs && ( - - )} - ))} -
-
- - ); -} - -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', - }), - }; -}; diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index 4094d4f515c..874234837ac 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -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 { - config.featureToggles.logsInfiniteScrolling = true; -}); -afterAll(() => { - config.featureToggles.logsInfiniteScrolling = originalState; -}); - describe('InfiniteScroll', () => { test('Wraps components without adding DOM elements', async () => { const { container } = render( diff --git a/public/app/features/logs/components/InfiniteScroll.tsx b/public/app/features/logs/components/InfiniteScroll.tsx index e21ca4a4447..3c4ab5001b1 100644 --- a/public/app/features/logs/components/InfiniteScroll.tsx +++ b/public/app/features/logs/components/InfiniteScroll.tsx @@ -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); diff --git a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx index 631c2831e9c..2507a3b5176 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.test.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.test.tsx @@ -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', diff --git a/public/app/features/logs/components/panel/InfiniteScroll.tsx b/public/app/features/logs/components/panel/InfiniteScroll.tsx index 45cc616ea61..3c7de64b6be 100644 --- a/public/app/features/logs/components/panel/InfiniteScroll.tsx +++ b/public/app/features/logs/components/panel/InfiniteScroll.tsx @@ -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; } diff --git a/public/app/features/logs/logsModel.test.ts b/public/app/features/logs/logsModel.test.ts index f6221b6c6af..23d0badb4f5 100644 --- a/public/app/features/logs/logsModel.test.ts +++ b/public/app/features/logs/logsModel.test.ts @@ -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({ diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index 87cdc031ffc..e60887518fe 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -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] = { diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index c2659859b3e..191e503fd90 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -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; } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 9539dbefc30..cc900022bb4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": {