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 {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
displayedFields?: Array<string>;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
@ -23,6 +24,8 @@ export interface Options {
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
onClickHideField?: unknown;
|
||||
onClickShowField?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
@ -31,3 +34,7 @@ export interface Options {
|
||||
sortOrder: common.LogsSortOrder;
|
||||
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?: {}) => {
|
||||
|
@ -36,6 +36,8 @@ import {
|
||||
isOnClickFilterOutLabel,
|
||||
isOnClickFilterOutString,
|
||||
isOnClickFilterString,
|
||||
isOnClickHideField,
|
||||
isOnClickShowField,
|
||||
Options,
|
||||
} from './types';
|
||||
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.
|
||||
* 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 {
|
||||
@ -85,6 +96,7 @@ export const LogsPanel = ({
|
||||
onClickFilterOutString,
|
||||
onClickFilterString,
|
||||
isFilterLabelActive,
|
||||
...options
|
||||
},
|
||||
id,
|
||||
}: LogsPanelProps) => {
|
||||
@ -96,6 +108,7 @@ export const LogsPanel = ({
|
||||
const timeRange = data.timeRange;
|
||||
const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets);
|
||||
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
|
||||
const [displayedFields, setDisplayedFields] = useState<string[]>(options.displayedFields ?? []);
|
||||
let closeCallback = useRef<() => void>();
|
||||
|
||||
const { eventBus, onAddAdHocFilter } = usePanelContext();
|
||||
@ -272,6 +285,26 @@ export const LogsPanel = ({
|
||||
[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) {
|
||||
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
|
||||
}
|
||||
@ -290,6 +323,9 @@ export const LogsPanel = ({
|
||||
const defaultOnClickFilterLabel = onAddAdHocFilter ? handleOnClickFilterLabel : undefined;
|
||||
const defaultOnClickFilterOutLabel = onAddAdHocFilter ? handleOnClickFilterOutLabel : undefined;
|
||||
|
||||
const onClickShowField = isOnClickShowField(options.onClickShowField) ? options.onClickShowField : showField;
|
||||
const onClickHideField = isOnClickHideField(options.onClickHideField) ? options.onClickHideField : hideField;
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextRow && (
|
||||
@ -342,6 +378,9 @@ export const LogsPanel = ({
|
||||
isOnClickFilterOutString(onClickFilterOutString) ? onClickFilterOutString : undefined
|
||||
}
|
||||
isFilterLabelActive={isIsFilterLabelActive(isFilterLabelActive) ? isFilterLabelActive : undefined}
|
||||
displayedFields={displayedFields}
|
||||
onClickShowField={displayedFields !== undefined ? onClickShowField : undefined}
|
||||
onClickHideField={displayedFields !== undefined ? onClickHideField : undefined}
|
||||
/>
|
||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||
</div>
|
||||
|
@ -41,6 +41,9 @@ composableKinds: PanelCfg: {
|
||||
isFilterLabelActive?: _
|
||||
onClickFilterString?: _
|
||||
onClickFilterOutString?: _
|
||||
onClickShowField?: _
|
||||
onClickHideField?: _
|
||||
displayedFields?: [...string]
|
||||
} @cuetsy(kind="interface")
|
||||
}
|
||||
}]
|
||||
|
@ -12,6 +12,7 @@ import * as common from '@grafana/schema';
|
||||
|
||||
export interface Options {
|
||||
dedupStrategy: common.LogsDedupStrategy;
|
||||
displayedFields?: Array<string>;
|
||||
enableLogDetails: boolean;
|
||||
isFilterLabelActive?: unknown;
|
||||
/**
|
||||
@ -21,6 +22,8 @@ export interface Options {
|
||||
onClickFilterOutLabel?: unknown;
|
||||
onClickFilterOutString?: unknown;
|
||||
onClickFilterString?: unknown;
|
||||
onClickHideField?: unknown;
|
||||
onClickShowField?: unknown;
|
||||
prettifyLogMessage: boolean;
|
||||
showCommonLabels: boolean;
|
||||
showLabels: boolean;
|
||||
@ -29,3 +32,7 @@ export interface Options {
|
||||
sortOrder: common.LogsSortOrder;
|
||||
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 onClickFilterOutStringType = (value: string, refId?: string) => void;
|
||||
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 {
|
||||
return typeof callback === 'function';
|
||||
@ -27,3 +29,11 @@ export function isOnClickFilterOutString(callback: unknown): callback is onClick
|
||||
export function isIsFilterLabelActive(callback: unknown): callback is isFilterLabelActiveType {
|
||||
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