diff --git a/packages/grafana-data/src/index.ts b/packages/grafana-data/src/index.ts index d6d414a81c5..76195d7d8c0 100644 --- a/packages/grafana-data/src/index.ts +++ b/packages/grafana-data/src/index.ts @@ -764,6 +764,8 @@ export { type DataSourceWithQueryModificationSupport, hasToggleableQueryFiltersSupport, hasQueryModificationSupport, + LogSortOrderChangeEvent, + type LogSortOrderChangePayload, } from './types/logs'; export { type AnnotationQuery, diff --git a/packages/grafana-data/src/types/logs.ts b/packages/grafana-data/src/types/logs.ts index 86df7bde61f..67a194202a7 100644 --- a/packages/grafana-data/src/types/logs.ts +++ b/packages/grafana-data/src/types/logs.ts @@ -1,6 +1,8 @@ import { Observable } from 'rxjs'; -import { DataQuery } from '@grafana/schema'; +import { DataQuery, LogsSortOrder } from '@grafana/schema'; + +import { BusEventWithPayload } from '../events/types'; import { KeyValue, Labels } from './data'; import { DataFrame } from './dataFrame'; @@ -366,3 +368,11 @@ export const hasQueryModificationSupport = ( 'getSupportedQueryModifications' in datasource ); }; + +export interface LogSortOrderChangePayload { + order: LogsSortOrder; +} + +export class LogSortOrderChangeEvent extends BusEventWithPayload { + static type = 'logs-sort-order-change'; +} diff --git a/public/app/features/explore/Logs/Logs.test.tsx b/public/app/features/explore/Logs/Logs.test.tsx index 620f576e118..1749d92d48d 100644 --- a/public/app/features/explore/Logs/Logs.test.tsx +++ b/public/app/features/explore/Logs/Logs.test.tsx @@ -14,10 +14,12 @@ import { toUtc, createDataFrame, ExploreLogsPanelState, + DataQuery, } from '@grafana/data'; import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize'; import { config } from '@grafana/runtime'; import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields'; +import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; import { configureStore } from 'app/store/configureStore'; import { initialExploreState } from '../state/main'; @@ -46,6 +48,17 @@ jest.mock('../state/explorePane', () => ({ changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => { return fakeChangePanelState(exploreId, panel, panelState); }, + changeQueries: (args: { queries: DataQuery[]; exploreId: string | undefined }) => { + return fakeChangeQueries(args); + }, +})); + +const fakeChangeQueries = jest.fn().mockReturnValue({ type: 'fakeChangeQueries' }); +jest.mock('../state/query', () => ({ + ...jest.requireActual('../state/query'), + changeQueries: (args: { queries: DataQuery[]; exploreId: string | undefined }) => { + return fakeChangeQueries(args); + }, })); describe('Logs', () => { @@ -377,6 +390,26 @@ describe('Logs', () => { expect(logRows[2].textContent).toContain('log message 3'); }); + it('should sync the query direction when changing the order of loki queries', async () => { + const query = { expr: '{a="b"}', refId: 'A', datasource: { type: 'loki' } }; + setup({ logsQueries: [query] }); + const oldestFirstSelection = screen.getByLabelText('Oldest first'); + await userEvent.click(oldestFirstSelection); + expect(fakeChangeQueries).toHaveBeenCalledWith({ + exploreId: 'left', + queries: [{ ...query, direction: LokiQueryDirection.Forward }], + }); + }); + + it('should not change the query direction when changing the order of non-loki queries', async () => { + fakeChangeQueries.mockClear(); + const query = { refId: 'B' }; + setup({ logsQueries: [query] }); + const oldestFirstSelection = screen.getByLabelText('Oldest first'); + await userEvent.click(oldestFirstSelection); + expect(fakeChangeQueries).not.toHaveBeenCalled(); + }); + describe('for permalinking', () => { it('should dispatch a `changePanelState` event without the id', () => { const panelState = { logs: { id: '1' } }; diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 3a026431656..a82f30e196b 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -58,6 +58,7 @@ import { LogRowContextModal } from 'app/features/logs/components/log-context/Log import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel'; import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils'; import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen'; +import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils'; import { getState } from 'app/store/store'; import { ExploreItemState, useDispatch } from 'app/types'; @@ -72,6 +73,7 @@ import { import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext'; import { getUrlStateFromPaneState } from '../hooks/useStateSync'; import { changePanelState } from '../state/explorePane'; +import { changeQueries } from '../state/query'; import { LogsFeedback } from './LogsFeedback'; import { LogsMetaRow } from './LogsMetaRow'; @@ -468,6 +470,31 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { const newSortOrder = logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending; store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder); + if (logsQueries) { + let hasLokiQueries = false; + const newQueries = logsQueries.map((query) => { + if (query.datasource?.type !== 'loki' || !isLokiQuery(query)) { + return query; + } + hasLokiQueries = true; + + if (query.direction === LokiQueryDirection.Scan) { + // Don't override Scan. When the direction is Scan it means that the user specifically assigned this direction to the query. + return query; + } + const newDirection = + newSortOrder === LogsSortOrder.Ascending ? LokiQueryDirection.Forward : LokiQueryDirection.Backward; + if (newDirection !== query.direction) { + query.direction = newDirection; + } + return query; + }); + + if (hasLokiQueries) { + dispatch(changeQueries({ exploreId, queries: newQueries })); + } + } + setLogsSortOrder(newSortOrder); }, 0); cancelFlippingTimer.current = window.setTimeout(() => setIsFlipping(false), 1000); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx index bb41f4cdf47..47626e222f7 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.test.tsx @@ -15,6 +15,9 @@ import { LokiQueryEditorProps } from './types'; jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), + getAppEvents: jest.fn().mockReturnValue({ + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }), reportInteraction: jest.fn(), }; }); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx index c36399ea7dc..a7a55a5b709 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditorByApp.test.tsx @@ -9,6 +9,13 @@ import { testIds as regularTestIds } from './LokiQueryEditor'; import { LokiQueryEditorByApp } from './LokiQueryEditorByApp'; import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getAppEvents: jest.fn().mockReturnValue({ + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }), +})); + function setup(app: CoreApp): RenderResult { const dataSource = createLokiDatasource(); dataSource.metadataRequest = jest.fn(); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx index dadb297a1a4..ce1aabe2391 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoFieldWrapper.test.tsx @@ -6,6 +6,13 @@ import { createLokiDatasource } from '../../__mocks__/datasource'; import { MonacoQueryFieldWrapper, Props } from './MonacoQueryFieldWrapper'; +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + getAppEvents: jest.fn().mockReturnValue({ + subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }), + }), +})); + function renderComponent({ initialValue = '', onChange = jest.fn(), onRunQuery = jest.fn() }: Partial = {}) { const datasource = createLokiDatasource(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx index cb827dd3bf5..bbd8f242906 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.test.tsx @@ -1,9 +1,37 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { LokiQuery, LokiQueryType } from '../../types'; +import { CoreApp, LogSortOrderChangeEvent, LogsSortOrder, store } from '@grafana/data'; +import { config, getAppEvents } from '@grafana/runtime'; -import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; +import { LokiQuery, LokiQueryDirection, LokiQueryType } from '../../types'; + +import { LokiQueryBuilderOptions, Props } from './LokiQueryBuilderOptions'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + config: { + ...jest.requireActual('@grafana/runtime').config, + featureToggles: { + ...jest.requireActual('@grafana/runtime').featureToggles, + lokiShardSplitting: true, + }, + }, + getAppEvents: jest.fn(), +})); + +const subscribeMock = jest.fn(); +beforeAll(() => { + config.featureToggles.lokiShardSplitting = true; + subscribeMock.mockImplementation(() => ({ unsubscribe: jest.fn() })); + jest.mocked(getAppEvents).mockReturnValue({ + publish: jest.fn(), + getStream: jest.fn(), + subscribe: subscribeMock, + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }); +}); describe('LokiQueryBuilderOptions', () => { it('can change query type', async () => { @@ -86,7 +114,7 @@ describe('LokiQueryBuilderOptions', () => { }); it('shows correct options for log query', async () => { - setup({ expr: '{foo="bar"}' }); + setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Backward }); expect(screen.getByText('Line limit: 20')).toBeInTheDocument(); expect(screen.getByText('Type: Range')).toBeInTheDocument(); expect(screen.getByText('Direction: Backward')).toBeInTheDocument(); @@ -184,9 +212,91 @@ describe('LokiQueryBuilderOptions', () => { step: '4s', }); }); + + describe('Query direction', () => { + it("initializes query direction when it's empty", async () => { + const onChange = jest.fn(); + setup({ expr: '{foo="bar"}' }, onChange); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Backward, + }) + ); + }); + + it('uses backward as default in Explore with no previous stored preference', async () => { + const onChange = jest.fn(); + store.delete('grafana.explore.logs.sortOrder'); + setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore }); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Backward, + }) + ); + }); + + it('uses the stored sorting option to determine direction in Explore', async () => { + store.set('grafana.explore.logs.sortOrder', LogsSortOrder.Ascending); + const onChange = jest.fn(); + setup({ expr: '{foo="bar"}' }, onChange, { app: CoreApp.Explore }); + await waitFor(() => + expect(onChange).toHaveBeenCalledWith({ + expr: '{foo="bar"}', + refId: 'A', + direction: LokiQueryDirection.Forward, + }) + ); + store.delete('grafana.explore.logs.sortOrder'); + }); + + describe('Event handling', () => { + let listener: (event: LogSortOrderChangeEvent) => void = jest.fn(); + const onChangeMock = jest.fn(); + beforeEach(() => { + onChangeMock.mockClear(); + listener = jest.fn(); + subscribeMock.mockImplementation((_: unknown, callback: (event: LogSortOrderChangeEvent) => void) => { + listener = callback; + return { unsubscribe: jest.fn() }; + }); + }); + it('subscribes to sort change event and updates the direction', () => { + setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Backward }, onChangeMock, { + app: CoreApp.Dashboard, + }); + expect(screen.getByText(/Direction: Backward/)).toBeInTheDocument(); + listener( + new LogSortOrderChangeEvent({ + order: LogsSortOrder.Ascending, + }) + ); + expect(onChangeMock).toHaveBeenCalledTimes(1); + expect(onChangeMock).toHaveBeenCalledWith({ + direction: 'forward', + expr: '{foo="bar"}', + refId: 'A', + }); + }); + + it('does not change the direction when the current direction is scan', () => { + setup({ expr: '{foo="bar"}', direction: LokiQueryDirection.Scan }, onChangeMock, { app: CoreApp.Dashboard }); + expect(screen.getByText(/Direction: Scan/)).toBeInTheDocument(); + listener( + new LogSortOrderChangeEvent({ + order: LogsSortOrder.Ascending, + }) + ); + expect(onChangeMock).not.toHaveBeenCalled(); + }); + }); + }); }); -function setup(queryOverrides: Partial = {}, onChange = jest.fn()) { +function setup(queryOverrides: Partial = {}, onChange = jest.fn(), propOverrides: Partial = {}) { const props = { query: { refId: 'A', @@ -197,6 +307,7 @@ function setup(queryOverrides: Partial = {}, onChange = jest.fn()) { onChange, maxLines: 20, queryStats: { streams: 0, chunks: 0, bytes: 0, entries: 0 }, + ...propOverrides, }; const { container } = render(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx index bd7c2e6ff00..29d02331867 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx @@ -1,10 +1,18 @@ import { trim } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import * as React from 'react'; -import { CoreApp, isValidDuration, isValidGrafanaDuration, SelectableValue } from '@grafana/data'; +import { + CoreApp, + isValidDuration, + isValidGrafanaDuration, + LogSortOrderChangeEvent, + LogsSortOrder, + SelectableValue, + store, +} from '@grafana/data'; import { EditorField, EditorRow, QueryOptionGroup } from '@grafana/experimental'; -import { config, reportInteraction } from '@grafana/runtime'; +import { config, getAppEvents, reportInteraction } from '@grafana/runtime'; import { Alert, AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui'; import { @@ -30,6 +38,13 @@ export const LokiQueryBuilderOptions = React.memo( ({ app, query, onChange, onRunQuery, maxLines, queryStats }) => { const [splitDurationValid, setSplitDurationValid] = useState(true); + useEffect(() => { + // Initialize the query direction according to the current environment. + if (!query.direction) { + onChange({ ...query, direction: getDefaultQueryDirection(app) }); + } + }, [app, onChange, query]); + useEffect(() => { if (query.step && !isValidGrafanaDuration(`${query.step}`) && parseInt(query.step, 10)) { onChange({ @@ -44,10 +59,13 @@ export const LokiQueryBuilderOptions = React.memo( onRunQuery(); }; - const onQueryDirectionChange = (value: LokiQueryDirection) => { - onChange({ ...query, direction: value }); - onRunQuery(); - }; + const onQueryDirectionChange = useCallback( + (value: LokiQueryDirection) => { + onChange({ ...query, direction: value }); + onRunQuery(); + }, + [onChange, onRunQuery, query] + ); const onResolutionChange = (option: SelectableValue) => { reportInteraction('grafana_loki_resolution_clicked', { @@ -87,14 +105,33 @@ export const LokiQueryBuilderOptions = React.memo( onRunQuery(); } + useEffect(() => { + if (app !== CoreApp.Dashboard && app !== CoreApp.PanelEditor) { + return; + } + const subscription = getAppEvents().subscribe(LogSortOrderChangeEvent, (sortEvent: LogSortOrderChangeEvent) => { + if (query.direction === LokiQueryDirection.Scan) { + return; + } + const newDirection = + sortEvent.payload.order === LogsSortOrder.Ascending + ? LokiQueryDirection.Forward + : LokiQueryDirection.Backward; + if (newDirection !== query.direction) { + onQueryDirectionChange(newDirection); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [app, onQueryDirectionChange, query.direction]); + let queryType = getLokiQueryType(query); const isLogQuery = isLogsQuery(query.expr); const filteredQueryTypeOptions = isLogQuery ? queryTypeOptions.filter((o) => o.value !== LokiQueryType.Instant) : queryTypeOptions; - const queryDirection = query.direction ?? LokiQueryDirection.Backward; - // if the state's queryType is still Instant, trigger a change to range for log queries if (isLogQuery && queryType === LokiQueryType.Instant) { onChange({ ...query, queryType: LokiQueryType.Range }); @@ -112,7 +149,7 @@ export const LokiQueryBuilderOptions = React.memo( ( /> - + )} @@ -214,7 +251,7 @@ function getCollapsedInfo( maxLines: number, isLogQuery: boolean, isValidStep: boolean, - direction: LokiQueryDirection + direction: LokiQueryDirection | undefined ): string[] { const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType); const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1)); @@ -227,7 +264,7 @@ function getCollapsedInfo( items.push(`Type: ${queryTypeLabel?.label}`); - if (isLogQuery) { + if (isLogQuery && direction) { items.push(`Line limit: ${query.maxLines ?? maxLines}`); items.push(`Direction: ${getQueryDirectionLabel(direction)}`); } else { @@ -243,4 +280,20 @@ function getCollapsedInfo( return items; } +function getDefaultQueryDirection(app?: CoreApp) { + if (app !== CoreApp.Explore) { + /** + * The default direction is backward because the default sort order is Descending. + * See: + * - public/app/features/explore/Logs/Logs.tsx + * - public/app/plugins/panel/logs/module.tsx + */ + return LokiQueryDirection.Backward; + } + // See app/features/explore/Logs/utils/logs + const key = 'grafana.explore.logs.sortOrder'; + const storedOrder = store.get(key) || LogsSortOrder.Descending; + return storedOrder === LogsSortOrder.Ascending ? LokiQueryDirection.Forward : LokiQueryDirection.Backward; +} + LokiQueryBuilderOptions.displayName = 'LokiQueryBuilderOptions'; diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx index 86795875d59..5f83f431ba3 100644 --- a/public/app/plugins/panel/logs/LogsPanel.test.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx @@ -13,7 +13,9 @@ import { LogsDedupStrategy, EventBusSrv, DataFrameType, + LogSortOrderChangeEvent, } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; import * as grafanaUI from '@grafana/ui'; import * as styles from 'app/features/logs/components/getLogRowStyles'; import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal'; @@ -39,6 +41,7 @@ const datasourceSrv = new DatasourceSrvMock(defaultDs, { const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), + getAppEvents: jest.fn(), getDataSourceSrv: () => getDataSourceSrvMock(), })); @@ -69,7 +72,35 @@ const defaultProps = { scopedVars: {}, startTime: 1, }, - series: [], + series: [ + createDataFrame({ + refId: 'A', + fields: [ + { + name: 'timestamp', + type: FieldType.time, + values: ['2019-04-26T09:28:11.352440161Z'], + }, + { + name: 'body', + type: FieldType.string, + values: ['logline text'], + }, + { + name: 'labels', + type: FieldType.other, + values: [ + { + app: 'common_app', + }, + ], + }, + ], + meta: { + type: DataFrameType.LogLines, + }, + }), + ], state: LoadingState.Done, timeRange: getDefaultTimeRange(), }, @@ -103,7 +134,32 @@ const defaultProps = { onChangeTimeRange: jest.fn(), }; +const publishMock = jest.fn(); +beforeAll(() => { + jest.mocked(getAppEvents).mockReturnValue({ + publish: publishMock, + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }); +}); + describe('LogsPanel', () => { + it('publishes an event with the current sort order', async () => { + publishMock.mockClear(); + setup(); + + await screen.findByText('logline text'); + + expect(publishMock).toHaveBeenCalledTimes(1); + expect(publishMock).toHaveBeenCalledWith( + new LogSortOrderChangeEvent({ + order: LogsSortOrder.Descending, + }) + ); + }); + describe('when returned series include common labels', () => { const seriesWithCommonLabels = [ createDataFrame({ diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 5b92496f86c..73d835edbbf 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -27,9 +27,10 @@ import { TimeZone, toUtc, urlUtil, + LogSortOrderChangeEvent, } from '@grafana/data'; import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil'; -import { config } from '@grafana/runtime'; +import { config, getAppEvents } from '@grafana/runtime'; import { ScrollContainer, usePanelContext, useStyles2 } from '@grafana/ui'; import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll'; @@ -143,8 +144,16 @@ export const LogsPanel = ({ // Prevents the scroll position to change when new data from infinite scrolling is received const keepScrollPositionRef = useRef(false); let closeCallback = useRef<() => void>(); - const { eventBus, onAddAdHocFilter } = usePanelContext(); + + useEffect(() => { + getAppEvents().publish( + new LogSortOrderChangeEvent({ + order: sortOrder, + }) + ); + }, [sortOrder]); + const onLogRowHover = useCallback( (row?: LogRowModel) => { if (row) {