mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Logs panel: Enable displayedFields in dashboards and apps (#91810)
* LogsPanelCfg: add displayedFields * LogsPanel: expose displayedFields * Chore: add docs * LogsPanel: add callbacks to external API * LogsPanel: expose field callbacks and add default implementation * chore: add unit test * chore: unfocus test * LogsPanel: add docs for new props * Enable by default
This commit is contained in:
parent
0a2db346ab
commit
699ff406c3
@ -14,6 +14,7 @@ export const pluginVersion = "11.2.0-pre";
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
dedupStrategy: common.LogsDedupStrategy;
|
dedupStrategy: common.LogsDedupStrategy;
|
||||||
|
displayedFields?: Array<string>;
|
||||||
enableLogDetails: boolean;
|
enableLogDetails: boolean;
|
||||||
isFilterLabelActive?: unknown;
|
isFilterLabelActive?: unknown;
|
||||||
/**
|
/**
|
||||||
@ -23,6 +24,8 @@ export interface Options {
|
|||||||
onClickFilterOutLabel?: unknown;
|
onClickFilterOutLabel?: unknown;
|
||||||
onClickFilterOutString?: unknown;
|
onClickFilterOutString?: unknown;
|
||||||
onClickFilterString?: unknown;
|
onClickFilterString?: unknown;
|
||||||
|
onClickHideField?: unknown;
|
||||||
|
onClickShowField?: unknown;
|
||||||
prettifyLogMessage: boolean;
|
prettifyLogMessage: boolean;
|
||||||
showCommonLabels: boolean;
|
showCommonLabels: boolean;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
@ -31,3 +34,7 @@ export interface Options {
|
|||||||
sortOrder: common.LogsSortOrder;
|
sortOrder: common.LogsSortOrder;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: Partial<Options> = {
|
||||||
|
displayedFields: [],
|
||||||
|
};
|
||||||
|
@ -426,6 +426,122 @@ describe('LogsPanel', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Show/hide fields', () => {
|
||||||
|
const series = [
|
||||||
|
createDataFrame({
|
||||||
|
refId: 'A',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: FieldType.time,
|
||||||
|
values: ['2019-04-26T09:28:11.352440161Z'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'message',
|
||||||
|
type: FieldType.string,
|
||||||
|
values: ['logline text'],
|
||||||
|
labels: {
|
||||||
|
app: 'common_app',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('displays the provided fields instead of the log line', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
showLabels: false,
|
||||||
|
showTime: false,
|
||||||
|
wrapLogMessage: false,
|
||||||
|
showCommonLabels: false,
|
||||||
|
prettifyLogMessage: false,
|
||||||
|
sortOrder: LogsSortOrder.Descending,
|
||||||
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
|
enableLogDetails: true,
|
||||||
|
displayedFields: ['app'],
|
||||||
|
onClickHideField: undefined,
|
||||||
|
onClickShowField: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('logline text')).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('app=common_app'));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText('Hide this field')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByLabelText('Hide this field'));
|
||||||
|
|
||||||
|
expect(screen.getByText('logline text')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables the behavior with a default implementation', async () => {
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
showLabels: false,
|
||||||
|
showTime: false,
|
||||||
|
wrapLogMessage: false,
|
||||||
|
showCommonLabels: false,
|
||||||
|
prettifyLogMessage: false,
|
||||||
|
sortOrder: LogsSortOrder.Descending,
|
||||||
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
|
enableLogDetails: true,
|
||||||
|
displayedFields: [],
|
||||||
|
onClickHideField: undefined,
|
||||||
|
onClickShowField: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('logline text'));
|
||||||
|
await userEvent.click(screen.getByLabelText('Show this field instead of the message'));
|
||||||
|
|
||||||
|
expect(screen.getByText('app=common_app')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByLabelText('Hide this field'));
|
||||||
|
|
||||||
|
expect(screen.getByText('logline text')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overrides the default implementation when the callbacks are provided', async () => {
|
||||||
|
const onClickShowFieldMock = jest.fn();
|
||||||
|
|
||||||
|
setup({
|
||||||
|
data: {
|
||||||
|
series,
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
showLabels: false,
|
||||||
|
showTime: false,
|
||||||
|
wrapLogMessage: false,
|
||||||
|
showCommonLabels: false,
|
||||||
|
prettifyLogMessage: false,
|
||||||
|
sortOrder: LogsSortOrder.Descending,
|
||||||
|
dedupStrategy: LogsDedupStrategy.none,
|
||||||
|
enableLogDetails: true,
|
||||||
|
onClickHideField: jest.fn(),
|
||||||
|
onClickShowField: onClickShowFieldMock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await screen.findByRole('row')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByText('logline text'));
|
||||||
|
await userEvent.click(screen.getByLabelText('Show this field instead of the message'));
|
||||||
|
|
||||||
|
expect(onClickShowFieldMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const setup = (propsOverrides?: {}) => {
|
const setup = (propsOverrides?: {}) => {
|
||||||
|
@ -36,6 +36,8 @@ import {
|
|||||||
isOnClickFilterOutLabel,
|
isOnClickFilterOutLabel,
|
||||||
isOnClickFilterOutString,
|
isOnClickFilterOutString,
|
||||||
isOnClickFilterString,
|
isOnClickFilterString,
|
||||||
|
isOnClickHideField,
|
||||||
|
isOnClickShowField,
|
||||||
Options,
|
Options,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
|
||||||
@ -56,6 +58,15 @@ interface LogsPanelProps extends PanelProps<Options> {
|
|||||||
*
|
*
|
||||||
* Determines if a given key => value filter is active in a given query. Used by Log details.
|
* Determines if a given key => value filter is active in a given query. Used by Log details.
|
||||||
* isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
* isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
|
*
|
||||||
|
* Array of field names to display instead of the log line. Pass a list of fields or an empty array to enable hide/show fields in Log Details.
|
||||||
|
* displayedFields?: string[]
|
||||||
|
*
|
||||||
|
* Called from the "eye" icon in Log Details to request showing the displayed field. If ommited, a default implementation is used.
|
||||||
|
* onClickShowField?: (key: string) => void;
|
||||||
|
*
|
||||||
|
* Called from the "eye" icon in Log Details to request hiding the displayed field. If ommited, a default implementation is used.
|
||||||
|
* onClickHideField?: (key: string) => void;
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
interface LogsPermalinkUrlState {
|
interface LogsPermalinkUrlState {
|
||||||
@ -85,6 +96,7 @@ export const LogsPanel = ({
|
|||||||
onClickFilterOutString,
|
onClickFilterOutString,
|
||||||
onClickFilterString,
|
onClickFilterString,
|
||||||
isFilterLabelActive,
|
isFilterLabelActive,
|
||||||
|
...options
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
}: LogsPanelProps) => {
|
}: LogsPanelProps) => {
|
||||||
@ -96,6 +108,7 @@ export const LogsPanel = ({
|
|||||||
const timeRange = data.timeRange;
|
const timeRange = data.timeRange;
|
||||||
const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets);
|
const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets);
|
||||||
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
|
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [displayedFields, setDisplayedFields] = useState<string[]>(options.displayedFields ?? []);
|
||||||
let closeCallback = useRef<() => void>();
|
let closeCallback = useRef<() => void>();
|
||||||
|
|
||||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||||
@ -272,6 +285,26 @@ export const LogsPanel = ({
|
|||||||
[onAddAdHocFilter]
|
[onAddAdHocFilter]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const showField = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
const index = displayedFields?.indexOf(key);
|
||||||
|
if (index === -1) {
|
||||||
|
setDisplayedFields(displayedFields?.concat(key));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[displayedFields]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideField = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
const index = displayedFields?.indexOf(key);
|
||||||
|
if (index !== undefined && index > -1) {
|
||||||
|
setDisplayedFields(displayedFields?.filter((k) => key !== k));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[displayedFields]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data || logRows.length === 0) {
|
if (!data || logRows.length === 0) {
|
||||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||||
}
|
}
|
||||||
@ -290,6 +323,9 @@ export const LogsPanel = ({
|
|||||||
const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined;
|
const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined;
|
||||||
const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined;
|
const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined;
|
||||||
|
|
||||||
|
const onClickShowField = isOnClickShowField(options.onClickShowField) ? options.onClickShowField : showField;
|
||||||
|
const onClickHideField = isOnClickHideField(options.onClickHideField) ? options.onClickHideField : hideField;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contextRow && (
|
{contextRow && (
|
||||||
@ -342,6 +378,9 @@ export const LogsPanel = ({
|
|||||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||||
}
|
}
|
||||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||||
|
displayedFields={displayedFields}
|
||||||
|
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||||
|
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||||
/>
|
/>
|
||||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,6 +41,9 @@ composableKinds: PanelCfg: {
|
|||||||
isFilterLabelActive?: _
|
isFilterLabelActive?: _
|
||||||
onClickFilterString?: _
|
onClickFilterString?: _
|
||||||
onClickFilterOutString?: _
|
onClickFilterOutString?: _
|
||||||
|
onClickShowField?: _
|
||||||
|
onClickHideField?: _
|
||||||
|
displayedFields?: [...string]
|
||||||
} @cuetsy(kind="interface")
|
} @cuetsy(kind="interface")
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
@ -12,6 +12,7 @@ import * as common from '@grafana/schema';
|
|||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
dedupStrategy: common.LogsDedupStrategy;
|
dedupStrategy: common.LogsDedupStrategy;
|
||||||
|
displayedFields?: Array<string>;
|
||||||
enableLogDetails: boolean;
|
enableLogDetails: boolean;
|
||||||
isFilterLabelActive?: unknown;
|
isFilterLabelActive?: unknown;
|
||||||
/**
|
/**
|
||||||
@ -21,6 +22,8 @@ export interface Options {
|
|||||||
onClickFilterOutLabel?: unknown;
|
onClickFilterOutLabel?: unknown;
|
||||||
onClickFilterOutString?: unknown;
|
onClickFilterOutString?: unknown;
|
||||||
onClickFilterString?: unknown;
|
onClickFilterString?: unknown;
|
||||||
|
onClickHideField?: unknown;
|
||||||
|
onClickShowField?: unknown;
|
||||||
prettifyLogMessage: boolean;
|
prettifyLogMessage: boolean;
|
||||||
showCommonLabels: boolean;
|
showCommonLabels: boolean;
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
@ -29,3 +32,7 @@ export interface Options {
|
|||||||
sortOrder: common.LogsSortOrder;
|
sortOrder: common.LogsSortOrder;
|
||||||
wrapLogMessage: boolean;
|
wrapLogMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultOptions: Partial<Options> = {
|
||||||
|
displayedFields: [],
|
||||||
|
};
|
||||||
|
@ -7,6 +7,8 @@ type onClickFilterOutLabelType = (key: string, value: string, frame?: DataFrame)
|
|||||||
type onClickFilterValueType = (value: string, refId?: string) => void;
|
type onClickFilterValueType = (value: string, refId?: string) => void;
|
||||||
type onClickFilterOutStringType = (value: string, refId?: string) => void;
|
type onClickFilterOutStringType = (value: string, refId?: string) => void;
|
||||||
type isFilterLabelActiveType = (key: string, value: string, refId?: string) => Promise<boolean>;
|
type isFilterLabelActiveType = (key: string, value: string, refId?: string) => Promise<boolean>;
|
||||||
|
type isOnClickShowFieldType = (value: string) => void;
|
||||||
|
type isOnClickHideFieldType = (value: string) => void;
|
||||||
|
|
||||||
export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType {
|
export function isOnClickFilterLabel(callback: unknown): callback is onClickFilterLabelType {
|
||||||
return typeof callback === 'function';
|
return typeof callback === 'function';
|
||||||
@ -27,3 +29,11 @@ export function isOnClickFilterOutString(callback: unknown): callback is onClick
|
|||||||
export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType {
|
export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType {
|
||||||
return typeof callback === 'function';
|
return typeof callback === 'function';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOnClickShowField(callback: unknown): callback is isOnClickShowFieldType {
|
||||||
|
return typeof callback === 'function';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOnClickHideField(callback: unknown): callback is isOnClickHideFieldType {
|
||||||
|
return typeof callback === 'function';
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user