mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Loki: Sync query direction with sort order in Explore and Dashboards (#98722)
* Logs: sync direction and sort order for loki queries in explore * Logs: emit event on sort order change * Loki query editor: listen to sort change events and update direction * Loki query editor: unsubscribe to sort event * Logs: don't publish events in Explore * LokiQueryBuilderOptions: use stored order as default value when in explore * Query builder options: initialize query direction * Logs: unit test * LogsPanel: update unit test * Update tests * LokiQueryBuilderOptions: unit test * Update public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> * Update public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderOptions.tsx --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com>
This commit is contained in:
parent
90e65f6ce6
commit
3eace5f7c8
@ -764,6 +764,8 @@ export {
|
|||||||
type DataSourceWithQueryModificationSupport,
|
type DataSourceWithQueryModificationSupport,
|
||||||
hasToggleableQueryFiltersSupport,
|
hasToggleableQueryFiltersSupport,
|
||||||
hasQueryModificationSupport,
|
hasQueryModificationSupport,
|
||||||
|
LogSortOrderChangeEvent,
|
||||||
|
type LogSortOrderChangePayload,
|
||||||
} from './types/logs';
|
} from './types/logs';
|
||||||
export {
|
export {
|
||||||
type AnnotationQuery,
|
type AnnotationQuery,
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Observable } from 'rxjs';
|
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 { KeyValue, Labels } from './data';
|
||||||
import { DataFrame } from './dataFrame';
|
import { DataFrame } from './dataFrame';
|
||||||
@ -366,3 +368,11 @@ export const hasQueryModificationSupport = <TQuery extends DataQuery>(
|
|||||||
'getSupportedQueryModifications' in datasource
|
'getSupportedQueryModifications' in datasource
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface LogSortOrderChangePayload {
|
||||||
|
order: LogsSortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LogSortOrderChangeEvent extends BusEventWithPayload<LogSortOrderChangePayload> {
|
||||||
|
static type = 'logs-sort-order-change';
|
||||||
|
}
|
||||||
|
@ -14,10 +14,12 @@ import {
|
|||||||
toUtc,
|
toUtc,
|
||||||
createDataFrame,
|
createDataFrame,
|
||||||
ExploreLogsPanelState,
|
ExploreLogsPanelState,
|
||||||
|
DataQuery,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
|
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
|
import { extractFieldsTransformer } from 'app/features/transformers/extractFields/extractFields';
|
||||||
|
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
import { initialExploreState } from '../state/main';
|
import { initialExploreState } from '../state/main';
|
||||||
@ -46,6 +48,17 @@ jest.mock('../state/explorePane', () => ({
|
|||||||
changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => {
|
changePanelState: (exploreId: string, panel: 'logs', panelState: {} | ExploreLogsPanelState) => {
|
||||||
return fakeChangePanelState(exploreId, panel, panelState);
|
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', () => {
|
describe('Logs', () => {
|
||||||
@ -377,6 +390,26 @@ describe('Logs', () => {
|
|||||||
expect(logRows[2].textContent).toContain('log message 3');
|
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', () => {
|
describe('for permalinking', () => {
|
||||||
it('should dispatch a `changePanelState` event without the id', () => {
|
it('should dispatch a `changePanelState` event without the id', () => {
|
||||||
const panelState = { logs: { id: '1' } };
|
const panelState = { logs: { id: '1' } };
|
||||||
|
@ -58,6 +58,7 @@ import { LogRowContextModal } from 'app/features/logs/components/log-context/Log
|
|||||||
import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel';
|
import { LogLevelColor, dedupLogRows, filterLogLevels } from 'app/features/logs/logsModel';
|
||||||
import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils';
|
import { getLogLevel, getLogLevelFromKey, getLogLevelInfo } from 'app/features/logs/utils';
|
||||||
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
|
import { LokiQueryDirection } from 'app/plugins/datasource/loki/dataquery.gen';
|
||||||
|
import { isLokiQuery } from 'app/plugins/datasource/loki/queryUtils';
|
||||||
import { getState } from 'app/store/store';
|
import { getState } from 'app/store/store';
|
||||||
import { ExploreItemState, useDispatch } from 'app/types';
|
import { ExploreItemState, useDispatch } from 'app/types';
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ import {
|
|||||||
import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext';
|
import { useContentOutlineContext } from '../ContentOutline/ContentOutlineContext';
|
||||||
import { getUrlStateFromPaneState } from '../hooks/useStateSync';
|
import { getUrlStateFromPaneState } from '../hooks/useStateSync';
|
||||||
import { changePanelState } from '../state/explorePane';
|
import { changePanelState } from '../state/explorePane';
|
||||||
|
import { changeQueries } from '../state/query';
|
||||||
|
|
||||||
import { LogsFeedback } from './LogsFeedback';
|
import { LogsFeedback } from './LogsFeedback';
|
||||||
import { LogsMetaRow } from './LogsMetaRow';
|
import { LogsMetaRow } from './LogsMetaRow';
|
||||||
@ -468,6 +470,31 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
|||||||
const newSortOrder =
|
const newSortOrder =
|
||||||
logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending;
|
logsSortOrder === LogsSortOrder.Descending ? LogsSortOrder.Ascending : LogsSortOrder.Descending;
|
||||||
store.set(SETTINGS_KEYS.logsSortOrder, newSortOrder);
|
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);
|
setLogsSortOrder(newSortOrder);
|
||||||
}, 0);
|
}, 0);
|
||||||
cancelFlippingTimer.current = window.setTimeout(() => setIsFlipping(false), 1000);
|
cancelFlippingTimer.current = window.setTimeout(() => setIsFlipping(false), 1000);
|
||||||
|
@ -15,6 +15,9 @@ import { LokiQueryEditorProps } from './types';
|
|||||||
jest.mock('@grafana/runtime', () => {
|
jest.mock('@grafana/runtime', () => {
|
||||||
return {
|
return {
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getAppEvents: jest.fn().mockReturnValue({
|
||||||
|
subscribe: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }),
|
||||||
|
}),
|
||||||
reportInteraction: jest.fn(),
|
reportInteraction: jest.fn(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,13 @@ import { testIds as regularTestIds } from './LokiQueryEditor';
|
|||||||
import { LokiQueryEditorByApp } from './LokiQueryEditorByApp';
|
import { LokiQueryEditorByApp } from './LokiQueryEditorByApp';
|
||||||
import { testIds as alertingTestIds } from './LokiQueryEditorForAlerting';
|
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 {
|
function setup(app: CoreApp): RenderResult {
|
||||||
const dataSource = createLokiDatasource();
|
const dataSource = createLokiDatasource();
|
||||||
dataSource.metadataRequest = jest.fn();
|
dataSource.metadataRequest = jest.fn();
|
||||||
|
@ -6,6 +6,13 @@ import { createLokiDatasource } from '../../__mocks__/datasource';
|
|||||||
|
|
||||||
import { MonacoQueryFieldWrapper, Props } from './MonacoQueryFieldWrapper';
|
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<Props> = {}) {
|
function renderComponent({ initialValue = '', onChange = jest.fn(), onRunQuery = jest.fn() }: Partial<Props> = {}) {
|
||||||
const datasource = createLokiDatasource();
|
const datasource = createLokiDatasource();
|
||||||
|
|
||||||
|
@ -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 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', () => {
|
describe('LokiQueryBuilderOptions', () => {
|
||||||
it('can change query type', async () => {
|
it('can change query type', async () => {
|
||||||
@ -86,7 +114,7 @@ describe('LokiQueryBuilderOptions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows correct options for log query', async () => {
|
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('Line limit: 20')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Type: Range')).toBeInTheDocument();
|
expect(screen.getByText('Type: Range')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Direction: Backward')).toBeInTheDocument();
|
expect(screen.getByText('Direction: Backward')).toBeInTheDocument();
|
||||||
@ -184,9 +212,91 @@ describe('LokiQueryBuilderOptions', () => {
|
|||||||
step: '4s',
|
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<LokiQuery> = {}, onChange = jest.fn()) {
|
function setup(queryOverrides: Partial<LokiQuery> = {}, onChange = jest.fn(), propOverrides: Partial<Props> = {}) {
|
||||||
const props = {
|
const props = {
|
||||||
query: {
|
query: {
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
@ -197,6 +307,7 @@ function setup(queryOverrides: Partial<LokiQuery> = {}, onChange = jest.fn()) {
|
|||||||
onChange,
|
onChange,
|
||||||
maxLines: 20,
|
maxLines: 20,
|
||||||
queryStats: { streams: 0, chunks: 0, bytes: 0, entries: 0 },
|
queryStats: { streams: 0, chunks: 0, bytes: 0, entries: 0 },
|
||||||
|
...propOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<LokiQueryBuilderOptions {...props} />);
|
const { container } = render(<LokiQueryBuilderOptions {...props} />);
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { trim } from 'lodash';
|
import { trim } from 'lodash';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import * as React 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 { 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 { Alert, AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -30,6 +38,13 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
|||||||
({ app, query, onChange, onRunQuery, maxLines, queryStats }) => {
|
({ app, query, onChange, onRunQuery, maxLines, queryStats }) => {
|
||||||
const [splitDurationValid, setSplitDurationValid] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (query.step && !isValidGrafanaDuration(`${query.step}`) && parseInt(query.step, 10)) {
|
if (query.step && !isValidGrafanaDuration(`${query.step}`) && parseInt(query.step, 10)) {
|
||||||
onChange({
|
onChange({
|
||||||
@ -44,10 +59,13 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
|||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onQueryDirectionChange = (value: LokiQueryDirection) => {
|
const onQueryDirectionChange = useCallback(
|
||||||
onChange({ ...query, direction: value });
|
(value: LokiQueryDirection) => {
|
||||||
onRunQuery();
|
onChange({ ...query, direction: value });
|
||||||
};
|
onRunQuery();
|
||||||
|
},
|
||||||
|
[onChange, onRunQuery, query]
|
||||||
|
);
|
||||||
|
|
||||||
const onResolutionChange = (option: SelectableValue<number>) => {
|
const onResolutionChange = (option: SelectableValue<number>) => {
|
||||||
reportInteraction('grafana_loki_resolution_clicked', {
|
reportInteraction('grafana_loki_resolution_clicked', {
|
||||||
@ -87,14 +105,33 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
|||||||
onRunQuery();
|
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);
|
let queryType = getLokiQueryType(query);
|
||||||
const isLogQuery = isLogsQuery(query.expr);
|
const isLogQuery = isLogsQuery(query.expr);
|
||||||
const filteredQueryTypeOptions = isLogQuery
|
const filteredQueryTypeOptions = isLogQuery
|
||||||
? queryTypeOptions.filter((o) => o.value !== LokiQueryType.Instant)
|
? queryTypeOptions.filter((o) => o.value !== LokiQueryType.Instant)
|
||||||
: queryTypeOptions;
|
: queryTypeOptions;
|
||||||
|
|
||||||
const queryDirection = query.direction ?? LokiQueryDirection.Backward;
|
|
||||||
|
|
||||||
// if the state's queryType is still Instant, trigger a change to range for log queries
|
// if the state's queryType is still Instant, trigger a change to range for log queries
|
||||||
if (isLogQuery && queryType === LokiQueryType.Instant) {
|
if (isLogQuery && queryType === LokiQueryType.Instant) {
|
||||||
onChange({ ...query, queryType: LokiQueryType.Range });
|
onChange({ ...query, queryType: LokiQueryType.Range });
|
||||||
@ -112,7 +149,7 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
|||||||
<EditorRow>
|
<EditorRow>
|
||||||
<QueryOptionGroup
|
<QueryOptionGroup
|
||||||
title="Options"
|
title="Options"
|
||||||
collapsedInfo={getCollapsedInfo(query, queryType, maxLines, isLogQuery, isValidStep, queryDirection)}
|
collapsedInfo={getCollapsedInfo(query, queryType, maxLines, isLogQuery, isValidStep, query.direction)}
|
||||||
queryStats={queryStats}
|
queryStats={queryStats}
|
||||||
>
|
>
|
||||||
<EditorField
|
<EditorField
|
||||||
@ -145,7 +182,7 @@ export const LokiQueryBuilderOptions = React.memo<Props>(
|
|||||||
/>
|
/>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
<EditorField label="Direction" tooltip="Direction to search for logs.">
|
<EditorField label="Direction" tooltip="Direction to search for logs.">
|
||||||
<RadioButtonGroup options={queryDirections} value={queryDirection} onChange={onQueryDirectionChange} />
|
<RadioButtonGroup options={queryDirections} value={query.direction} onChange={onQueryDirectionChange} />
|
||||||
</EditorField>
|
</EditorField>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -214,7 +251,7 @@ function getCollapsedInfo(
|
|||||||
maxLines: number,
|
maxLines: number,
|
||||||
isLogQuery: boolean,
|
isLogQuery: boolean,
|
||||||
isValidStep: boolean,
|
isValidStep: boolean,
|
||||||
direction: LokiQueryDirection
|
direction: LokiQueryDirection | undefined
|
||||||
): string[] {
|
): string[] {
|
||||||
const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType);
|
const queryTypeLabel = queryTypeOptions.find((x) => x.value === queryType);
|
||||||
const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1));
|
const resolutionLabel = RESOLUTION_OPTIONS.find((x) => x.value === (query.resolution ?? 1));
|
||||||
@ -227,7 +264,7 @@ function getCollapsedInfo(
|
|||||||
|
|
||||||
items.push(`Type: ${queryTypeLabel?.label}`);
|
items.push(`Type: ${queryTypeLabel?.label}`);
|
||||||
|
|
||||||
if (isLogQuery) {
|
if (isLogQuery && direction) {
|
||||||
items.push(`Line limit: ${query.maxLines ?? maxLines}`);
|
items.push(`Line limit: ${query.maxLines ?? maxLines}`);
|
||||||
items.push(`Direction: ${getQueryDirectionLabel(direction)}`);
|
items.push(`Direction: ${getQueryDirectionLabel(direction)}`);
|
||||||
} else {
|
} else {
|
||||||
@ -243,4 +280,20 @@ function getCollapsedInfo(
|
|||||||
return items;
|
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';
|
LokiQueryBuilderOptions.displayName = 'LokiQueryBuilderOptions';
|
||||||
|
@ -13,7 +13,9 @@ import {
|
|||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
EventBusSrv,
|
EventBusSrv,
|
||||||
DataFrameType,
|
DataFrameType,
|
||||||
|
LogSortOrderChangeEvent,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import { getAppEvents } from '@grafana/runtime';
|
||||||
import * as grafanaUI from '@grafana/ui';
|
import * as grafanaUI from '@grafana/ui';
|
||||||
import * as styles from 'app/features/logs/components/getLogRowStyles';
|
import * as styles from 'app/features/logs/components/getLogRowStyles';
|
||||||
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
|
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);
|
const getDataSourceSrvMock = jest.fn().mockReturnValue(datasourceSrv);
|
||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getAppEvents: jest.fn(),
|
||||||
getDataSourceSrv: () => getDataSourceSrvMock(),
|
getDataSourceSrv: () => getDataSourceSrvMock(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -69,7 +72,35 @@ const defaultProps = {
|
|||||||
scopedVars: {},
|
scopedVars: {},
|
||||||
startTime: 1,
|
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,
|
state: LoadingState.Done,
|
||||||
timeRange: getDefaultTimeRange(),
|
timeRange: getDefaultTimeRange(),
|
||||||
},
|
},
|
||||||
@ -103,7 +134,32 @@ const defaultProps = {
|
|||||||
onChangeTimeRange: jest.fn(),
|
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', () => {
|
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', () => {
|
describe('when returned series include common labels', () => {
|
||||||
const seriesWithCommonLabels = [
|
const seriesWithCommonLabels = [
|
||||||
createDataFrame({
|
createDataFrame({
|
||||||
|
@ -27,9 +27,10 @@ import {
|
|||||||
TimeZone,
|
TimeZone,
|
||||||
toUtc,
|
toUtc,
|
||||||
urlUtil,
|
urlUtil,
|
||||||
|
LogSortOrderChangeEvent,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { convertRawToRange } from '@grafana/data/src/datetime/rangeutil';
|
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 { ScrollContainer, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
import { InfiniteScroll } from 'app/features/logs/components/InfiniteScroll';
|
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
|
// Prevents the scroll position to change when new data from infinite scrolling is received
|
||||||
const keepScrollPositionRef = useRef(false);
|
const keepScrollPositionRef = useRef(false);
|
||||||
let closeCallback = useRef<() => void>();
|
let closeCallback = useRef<() => void>();
|
||||||
|
|
||||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAppEvents().publish(
|
||||||
|
new LogSortOrderChangeEvent({
|
||||||
|
order: sortOrder,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [sortOrder]);
|
||||||
|
|
||||||
const onLogRowHover = useCallback(
|
const onLogRowHover = useCallback(
|
||||||
(row?: LogRowModel) => {
|
(row?: LogRowModel) => {
|
||||||
if (row) {
|
if (row) {
|
||||||
|
Loading…
Reference in New Issue
Block a user