Loki: Update log context UI (#66730)

* fix logrowcontext scrolling behavior

* Loki: Update loki context ui menu

* Update

* Add test, update

* Use escapeLabelValueInSelector when displaying labels

* Update test for new appliedContextFilters

---------

Co-authored-by: Sven Grossmann <svennergr@gmail.com>
This commit is contained in:
Ivana Huckova
2023-04-18 15:59:22 +02:00
committed by GitHub
parent 0741f47876
commit f612a72f96
9 changed files with 226 additions and 152 deletions

View File

@@ -141,7 +141,7 @@ export interface DataSourceWithLogsContextSupport<TQuery extends DataQuery = Dat
* @alpha * @alpha
* @internal * @internal
*/ */
getLogRowContextUi?(row: LogRowModel, runContextQuery?: () => void): React.ReactNode; getLogRowContextUi?(row: LogRowModel, runContextQuery?: () => void, origQuery?: TQuery): React.ReactNode;
} }
export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => { export const hasLogsContextSupport = (datasource: unknown): datasource is DataSourceWithLogsContextSupport => {

View File

@@ -25,7 +25,6 @@ import {
DataHoverEvent, DataHoverEvent,
DataHoverClearEvent, DataHoverClearEvent,
EventBus, EventBus,
DataSourceWithLogsContextSupport,
LogRowContextOptions, LogRowContextOptions,
} from '@grafana/data'; } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
@@ -80,7 +79,7 @@ interface Props extends Themeable2 {
onStartScanning?: () => void; onStartScanning?: () => void;
onStopScanning?: () => void; onStopScanning?: () => void;
getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<any>; getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<any>;
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode;
getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>; getFieldLinks: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
addResultsToCache: () => void; addResultsToCache: () => void;
clearCache: () => void; clearCache: () => void;

View File

@@ -65,10 +65,13 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
}; };
getLogRowContextUi = (row: LogRowModel, runContextQuery?: () => void): React.ReactNode => { getLogRowContextUi = (row: LogRowModel, runContextQuery?: () => void): React.ReactNode => {
const { datasourceInstance } = this.props; const { datasourceInstance, logsQueries } = this.props;
if (hasLogsContextUiSupport(datasourceInstance) && datasourceInstance.getLogRowContextUi) { if (hasLogsContextUiSupport(datasourceInstance) && datasourceInstance.getLogRowContextUi) {
return datasourceInstance.getLogRowContextUi(row, runContextQuery); const query = (logsQueries ?? []).find(
(q) => q.refId === row.dataFrame.refId && q.datasource != null && q.datasource.type === datasourceInstance.type
);
return datasourceInstance.getLogRowContextUi(row, runContextQuery, query);
} }
return <></>; return <></>;

View File

@@ -10,7 +10,6 @@ import {
LogsSortOrder, LogsSortOrder,
CoreApp, CoreApp,
DataFrame, DataFrame,
DataSourceWithLogsContextSupport,
DataQueryResponse, DataQueryResponse,
LogRowContextOptions, LogRowContextOptions,
} from '@grafana/data'; } from '@grafana/data';
@@ -43,7 +42,7 @@ export interface Props extends Themeable2 {
onClickFilterLabel?: (key: string, value: string) => void; onClickFilterLabel?: (key: string, value: string) => void;
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>; getRowContext?: (row: LogRowModel, options?: LogRowContextOptions) => Promise<DataQueryResponse>;
getLogRowContextUi?: DataSourceWithLogsContextSupport['getLogRowContextUi']; getLogRowContextUi?: (row: LogRowModel, runContextQuery?: () => void) => React.ReactNode;
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>; getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
onClickShowField?: (key: string) => void; onClickShowField?: (key: string) => void;
onClickHideField?: (key: string) => void; onClickHideField?: (key: string) => void;

View File

@@ -42,7 +42,7 @@ describe('LogContextProvider', () => {
beforeEach(() => { beforeEach(() => {
logContextProvider = new LogContextProvider(defaultDatasourceMock); logContextProvider = new LogContextProvider(defaultDatasourceMock);
logContextProvider.getInitContextFiltersFromLabels = jest.fn(() => logContextProvider.getInitContextFiltersFromLabels = jest.fn(() =>
Promise.resolve([{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }]) Promise.resolve([{ value: 'baz', enabled: true, fromParser: false, label: 'bar' }])
); );
}); });
@@ -59,8 +59,8 @@ describe('LogContextProvider', () => {
it('should not call getInitContextFilters if appliedContextFilters', async () => { it('should not call getInitContextFilters if appliedContextFilters', async () => {
logContextProvider.appliedContextFilters = [ logContextProvider.appliedContextFilters = [
{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }, { value: 'baz', enabled: true, fromParser: false, label: 'bar' },
{ value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, { value: 'abc', enabled: true, fromParser: false, label: 'xyz' },
]; ];
await logContextProvider.getLogRowContext(defaultLogRow, { await logContextProvider.getLogRowContext(defaultLogRow, {
limit: 10, limit: 10,
@@ -89,9 +89,9 @@ describe('LogContextProvider', () => {
it('should not apply parsed labels', async () => { it('should not apply parsed labels', async () => {
logContextProvider.appliedContextFilters = [ logContextProvider.appliedContextFilters = [
{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }, { value: 'baz', enabled: true, fromParser: false, label: 'bar' },
{ value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, { value: 'abc', enabled: true, fromParser: false, label: 'xyz' },
{ value: 'foo', enabled: true, fromParser: true, label: 'foo' }, { value: 'uniqueParsedLabel', enabled: true, fromParser: true, label: 'foo' },
]; ];
const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget(
defaultLogRow, defaultLogRow,
@@ -107,8 +107,8 @@ describe('LogContextProvider', () => {
describe('query with parser', () => { describe('query with parser', () => {
it('should apply parser', async () => { it('should apply parser', async () => {
logContextProvider.appliedContextFilters = [ logContextProvider.appliedContextFilters = [
{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }, { value: 'baz', enabled: true, fromParser: false, label: 'bar' },
{ value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, { value: 'abc', enabled: true, fromParser: false, label: 'xyz' },
]; ];
const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget(
defaultLogRow, defaultLogRow,
@@ -124,9 +124,9 @@ describe('LogContextProvider', () => {
it('should apply parser and parsed labels', async () => { it('should apply parser and parsed labels', async () => {
logContextProvider.appliedContextFilters = [ logContextProvider.appliedContextFilters = [
{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }, { value: 'baz', enabled: true, fromParser: false, label: 'bar' },
{ value: 'xyz', enabled: true, fromParser: false, label: 'xyz' }, { value: 'abc', enabled: true, fromParser: false, label: 'xyz' },
{ value: 'foo', enabled: true, fromParser: true, label: 'foo' }, { value: 'uniqueParsedLabel', enabled: true, fromParser: true, label: 'foo' },
]; ];
const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget(
defaultLogRow, defaultLogRow,
@@ -143,8 +143,8 @@ describe('LogContextProvider', () => {
it('should not apply parser and parsed labels if more parsers in original query', async () => { it('should not apply parser and parsed labels if more parsers in original query', async () => {
logContextProvider.appliedContextFilters = [ logContextProvider.appliedContextFilters = [
{ value: 'bar', enabled: true, fromParser: false, label: 'bar' }, { value: 'baz', enabled: true, fromParser: false, label: 'bar' },
{ value: 'foo', enabled: true, fromParser: true, label: 'foo' }, { value: 'uniqueParsedLabel', enabled: true, fromParser: true, label: 'foo' },
]; ];
const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget( const contextQuery = await logContextProvider.prepareLogRowContextQueryTarget(
defaultLogRow, defaultLogRow,

View File

@@ -137,7 +137,7 @@ export class LogContextProvider {
}; };
} }
getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { getLogRowContextUi(row: LogRowModel, runContextQuery?: () => void, originalQuery?: DataQuery): React.ReactNode {
const updateFilter = (contextFilters: ContextFilter[]) => { const updateFilter = (contextFilters: ContextFilter[]) => {
this.appliedContextFilters = contextFilters; this.appliedContextFilters = contextFilters;
@@ -153,8 +153,15 @@ export class LogContextProvider {
this.appliedContextFilters = []; this.appliedContextFilters = [];
}); });
let origQuery: LokiQuery | undefined = undefined;
// Type guard for LokiQuery
if (originalQuery && isLokiQuery(originalQuery)) {
origQuery = originalQuery;
}
return LokiContextUi({ return LokiContextUi({
row, row,
origQuery,
updateFilter, updateFilter,
onClose: this.onContextClose, onClose: this.onContextClose,
logContextProvider: this, logContextProvider: this,
@@ -164,10 +171,9 @@ export class LogContextProvider {
processContextFiltersToExpr = (row: LogRowModel, contextFilters: ContextFilter[], query: LokiQuery | undefined) => { processContextFiltersToExpr = (row: LogRowModel, contextFilters: ContextFilter[], query: LokiQuery | undefined) => {
const labelFilters = contextFilters const labelFilters = contextFilters
.map((filter) => { .map((filter) => {
const label = filter.value;
if (!filter.fromParser && filter.enabled) { if (!filter.fromParser && filter.enabled) {
// escape backslashes in label as users can't escape them by themselves // escape backslashes in label as users can't escape them by themselves
return `${label}="${escapeLabelValueInExactSelector(row.labels[label])}"`; return `${filter.label}="${escapeLabelValueInExactSelector(filter.value)}"`;
} }
return ''; return '';
}) })
@@ -186,7 +192,7 @@ export class LogContextProvider {
const parsedLabels = contextFilters.filter((filter) => filter.fromParser && filter.enabled); const parsedLabels = contextFilters.filter((filter) => filter.fromParser && filter.enabled);
for (const parsedLabel of parsedLabels) { for (const parsedLabel of parsedLabels) {
if (parsedLabel.enabled) { if (parsedLabel.enabled) {
expr = addLabelToQuery(expr, parsedLabel.label, '=', row.labels[parsedLabel.label]); expr = addLabelToQuery(expr, parsedLabel.label, '=', parsedLabel.value);
} }
} }
} }
@@ -202,10 +208,9 @@ export class LogContextProvider {
Object.entries(labels).forEach(([label, value]) => { Object.entries(labels).forEach(([label, value]) => {
const filter: ContextFilter = { const filter: ContextFilter = {
label, label,
value: label, // this looks weird in the first place, but we need to set the label as value here value: value,
enabled: allLabels.includes(label), enabled: allLabels.includes(label),
fromParser: !allLabels.includes(label), fromParser: !allLabels.includes(label),
description: value,
}; };
contextFilters.push(filter); contextFilters.push(filter);
}); });

View File

@@ -1,10 +1,12 @@
import { act, render, screen, waitFor } from '@testing-library/react'; import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react'; import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { LogRowModel } from '@grafana/data'; import { LogRowModel } from '@grafana/data';
import { LogContextProvider } from '../LogContextProvider'; import { LogContextProvider } from '../LogContextProvider';
import { ContextFilter, LokiQuery } from '../types';
import { LokiContextUi, LokiContextUiProps } from './LokiContextUi'; import { LokiContextUi, LokiContextUiProps } from './LokiContextUi';
@@ -14,6 +16,49 @@ jest.mock('@grafana/runtime', () => ({
reportInteraction: () => null, reportInteraction: () => null,
})); }));
jest.mock('app/core/store', () => {
return {
set() {},
getBool() {
return true;
},
};
});
const setupProps = (): LokiContextUiProps => {
const defaults: LokiContextUiProps = {
logContextProvider: mockLogContextProvider as unknown as LogContextProvider,
updateFilter: jest.fn(),
row: {
entry: 'WARN test 1.23 on [xxx]',
labels: {
label1: 'value1',
label3: 'value3',
},
} as unknown as LogRowModel,
onClose: jest.fn(),
};
return defaults;
};
const mockLogContextProvider = {
getInitContextFiltersFromLabels: jest.fn().mockImplementation(() =>
Promise.resolve([
{ value: 'value1', enabled: true, fromParser: false, label: 'label1' },
{ value: 'value3', enabled: false, fromParser: true, label: 'label3' },
])
),
processContextFiltersToExpr: jest.fn().mockImplementation(
(row: LogRowModel, contextFilters: ContextFilter[], query: LokiQuery | undefined) =>
`{${contextFilters
.filter((filter) => filter.enabled)
.map((filter) => `${filter.label}="${filter.value}"`)
.join('` ')}}`
),
getLogRowContext: jest.fn(),
};
describe('LokiContextUi', () => { describe('LokiContextUi', () => {
const savedGlobal = global; const savedGlobal = global;
beforeAll(() => { beforeAll(() => {
@@ -29,38 +74,11 @@ describe('LokiContextUi', () => {
afterAll(() => { afterAll(() => {
global = savedGlobal; global = savedGlobal;
}); });
const setupProps = (): LokiContextUiProps => {
const mockLogContextProvider = {
getInitContextFiltersFromLabels: jest.fn().mockImplementation(() =>
Promise.resolve([
{ value: 'label1', enabled: true, fromParser: false, label: 'label1' },
{ value: 'label3', enabled: false, fromParser: true, label: 'label3' },
])
),
};
const defaults: LokiContextUiProps = { it('renders and shows executed query text', async () => {
logContextProvider: mockLogContextProvider as unknown as LogContextProvider,
updateFilter: jest.fn(),
row: {
entry: 'WARN test 1.23 on [xxx]',
labels: {
label1: 'value1',
label3: 'value3',
},
} as unknown as LogRowModel,
onClose: jest.fn(),
};
return defaults;
};
it('renders and shows basic text', async () => {
const props = setupProps(); const props = setupProps();
render(<LokiContextUi {...props} />); render(<LokiContextUi {...props} />);
expect(await screen.findByText(/Executed log context query:/)).toBeInTheDocument();
// Initial set of labels is available and not selected
expect(await screen.findByText(/Select labels to be included in the context query/)).toBeInTheDocument();
}); });
it('initialize context filters', async () => { it('initialize context filters', async () => {
@@ -79,7 +97,7 @@ describe('LokiContextUi', () => {
expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled();
}); });
const select = await screen.findAllByRole('combobox'); const select = await screen.findAllByRole('combobox');
await selectOptionInTest(select[0], 'label1'); await selectOptionInTest(select[0], 'label1="value1"');
}); });
it('finds label3 as a parsed label', async () => { it('finds label3 as a parsed label', async () => {
@@ -89,7 +107,7 @@ describe('LokiContextUi', () => {
expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled();
}); });
const select = await screen.findAllByRole('combobox'); const select = await screen.findAllByRole('combobox');
await selectOptionInTest(select[1], 'label3'); await selectOptionInTest(select[1], 'label3="value3"');
}); });
it('calls updateFilter when selecting a label', async () => { it('calls updateFilter when selecting a label', async () => {
@@ -100,7 +118,7 @@ describe('LokiContextUi', () => {
expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled(); expect(props.logContextProvider.getInitContextFiltersFromLabels).toHaveBeenCalled();
expect(screen.getAllByRole('combobox')).toHaveLength(2); expect(screen.getAllByRole('combobox')).toHaveLength(2);
}); });
await selectOptionInTest(screen.getAllByRole('combobox')[1], 'label3'); await selectOptionInTest(screen.getAllByRole('combobox')[1], 'label3="value3"');
act(() => { act(() => {
jest.runAllTimers(); jest.runAllTimers();
}); });
@@ -117,4 +135,21 @@ describe('LokiContextUi', () => {
expect(props.onClose).toHaveBeenCalled(); expect(props.onClose).toHaveBeenCalled();
}); });
}); });
it('displays executed query even if context ui closed', async () => {
const props = setupProps();
render(<LokiContextUi {...props} />);
// We start with the context ui open
expect(await screen.findByText(/Executed log context query:/)).toBeInTheDocument();
// We click on it to close
await userEvent.click(screen.getByText(/Executed log context query:/));
await waitFor(() => {
// We should see the query text (it is split into multiple spans)
expect(screen.getByText('{')).toBeInTheDocument();
expect(screen.getByText('label1')).toBeInTheDocument();
expect(screen.getByText('=')).toBeInTheDocument();
expect(screen.getByText('"value1"')).toBeInTheDocument();
expect(screen.getByText('}')).toBeInTheDocument();
});
});
}); });

View File

@@ -1,40 +1,38 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import memoizeOne from 'memoize-one'; import React, { useCallback, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { GrafanaTheme2, LogRowModel, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, LogRowModel, SelectableValue } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { LoadingPlaceholder, MultiSelect, Tag, Tooltip, useStyles2 } from '@grafana/ui'; import { Collapse, Label, LoadingPlaceholder, MultiSelect, Tag, Tooltip, useStyles2 } from '@grafana/ui';
import store from 'app/core/store';
import { RawQuery } from '../../prometheus/querybuilder/shared/RawQuery';
import { LogContextProvider } from '../LogContextProvider'; import { LogContextProvider } from '../LogContextProvider';
import { ContextFilter } from '../types'; import { escapeLabelValueInSelector } from '../languageUtils';
import { lokiGrammar } from '../syntax';
import { ContextFilter, LokiQuery } from '../types';
export interface LokiContextUiProps { export interface LokiContextUiProps {
logContextProvider: LogContextProvider; logContextProvider: LogContextProvider;
row: LogRowModel; row: LogRowModel;
updateFilter: (value: ContextFilter[]) => void; updateFilter: (value: ContextFilter[]) => void;
onClose: () => void; onClose: () => void;
origQuery?: LokiQuery;
} }
function getStyles(theme: GrafanaTheme2) { function getStyles(theme: GrafanaTheme2) {
return { return {
labels: css` labels: css`
display: flex; display: flex;
gap: 2px; gap: ${theme.spacing(0.5)};
`, `,
multiSelectWrapper: css` wrapper: css`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
margin-top: ${theme.spacing(1)};
gap: ${theme.spacing(0.5)}; gap: ${theme.spacing(0.5)};
`, `,
multiSelect: css`
& .scrollbar-view {
overscroll-behavior: contain;
}
`,
loadingPlaceholder: css` loadingPlaceholder: css`
margin-bottom: 0px; margin-bottom: 0px;
float: right; float: right;
@@ -48,23 +46,37 @@ function getStyles(theme: GrafanaTheme2) {
hidden: css` hidden: css`
visibility: hidden; visibility: hidden;
`, `,
tag: css`
padding: ${theme.spacing(0.25)} ${theme.spacing(0.75)};
`,
label: css`
max-width: 100%;
margin: ${theme.spacing(2)} 0;
`,
query: css`
text-align: start;
line-break: anywhere;
margin-top: ${theme.spacing(0.5)};
`,
ui: css`
background-color: ${theme.colors.background.secondary};
padding: ${theme.spacing(2)};
`,
}; };
} }
const formatOptionLabel = memoizeOne(({ label, description }: SelectableValue<string>) => ( const IS_LOKI_LOG_CONTEXT_UI_OPEN = 'isLogContextQueryUiOpen';
<Tooltip content={`${label}="${description}"`} placement="top" interactive={true}>
<span>{label}</span>
</Tooltip>
));
export function LokiContextUi(props: LokiContextUiProps) { export function LokiContextUi(props: LokiContextUiProps) {
const { row, logContextProvider, updateFilter, onClose } = props; const { row, logContextProvider, updateFilter, onClose, origQuery } = props;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [contextFilters, setContextFilters] = useState<ContextFilter[]>([]); const [contextFilters, setContextFilters] = useState<ContextFilter[]>([]);
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(store.getBool(IS_LOKI_LOG_CONTEXT_UI_OPEN, true));
const timerHandle = React.useRef<number>(); const timerHandle = React.useRef<number>();
const previousInitialized = React.useRef<boolean>(false); const previousInitialized = React.useRef<boolean>(false);
const previousContextFilters = React.useRef<ContextFilter[]>([]); const previousContextFilters = React.useRef<ContextFilter[]>([]);
@@ -136,108 +148,129 @@ export function LokiContextUi(props: LokiContextUiProps) {
const parsedLabels = contextFilters.filter(({ fromParser }) => fromParser); const parsedLabels = contextFilters.filter(({ fromParser }) => fromParser);
const parsedLabelsEnabled = parsedLabels.filter(({ enabled }) => enabled); const parsedLabelsEnabled = parsedLabels.filter(({ enabled }) => enabled);
const contextFilterToSelectFilter = useCallback((contextFilter: ContextFilter): SelectableValue<string> => {
return {
label: `${contextFilter.label}="${escapeLabelValueInSelector(contextFilter.value)}"`,
value: contextFilter.label,
};
}, []);
return ( return (
<div className={styles.multiSelectWrapper}> <div className={styles.wrapper}>
<div className={styles.textWrapper}> <LoadingPlaceholder text="" className={`${styles.loadingPlaceholder} ${loading ? '' : styles.hidden}`} />
{' '} <Collapse
<Tooltip collapsible={true}
content={ isOpen={isOpen}
'This feature is experimental and only works on log queries containing no more than 1 parser (logfmt, json).' onToggle={() => {
} store.set(IS_LOKI_LOG_CONTEXT_UI_OPEN, !isOpen);
placement="top" setIsOpen((isOpen) => !isOpen);
> }}
<Tag label={
className={css({ <div className={styles.query}>
fontSize: 10, <Label>Executed log context query:</Label>
padding: '1px 5px', <RawQuery
verticalAlign: 'text-bottom', lang={{ grammar: lokiGrammar, name: 'loki' }}
})} query={logContextProvider.processContextFiltersToExpr(
name={'Experimental'} row,
colorIndex={1} contextFilters.filter(({ enabled }) => enabled),
/> origQuery
</Tooltip>{' '} )}
Select labels to be included in the context query: />
<LoadingPlaceholder text="" className={`${styles.loadingPlaceholder} ${loading ? '' : styles.hidden}`} /> </div>
</div> }
<div> >
<MultiSelect <div className={styles.ui}>
className={styles.multiSelect} <Tooltip
prefix="Labels" content={
options={realLabels} 'This feature is experimental and only works on log queries containing no more than 1 parser (logfmt, json).'
value={realLabelsEnabled}
formatOptionLabel={formatOptionLabel}
closeMenuOnSelect={true}
maxMenuHeight={200}
menuShouldPortal={false}
noOptionsMessage="No further labels available"
onChange={(keys, actionMeta) => {
if (actionMeta.action === 'select-option') {
reportInteraction('grafana_explore_logs_loki_log_context_filtered', {
logRowUid: row.uid,
type: 'label',
action: 'select',
});
} }
if (actionMeta.action === 'remove-value') { placement="top"
reportInteraction('grafana_explore_logs_loki_log_context_filtered', { >
logRowUid: row.uid, <Tag className={styles.tag} name={'Experimental feature'} colorIndex={1} />
type: 'label', </Tooltip>{' '}
action: 'remove', <Label
}); className={styles.label}
} description="Context query is created from all labels defining the stream for the selected log line. Select labels to be included in log context query."
return setContextFilters( >
contextFilters.map((filter) => { 1. Select labels
if (filter.fromParser) { </Label>
return filter;
}
filter.enabled = keys.some((key) => key.value === filter.value);
return filter;
})
);
}}
/>
</div>
{parsedLabels.length > 0 && (
<div>
<MultiSelect <MultiSelect
className={styles.multiSelect} options={realLabels.map(contextFilterToSelectFilter)}
prefix="Parsed Labels" value={realLabelsEnabled.map(contextFilterToSelectFilter)}
options={parsedLabels}
value={parsedLabelsEnabled}
formatOptionLabel={formatOptionLabel}
closeMenuOnSelect={true} closeMenuOnSelect={true}
menuShouldPortal={false}
maxMenuHeight={200} maxMenuHeight={200}
noOptionsMessage="No further labels available" noOptionsMessage="No further labels available"
isClearable={true}
onChange={(keys, actionMeta) => { onChange={(keys, actionMeta) => {
if (actionMeta.action === 'select-option') { if (actionMeta.action === 'select-option') {
reportInteraction('grafana_explore_logs_loki_log_context_filtered', { reportInteraction('grafana_explore_logs_loki_log_context_filtered', {
logRowUid: row.uid, logRowUid: row.uid,
type: 'parsed_label', type: 'label',
action: 'select', action: 'select',
}); });
} }
if (actionMeta.action === 'remove-value') { if (actionMeta.action === 'remove-value') {
reportInteraction('grafana_explore_logs_loki_log_context_filtered', { reportInteraction('grafana_explore_logs_loki_log_context_filtered', {
logRowUid: row.uid, logRowUid: row.uid,
type: 'parsed_label', type: 'label',
action: 'remove', action: 'remove',
}); });
} }
setContextFilters( return setContextFilters(
contextFilters.map((filter) => { contextFilters.map((filter) => {
if (!filter.fromParser) { if (filter.fromParser) {
return filter; return filter;
} }
filter.enabled = keys.some((key) => key.value === filter.value); filter.enabled = keys.some((key) => key.value === filter.label);
return filter; return filter;
}) })
); );
}} }}
/> />
{parsedLabels.length > 0 && (
<>
<Label
className={styles.label}
description={`By using parser, you are able to filter for extracted labels. Select extracted labels to be included in log context query.`}
>
2. Add extracted label filters
</Label>
<MultiSelect
options={parsedLabels.map(contextFilterToSelectFilter)}
value={parsedLabelsEnabled.map(contextFilterToSelectFilter)}
closeMenuOnSelect={true}
maxMenuHeight={200}
noOptionsMessage="No further labels available"
isClearable={true}
onChange={(keys, actionMeta) => {
if (actionMeta.action === 'select-option') {
reportInteraction('grafana_explore_logs_loki_log_context_filtered', {
logRowUid: row.uid,
type: 'parsed_label',
action: 'select',
});
}
if (actionMeta.action === 'remove-value') {
reportInteraction('grafana_explore_logs_loki_log_context_filtered', {
logRowUid: row.uid,
type: 'parsed_label',
action: 'remove',
});
}
setContextFilters(
contextFilters.map((filter) => {
if (!filter.fromParser) {
return filter;
}
filter.enabled = keys.some((key) => key.value === filter.label);
return filter;
})
);
}}
/>
</>
)}
</div> </div>
)} </Collapse>
</div> </div>
); );
} }

View File

@@ -651,8 +651,8 @@ export class LokiDatasource
return await this.logContextProvider.getLogRowContext(row, options, origQuery); return await this.logContextProvider.getLogRowContext(row, options, origQuery);
}; };
getLogRowContextUi(row: LogRowModel, runContextQuery: () => void): React.ReactNode { getLogRowContextUi(row: LogRowModel, runContextQuery: () => void, origQuery: DataQuery): React.ReactNode {
return this.logContextProvider.getLogRowContextUi(row, runContextQuery); return this.logContextProvider.getLogRowContextUi(row, runContextQuery, origQuery);
} }
testDatasource(): Promise<{ status: string; message: string }> { testDatasource(): Promise<{ status: string; message: string }> {