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; id?: string;
columns?: Record<number, string>; columns?: Record<number, string>;
visualisationType?: 'table' | 'logs'; 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> { export interface SplitOpenOptions<T extends AnyQuery = AnyQuery> {

@ -183,6 +183,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
...state.panelsState.logs, ...state.panelsState.logs,
columns: logsPanelState.columns ?? this.props.panelState?.logs?.columns, columns: logsPanelState.columns ?? this.props.panelState?.logs?.columns,
visualisationType: logsPanelState.visualisationType ?? this.state.visualisationType, 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'), to: toUtc('2019-01-01 16:00:00'),
raw: { from: 'now-1h', to: 'now' }, raw: { from: 'now-1h', to: 'now' },
}} }}
logsFrames={[logs ?? testDataFrame]} dataFrame={logs ?? testDataFrame}
{...partialProps} {...partialProps}
/> />
); );
@ -121,7 +121,7 @@ describe('LogsTable', () => {
it('should render extracted labels as columns (elastic)', async () => { it('should render extracted labels as columns (elastic)', async () => {
setup({ setup({
logsFrames: [getMockElasticFrame()], dataFrame: getMockElasticFrame(),
columnsWithMeta: { columnsWithMeta: {
counter: { active: true, percentOfLinesWithLabel: 3 }, counter: { active: true, percentOfLinesWithLabel: 3 },
level: { active: true, percentOfLinesWithLabel: 3 }, level: { active: true, percentOfLinesWithLabel: 3 },

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

@ -1,6 +1,6 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { useState, useEffect, useCallback } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { import {
DataFrame, DataFrame,
@ -8,11 +8,12 @@ import {
GrafanaTheme2, GrafanaTheme2,
Labels, Labels,
LogsSortOrder, LogsSortOrder,
SelectableValue,
SplitOpen, SplitOpen,
TimeRange, TimeRange,
} from '@grafana/data'; } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime/src'; import { reportInteraction } from '@grafana/runtime/src';
import { Themeable2 } from '@grafana/ui/'; import { InlineField, Select, Themeable2 } from '@grafana/ui/';
import { parseLogsFrame } from '../../logs/logsFrame'; import { parseLogsFrame } from '../../logs/logsFrame';
@ -44,6 +45,7 @@ type fieldNameMetaStore = Record<fieldName, fieldNameMeta>;
export function LogsTableWrap(props: Props) { export function LogsTableWrap(props: Props) {
const { logsFrames } = props; const { logsFrames } = props;
// Save the normalized cardinality of each label // Save the normalized cardinality of each label
const [columnsWithMeta, setColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined); const [columnsWithMeta, setColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
@ -51,7 +53,13 @@ export function LogsTableWrap(props: Props) {
const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined); const [filteredColumnsWithMeta, setFilteredColumnsWithMeta] = useState<fieldNameMetaStore | undefined>(undefined);
const height = getTableHeight(); 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( const getColumnsFromProps = useCallback(
(fieldNames: fieldNameMetaStore) => { (fieldNames: fieldNameMetaStore) => {
@ -100,11 +108,11 @@ export function LogsTableWrap(props: Props) {
*/ */
useEffect(() => { useEffect(() => {
// If the data frame is empty, there's nothing to viz, it could mean the user has unselected all columns // 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; return;
} }
const numberOfLogLines = dataFrame ? dataFrame.length : 0; const numberOfLogLines = currentDataFrame ? currentDataFrame.length : 0;
const logsFrame = parseLogsFrame(dataFrame); const logsFrame = parseLogsFrame(currentDataFrame);
const labels = logsFrame?.getLogFrameLabelsAsLabels(); const labels = logsFrame?.getLogFrameLabelsAsLabels();
const otherFields = []; const otherFields = [];
@ -197,7 +205,7 @@ export function LogsTableWrap(props: Props) {
setColumnsWithMeta(pendingLabelState); setColumnsWithMeta(pendingLabelState);
// The panel state is updated when the user interacts with the multi-select sidebar // The panel state is updated when the user interacts with the multi-select sidebar
}, [dataFrame, getColumnsFromProps]); }, [currentDataFrame, getColumnsFromProps]);
if (!columnsWithMeta) { if (!columnsWithMeta) {
return null; return null;
@ -259,6 +267,7 @@ export function LogsTableWrap(props: Props) {
// Only include active filters // Only include active filters
.filter((key) => pendingLabelState[key]?.active) .filter((key) => pendingLabelState[key]?.active)
), ),
refId: currentDataFrame.refId,
visualisationType: 'table', 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 sidebarWidth = 220;
const totalWidth = props.width; const totalWidth = props.width;
const tableWidth = totalWidth - sidebarWidth; const tableWidth = totalWidth - sidebarWidth;
const styles = getStyles(props.theme, height, sidebarWidth); const styles = getStyles(props.theme, height, sidebarWidth);
return ( return (
<div className={styles.wrapper}> <>
<section className={styles.sidebar}> <div>
<LogsColumnSearch onChange={onSearchInputChange} /> {logsFrames.length > 1 && (
<LogsTableMultiSelect <div>
toggleColumn={toggleColumn} <InlineField
filteredColumnsWithMeta={filteredColumnsWithMeta} 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} columnsWithMeta={columnsWithMeta}
height={height}
/> />
</section> </div>
<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>
); );
} }
@ -347,9 +391,6 @@ function getStyles(theme: GrafanaTheme2, height: number, width: number) {
width: width, width: width,
paddingRight: theme.spacing(1.5), paddingRight: theme.spacing(1.5),
}), }),
labelCount: css({}),
checkbox: css({}),
}; };
} }

Loading…
Cancel
Save