Logs Panel: Table UI - Multiple dataframes (queries) (#77589)

* Add dropdown to logs table UI to allow users to select which dataFrame to visualize in the table
pull/77757/head
Galen Kistler 2 years ago committed by GitHub
parent a01f8c5b42
commit 1dec96ebe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-data/src/types/explore.ts
  2. 1
      public/app/features/explore/Logs/Logs.tsx
  3. 4
      public/app/features/explore/Logs/LogsTable.test.tsx
  4. 19
      public/app/features/explore/Logs/LogsTable.tsx
  5. 101
      public/app/features/explore/Logs/LogsTableWrap.tsx

@ -51,6 +51,8 @@ export interface ExploreLogsPanelState {
id?: string;
columns?: Record<number, string>;
visualisationType?: 'table' | 'logs';
// Used for logs table visualisation, contains the refId of the dataFrame that is currently visualized
refId?: string;
}
export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {

@ -183,6 +183,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
...state.panelsState.logs,
columns: logsPanelState.columns ?? this.props.panelState?.logs?.columns,
visualisationType: logsPanelState.visualisationType ?? this.state.visualisationType,
refId: logsPanelState.refId ?? this.props.panelState?.logs?.refId,
})
);
}

@ -65,7 +65,7 @@ const getComponent = (partialProps?: Partial<ComponentProps<typeof LogsTable>>,
to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' },
}}
logsFrames={[logs ?? testDataFrame]}
dataFrame={logs ?? testDataFrame}
{...partialProps}
/>
);
@ -121,7 +121,7 @@ describe('LogsTable', () => {
it('should render extracted labels as columns (elastic)', async () => {
setup({
logsFrames: [getMockElasticFrame()],
dataFrame: getMockElasticFrame(),
columnsWithMeta: {
counter: { active: true, percentOfLinesWithLabel: 3 },
level: { active: true, percentOfLinesWithLabel: 3 },

@ -26,7 +26,7 @@ import { getFieldLinksForExplore } from '../utils/links';
import { fieldNameMeta } from './LogsTableWrap';
interface Props {
logsFrames: DataFrame[];
dataFrame: DataFrame;
width: number;
timeZone: string;
splitOpen: SplitOpen;
@ -39,12 +39,9 @@ interface Props {
}
export function LogsTable(props: Props) {
const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames, columnsWithMeta } = props;
const { timeZone, splitOpen, range, logsSortOrder, width, dataFrame, columnsWithMeta } = props;
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
// Only a single frame (query) is supported currently
const logFrameRaw = logsFrames ? logsFrames[0] : undefined;
const prepareTableFrame = useCallback(
(frame: DataFrame): DataFrame => {
if (!frame.length) {
@ -99,15 +96,13 @@ export function LogsTable(props: Props) {
useEffect(() => {
const prepare = async () => {
// Parse the dataframe to a logFrame
const logsFrame = logFrameRaw ? parseLogsFrame(logFrameRaw) : undefined;
const logsFrame = dataFrame ? parseLogsFrame(dataFrame) : undefined;
if (!logFrameRaw || !logsFrame) {
if (!logsFrame) {
setTableFrame(undefined);
return;
}
let dataFrame = logFrameRaw;
// create extract JSON transformation for every field that is `json.RawMessage`
const transformations: Array<DataTransformerConfig | CustomTransformOperator> = extractFields(dataFrame);
@ -139,7 +134,7 @@ export function LogsTable(props: Props) {
}
};
prepare();
}, [columnsWithMeta, logFrameRaw, logsSortOrder, prepareTableFrame]);
}, [columnsWithMeta, dataFrame, logsSortOrder, prepareTableFrame]);
if (!tableFrame) {
return null;
@ -152,11 +147,11 @@ export function LogsTable(props: Props) {
return;
}
if (operator === FILTER_FOR_OPERATOR) {
onClickFilterLabel(key, value);
onClickFilterLabel(key, value, dataFrame.refId);
}
if (operator === FILTER_OUT_OPERATOR) {
onClickFilterOutLabel(key, value);
onClickFilterOutLabel(key, value, dataFrame.refId);
}
};

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { debounce } from 'lodash';
import React, { useState, useEffect, useCallback } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
DataFrame,
@ -8,11 +8,12 @@ import {
GrafanaTheme2,
Labels,
LogsSortOrder,
SelectableValue,
SplitOpen,
TimeRange,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime/src';
import { Themeable2 } from '@grafana/ui/';
import { InlineField, Select, Themeable2 } from '@grafana/ui/';
import { parseLogsFrame } from '../../logs/logsFrame';
@ -44,6 +45,7 @@ type fieldNameMetaStore = Record<fieldName, fieldNameMeta>;
export function LogsTableWrap(props: Props) {
const { logsFrames } = props;
// Save the normalized cardinality of each label
const [columnsWithMeta, setColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
@ -51,7 +53,13 @@ export function LogsTableWrap(props: Props) {
const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
const height = getTableHeight();
const dataFrame = logsFrames[0];
// The current dataFrame containing the refId of the current query
const [currentDataFrame, setCurrentDataFrame] = useState<DataFrame>(
logsFrames.find((f) => f.refId === props?.panelState?.refId) ?? logsFrames[0]
);
// The refId of the current frame being displayed
const currentFrameRefId = currentDataFrame.refId;
const getColumnsFromProps = useCallback(
(fieldNames: fieldNameMetaStore) => {
@ -100,11 +108,11 @@ export function LogsTableWrap(props: Props) {
*/
useEffect(() => {
// If the data frame is empty, there's nothing to viz, it could mean the user has unselected all columns
if (!dataFrame.length) {
if (!currentDataFrame.length) {
return;
}
const numberOfLogLines = dataFrame ? dataFrame.length : 0;
const logsFrame = parseLogsFrame(dataFrame);
const numberOfLogLines = currentDataFrame ? currentDataFrame.length : 0;
const logsFrame = parseLogsFrame(currentDataFrame);
const labels = logsFrame?.getLogFrameLabelsAsLabels();
const otherFields = [];
@ -197,7 +205,7 @@ export function LogsTableWrap(props: Props) {
setColumnsWithMeta(pendingLabelState);
// The panel state is updated when the user interacts with the multi-select sidebar
}, [dataFrame, getColumnsFromProps]);
}, [currentDataFrame, getColumnsFromProps]);
if (!columnsWithMeta) {
return null;
@ -259,6 +267,7 @@ export function LogsTableWrap(props: Props) {
// Only include active filters
.filter((key) => pendingLabelState[key]?.active)
),
refId: currentDataFrame.refId,
visualisationType: 'table',
};
@ -300,34 +309,69 @@ export function LogsTableWrap(props: Props) {
}
};
const onFrameSelectorChange = (value: SelectableValue<string>) => {
const matchingDataFrame = logsFrames.find((frame) => frame.refId === value.value);
if (matchingDataFrame) {
setCurrentDataFrame(logsFrames.find((frame) => frame.refId === value.value) ?? logsFrames[0]);
}
props.updatePanelState({ refId: value.value });
};
const sidebarWidth = 220;
const totalWidth = props.width;
const tableWidth = totalWidth - sidebarWidth;
const styles = getStyles(props.theme, height, sidebarWidth);
return (
<div className={styles.wrapper}>
<section className={styles.sidebar}>
<LogsColumnSearch onChange={onSearchInputChange} />
<LogsTableMultiSelect
toggleColumn={toggleColumn}
filteredColumnsWithMeta={filteredColumnsWithMeta}
<>
<div>
{logsFrames.length > 1 && (
<div>
<InlineField
label="Select query"
htmlFor="explore_logs_table_frame_selector"
labelWidth={22}
tooltip="Select a query to visualize in the table."
>
<Select
inputId={'explore_logs_table_frame_selector'}
aria-label={'Select query by name'}
value={currentFrameRefId}
options={logsFrames.map((frame) => {
return {
label: frame.refId,
value: frame.refId,
};
})}
onChange={onFrameSelectorChange}
/>
</InlineField>
</div>
)}
</div>
<div className={styles.wrapper}>
<section className={styles.sidebar}>
<LogsColumnSearch onChange={onSearchInputChange} />
<LogsTableMultiSelect
toggleColumn={toggleColumn}
filteredColumnsWithMeta={filteredColumnsWithMeta}
columnsWithMeta={columnsWithMeta}
/>
</section>
<LogsTable
onClickFilterLabel={props.onClickFilterLabel}
onClickFilterOutLabel={props.onClickFilterOutLabel}
logsSortOrder={props.logsSortOrder}
range={props.range}
splitOpen={props.splitOpen}
timeZone={props.timeZone}
width={tableWidth}
dataFrame={currentDataFrame}
columnsWithMeta={columnsWithMeta}
height={height}
/>
</section>
<LogsTable
onClickFilterLabel={props.onClickFilterLabel}
onClickFilterOutLabel={props.onClickFilterOutLabel}
logsSortOrder={props.logsSortOrder}
range={props.range}
splitOpen={props.splitOpen}
timeZone={props.timeZone}
width={tableWidth}
logsFrames={logsFrames}
columnsWithMeta={columnsWithMeta}
height={height}
/>
</div>
</div>
</>
);
}
@ -347,9 +391,6 @@ function getStyles(theme: GrafanaTheme2, height: number, width: number) {
width: width,
paddingRight: theme.spacing(1.5),
}),
labelCount: css({}),
checkbox: css({}),
};
}

Loading…
Cancel
Save