mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Logs: Unify detected fields and labels in Log Details (#60448)
* removed js-fields * added buttons * rename detectedField to field * removed unused things from `logParser.ts` * improve comment * wip * better way for statistics * better hide-stats button * update tests * updated tests and var names * made props optional again * fix padding * fix unused import * removed test * close elements * renamed `LogRowMessageDetectedFields` to `LogRowMessageDisplayedFields` * add active style to menu button * changed comment in logParser * updated ToolbarButton colors * rename `Data Links` to `Links` * fix stats button being wrongly highlighted
This commit is contained in:
parent
8bab6d3658
commit
5b2184c485
@ -3986,9 +3986,6 @@ exports[`better eslint`] = {
|
||||
"public/app/features/logs/components/LogRows.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/logs/components/logParser.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/logs/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
|
@ -20,6 +20,8 @@ describe('Logs', () => {
|
||||
splitOpen={() => undefined}
|
||||
logsVolumeEnabled={true}
|
||||
onSetLogsVolumeEnabled={() => null}
|
||||
onClickFilterLabel={() => null}
|
||||
onClickFilterOutLabel={() => null}
|
||||
logsVolumeData={undefined}
|
||||
loadLogsVolumeData={() => undefined}
|
||||
logRows={rows}
|
||||
|
@ -74,8 +74,8 @@ interface Props extends Themeable2 {
|
||||
loadLogsVolumeData: () => void;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
onChangeTime: (range: AbsoluteTimeRange) => void;
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
onClickFilterLabel: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel: (key: string, value: string) => void;
|
||||
onStartScanning?: () => void;
|
||||
onStopScanning?: () => void;
|
||||
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
||||
@ -94,7 +94,7 @@ interface State {
|
||||
hiddenLogLevels: LogLevel[];
|
||||
logsSortOrder: LogsSortOrder | null;
|
||||
isFlipping: boolean;
|
||||
showDetectedFields: string[];
|
||||
displayedFields: string[];
|
||||
forceEscape: boolean;
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
hiddenLogLevels: [],
|
||||
logsSortOrder: store.get(SETTINGS_KEYS.logsSortOrder) || LogsSortOrder.Descending,
|
||||
isFlipping: false,
|
||||
showDetectedFields: [],
|
||||
displayedFields: [],
|
||||
forceEscape: false,
|
||||
};
|
||||
|
||||
@ -255,24 +255,24 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
showDetectedField = (key: string) => {
|
||||
const index = this.state.showDetectedFields.indexOf(key);
|
||||
showField = (key: string) => {
|
||||
const index = this.state.displayedFields.indexOf(key);
|
||||
|
||||
if (index === -1) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
showDetectedFields: state.showDetectedFields.concat(key),
|
||||
displayedFields: state.displayedFields.concat(key),
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
hideDetectedField = (key: string) => {
|
||||
const index = this.state.showDetectedFields.indexOf(key);
|
||||
hideField = (key: string) => {
|
||||
const index = this.state.displayedFields.indexOf(key);
|
||||
if (index > -1) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
showDetectedFields: state.showDetectedFields.filter((k) => key !== k),
|
||||
displayedFields: state.displayedFields.filter((k) => key !== k),
|
||||
};
|
||||
});
|
||||
}
|
||||
@ -281,7 +281,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
clearDetectedFields = () => {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
showDetectedFields: [],
|
||||
displayedFields: [],
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -355,7 +355,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
hiddenLogLevels,
|
||||
logsSortOrder,
|
||||
isFlipping,
|
||||
showDetectedFields,
|
||||
displayedFields,
|
||||
forceEscape,
|
||||
} = this.state;
|
||||
|
||||
@ -477,7 +477,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
dedupCount={dedupCount}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
forceEscape={forceEscape}
|
||||
showDetectedFields={showDetectedFields}
|
||||
displayedFields={displayedFields}
|
||||
onEscapeNewlines={this.onEscapeNewlines}
|
||||
clearDetectedFields={this.clearDetectedFields}
|
||||
/>
|
||||
@ -500,9 +500,9 @@ class UnthemedLogs extends PureComponent<Props, State> {
|
||||
timeZone={timeZone}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
showDetectedFields={showDetectedFields}
|
||||
onClickShowDetectedField={this.showDetectedField}
|
||||
onClickHideDetectedField={this.hideDetectedField}
|
||||
displayedFields={displayedFields}
|
||||
onClickShowField={this.showField}
|
||||
onClickHideField={this.hideField}
|
||||
app={CoreApp.Explore}
|
||||
scrollElement={scrollElement}
|
||||
onLogRowHover={this.onLogRowHover}
|
||||
|
@ -33,8 +33,8 @@ interface LogsContainerProps extends PropsFromRedux {
|
||||
syncedTimes: boolean;
|
||||
loadingState: LoadingState;
|
||||
scrollElement?: HTMLDivElement;
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
onClickFilterLabel: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel: (key: string, value: string) => void;
|
||||
onStartScanning: () => void;
|
||||
onStopScanning: () => void;
|
||||
eventBus: EventBus;
|
||||
|
@ -23,7 +23,7 @@ export type Props = {
|
||||
meta: LogsMetaItem[];
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
dedupCount: number;
|
||||
showDetectedFields: string[];
|
||||
displayedFields: string[];
|
||||
hasUnescapedContent: boolean;
|
||||
forceEscape: boolean;
|
||||
logRows: LogRowModel[];
|
||||
@ -36,7 +36,7 @@ export const LogsMetaRow = React.memo(
|
||||
meta,
|
||||
dedupStrategy,
|
||||
dedupCount,
|
||||
showDetectedFields,
|
||||
displayedFields,
|
||||
clearDetectedFields,
|
||||
hasUnescapedContent,
|
||||
forceEscape,
|
||||
@ -74,11 +74,11 @@ export const LogsMetaRow = React.memo(
|
||||
}
|
||||
|
||||
// Add detected fields info
|
||||
if (showDetectedFields?.length > 0) {
|
||||
if (displayedFields?.length > 0) {
|
||||
logsMetaItem.push(
|
||||
{
|
||||
label: 'Showing only detected fields',
|
||||
value: renderMetaItem(showDetectedFields, LogsMetaKind.LabelsMap),
|
||||
value: renderMetaItem(displayedFields, LogsMetaKind.LabelsMap),
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
|
@ -8,12 +8,15 @@ import { createLogRow } from './__mocks__/logRow';
|
||||
|
||||
const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowModel>) => {
|
||||
const props: Props = {
|
||||
displayedFields: [],
|
||||
showDuplicates: false,
|
||||
wrapLogMessage: false,
|
||||
row: createLogRow({ logLevel: LogLevel.error, timeEpochMs: 1546297200000, ...rowOverrides }),
|
||||
getRows: () => [],
|
||||
onClickFilterLabel: () => {},
|
||||
onClickFilterOutLabel: () => {},
|
||||
onClickShowField: () => {},
|
||||
onClickHideField: () => {},
|
||||
theme: createTheme(),
|
||||
...(propOverrides || {}),
|
||||
};
|
||||
@ -31,7 +34,7 @@ describe('LogDetails', () => {
|
||||
describe('when labels are present', () => {
|
||||
it('should render heading', () => {
|
||||
setup(undefined, { labels: { key1: 'label1', key2: 'label2' } });
|
||||
expect(screen.getAllByLabelText('Log labels')).toHaveLength(1);
|
||||
expect(screen.getAllByLabelText('Fields')).toHaveLength(1);
|
||||
});
|
||||
it('should render labels', () => {
|
||||
setup(undefined, { labels: { key1: 'label1', key2: 'label2' } });
|
||||
@ -48,29 +51,15 @@ describe('LogDetails', () => {
|
||||
expect(screen.getByLabelText('Log level').classList.toString()).not.toContain('logs-row__level');
|
||||
});
|
||||
});
|
||||
describe('when row entry has parsable fields', () => {
|
||||
it('should render heading ', () => {
|
||||
setup(undefined, { entry: 'test=successful' });
|
||||
expect(screen.getAllByTitle('Ad-hoc statistics')).toHaveLength(1);
|
||||
});
|
||||
it('should render detected fields', () => {
|
||||
setup(undefined, { entry: 'test=successful' });
|
||||
expect(screen.getByRole('cell', { name: 'test' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: 'successful' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('when row entry have parsable fields and labels are present', () => {
|
||||
it('should render all headings', () => {
|
||||
setup(undefined, { entry: 'test=successful', labels: { key: 'label' } });
|
||||
expect(screen.getAllByLabelText('Log labels')).toHaveLength(1);
|
||||
expect(screen.getAllByLabelText('Detected fields')).toHaveLength(1);
|
||||
expect(screen.getAllByLabelText('Fields')).toHaveLength(1);
|
||||
});
|
||||
it('should render all labels and detected fields', () => {
|
||||
setup(undefined, { entry: 'test=successful', labels: { key: 'label' } });
|
||||
expect(screen.getByRole('cell', { name: 'key' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: 'label' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: 'test' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('cell', { name: 'successful' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('when row entry and labels are not present', () => {
|
||||
|
@ -2,8 +2,8 @@ import { css, cx } from '@emotion/css';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { Field, LinkModel, LogRowModel, GrafanaTheme2, CoreApp, DataFrame } from '@grafana/data';
|
||||
import { withTheme2, Themeable2, Icon, Tooltip } from '@grafana/ui';
|
||||
import { CoreApp, DataFrame, Field, GrafanaTheme2, LinkModel, LogRowModel } from '@grafana/data';
|
||||
import { Themeable2, withTheme2 } from '@grafana/ui';
|
||||
|
||||
import { calculateFieldStats, calculateLogsLabelStats, calculateStats, getParser } from '../utils';
|
||||
|
||||
@ -25,9 +25,9 @@ export interface Props extends Themeable2 {
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
showDetectedFields?: string[];
|
||||
onClickShowDetectedField?: (key: string) => void;
|
||||
onClickHideDetectedField?: (key: string) => void;
|
||||
displayedFields?: string[];
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
@ -52,7 +52,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
class UnThemedLogDetails extends PureComponent<Props> {
|
||||
getParser = memoizeOne(getParser);
|
||||
|
||||
getStatsForDetectedField = (key: string) => {
|
||||
getStatsForField = (key: string) => {
|
||||
const matcher = this.getParser(this.props.row.entry)!.buildMatcher(key);
|
||||
return calculateFieldStats(this.props.getRows(), matcher);
|
||||
};
|
||||
@ -68,9 +68,9 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
getRows,
|
||||
showDuplicates,
|
||||
className,
|
||||
onClickShowDetectedField,
|
||||
onClickHideDetectedField,
|
||||
showDetectedFields,
|
||||
onClickShowField,
|
||||
onClickHideField,
|
||||
displayedFields,
|
||||
getFieldLinks,
|
||||
wrapLogMessage,
|
||||
} = this.props;
|
||||
@ -78,8 +78,12 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
const styles = getStyles(theme);
|
||||
const labels = row.labels ? row.labels : {};
|
||||
const labelsAvailable = Object.keys(labels).length > 0;
|
||||
const fields = getAllFields(row, getFieldLinks);
|
||||
const detectedFieldsAvailable = fields && fields.length > 0;
|
||||
const fieldsAndLinks = getAllFields(row, getFieldLinks);
|
||||
const links = fieldsAndLinks.filter((f) => f.links?.length).sort();
|
||||
const fields = fieldsAndLinks.filter((f) => f.links?.length === 0).sort();
|
||||
const fieldsAvailable = fields && fields.length > 0;
|
||||
const linksAvailable = links && links.length > 0;
|
||||
|
||||
// If logs with error, we are not showing the level color
|
||||
const levelClassName = cx(!hasError && [style.logsRowLevel, styles.logsRowLevelDetails]);
|
||||
|
||||
@ -91,10 +95,10 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
<div className={style.logDetailsContainer}>
|
||||
<table className={style.logDetailsTable}>
|
||||
<tbody>
|
||||
{labelsAvailable && (
|
||||
{(labelsAvailable || fieldsAvailable) && (
|
||||
<tr>
|
||||
<td colSpan={5} className={style.logDetailsHeading} aria-label="Log labels">
|
||||
Log labels
|
||||
<td colSpan={100} className={style.logDetailsHeading} aria-label="Fields">
|
||||
Fields
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@ -111,29 +115,47 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
getStats={() => calculateLogsLabelStats(getRows(), key)}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
row={row}
|
||||
app={app}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
displayedFields={displayedFields}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{fields.map((field) => {
|
||||
const { key, value, fieldIndex } = field;
|
||||
return (
|
||||
<LogDetailsRow
|
||||
key={`${key}=${value}`}
|
||||
parsedKey={key}
|
||||
parsedValue={value}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
getStats={() =>
|
||||
fieldIndex === undefined
|
||||
? this.getStatsForField(key)
|
||||
: calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())
|
||||
}
|
||||
displayedFields={displayedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
row={row}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{detectedFieldsAvailable && (
|
||||
{linksAvailable && (
|
||||
<tr>
|
||||
<td colSpan={5} className={style.logDetailsHeading} aria-label="Detected fields">
|
||||
Detected fields
|
||||
<Tooltip content="Fields that are parsed from log message and detected by Grafana.">
|
||||
<Icon
|
||||
name="question-circle"
|
||||
size="xs"
|
||||
className={css`
|
||||
margin-left: ${theme.spacing(0.5)};
|
||||
`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<td colSpan={100} className={style.logDetailsHeading} aria-label="Data Links">
|
||||
Links
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{fields.sort().map((field) => {
|
||||
{links.map((field) => {
|
||||
const { key, value, links, fieldIndex } = field;
|
||||
return (
|
||||
<LogDetailsRow
|
||||
@ -141,23 +163,23 @@ class UnThemedLogDetails extends PureComponent<Props> {
|
||||
parsedKey={key}
|
||||
parsedValue={value}
|
||||
links={links}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
getStats={() =>
|
||||
fieldIndex === undefined
|
||||
? this.getStatsForDetectedField(key)
|
||||
? this.getStatsForField(key)
|
||||
: calculateStats(row.dataFrame.fields[fieldIndex].values.toArray())
|
||||
}
|
||||
showDetectedFields={showDetectedFields}
|
||||
displayedFields={displayedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
row={row}
|
||||
app={app}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{!detectedFieldsAvailable && !labelsAvailable && (
|
||||
{!fieldsAvailable && !labelsAvailable && !linksAvailable && (
|
||||
<tr>
|
||||
<td colSpan={5} aria-label="No details">
|
||||
<td colSpan={100} aria-label="No details">
|
||||
No details available
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { screen, render, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { LogRowModel } from '@grafana/data';
|
||||
@ -17,9 +16,9 @@ const setup = (propOverrides?: Partial<Props>) => {
|
||||
getStats: () => null,
|
||||
onClickFilterLabel: () => {},
|
||||
onClickFilterOutLabel: () => {},
|
||||
onClickShowDetectedField: () => {},
|
||||
onClickHideDetectedField: () => {},
|
||||
showDetectedFields: [],
|
||||
onClickShowField: () => {},
|
||||
onClickHideField: () => {},
|
||||
displayedFields: [],
|
||||
row: {} as LogRowModel,
|
||||
};
|
||||
|
||||
@ -51,40 +50,24 @@ describe('LogDetailsRow', () => {
|
||||
|
||||
it('should render metrics button', () => {
|
||||
setup();
|
||||
expect(screen.getAllByTitle('Ad-hoc statistics')).toHaveLength(1);
|
||||
expect(screen.getAllByRole('button', { name: 'Ad-hoc statistics' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('if props is a label', () => {
|
||||
it('should render filter label button', () => {
|
||||
setup();
|
||||
expect(screen.getAllByTitle('Filter for value')).toHaveLength(1);
|
||||
expect(screen.getAllByRole('button', { name: 'Filter for value' })).toHaveLength(1);
|
||||
});
|
||||
it('should render filter out label button', () => {
|
||||
setup();
|
||||
expect(screen.getAllByTitle('Filter out value')).toHaveLength(1);
|
||||
});
|
||||
it('should not render filtering buttons if no filtering functions provided', () => {
|
||||
setup({ onClickFilterLabel: undefined, onClickFilterOutLabel: undefined });
|
||||
expect(screen.queryByTitle('Filter out value')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Filter out value' })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if props is not a label', () => {
|
||||
it('should not render a filter label button', () => {
|
||||
setup({ isLabel: false });
|
||||
expect(screen.queryByTitle('Filter for value')).not.toBeInTheDocument();
|
||||
});
|
||||
it('should render a show toggleFieldButton button', () => {
|
||||
setup({ isLabel: false });
|
||||
expect(screen.getAllByTitle('Show this field instead of the message')).toHaveLength(1);
|
||||
});
|
||||
it('should not render a show toggleFieldButton button if no detected fields toggling functions provided', () => {
|
||||
setup({
|
||||
isLabel: false,
|
||||
onClickShowDetectedField: undefined,
|
||||
onClickHideDetectedField: undefined,
|
||||
});
|
||||
expect(screen.queryByTitle('Show this field instead of the message')).not.toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Show this field instead of the message' })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,21 +92,9 @@ describe('LogDetailsRow', () => {
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('logLabelStats')).not.toBeInTheDocument();
|
||||
const adHocStatsButton = screen.getByTitle('Ad-hoc statistics');
|
||||
const adHocStatsButton = screen.getByRole('button', { name: 'Ad-hoc statistics' });
|
||||
fireEvent.click(adHocStatsButton);
|
||||
expect(screen.getByTestId('logLabelStats')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('logLabelStats')).toHaveTextContent('another value');
|
||||
});
|
||||
|
||||
it('should render clipboard button on hover of log row table value', async () => {
|
||||
setup({ parsedKey: 'key', parsedValue: 'value' });
|
||||
|
||||
const valueCell = screen.getByRole('cell', { name: 'value' });
|
||||
await userEvent.hover(valueCell);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Copy value to clipboard' })).toBeInTheDocument();
|
||||
await userEvent.unhover(valueCell);
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Copy value to clipboard' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { Field, LinkModel, LogLabelStatsModel, GrafanaTheme2, LogRowModel, CoreApp } from '@grafana/data';
|
||||
import { CoreApp, Field, GrafanaTheme2, LinkModel, LogLabelStatsModel, LogRowModel } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { withTheme2, Themeable2, ClipboardButton, DataLinkButton, IconButton } from '@grafana/ui';
|
||||
import { ClipboardButton, DataLinkButton, Themeable2, ToolbarButton, ToolbarButtonRow, withTheme2 } from '@grafana/ui';
|
||||
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
@ -19,9 +20,9 @@ export interface Props extends Themeable2 {
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
links?: Array<LinkModel<Field>>;
|
||||
getStats: () => LogLabelStatsModel[] | null;
|
||||
showDetectedFields?: string[];
|
||||
onClickShowDetectedField?: (key: string) => void;
|
||||
onClickHideDetectedField?: (key: string) => void;
|
||||
displayedFields?: string[];
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
row: LogRowModel;
|
||||
app?: CoreApp;
|
||||
}
|
||||
@ -30,10 +31,43 @@ interface State {
|
||||
showFieldsStats: boolean;
|
||||
fieldCount: number;
|
||||
fieldStats: LogLabelStatsModel[] | null;
|
||||
mouseOver: boolean;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const getStyles = memoizeOne((theme: GrafanaTheme2, activeButton: boolean) => {
|
||||
// those styles come from ToolbarButton. Unfortunately this is needed because we can not control the variant of the menu-button in a ToolbarButtonRow.
|
||||
const defaultOld = css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
background-color: ${theme.colors.background.primary};
|
||||
|
||||
&:hover {
|
||||
color: ${theme.colors.text.primary};
|
||||
background: ${theme.colors.background.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const defaultTopNav = css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.colors.text.primary};
|
||||
background: ${theme.colors.background.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const active = css`
|
||||
color: ${theme.v1.palette.orangeDark};
|
||||
border-color: ${theme.v1.palette.orangeDark};
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: ${theme.colors.text.primary};
|
||||
background: ${theme.colors.emphasize(theme.colors.background.canvas, 0.03)};
|
||||
}
|
||||
`;
|
||||
|
||||
const defaultToolbarButtonStyle = theme.flags.topnav ? defaultTopNav : defaultOld;
|
||||
return {
|
||||
noHoverBackground: css`
|
||||
label: noHoverBackground;
|
||||
@ -52,32 +86,83 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
showingField: css`
|
||||
color: ${theme.colors.primary.text};
|
||||
`,
|
||||
hoverValueCopy: css`
|
||||
margin: ${theme.spacing(0, 0, 0, 1.2)};
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
justify-content: center;
|
||||
border-radius: ${theme.shape.borderRadius(10)};
|
||||
width: ${theme.spacing(3.25)};
|
||||
height: ${theme.spacing(3.25)};
|
||||
copyButton: css`
|
||||
& > button {
|
||||
color: ${theme.colors.text.secondary};
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
height: ${theme.spacing(theme.components.height.sm)};
|
||||
width: ${theme.spacing(theme.components.height.sm)};
|
||||
svg {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span > div {
|
||||
top: -5px;
|
||||
& button {
|
||||
color: ${theme.colors.success.main};
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
wrapLine: css`
|
||||
label: wrapLine;
|
||||
white-space: pre-wrap;
|
||||
`,
|
||||
toolbarButtonRow: css`
|
||||
label: toolbarButtonRow;
|
||||
gap: ${theme.spacing(0.5)};
|
||||
|
||||
max-width: calc(3 * ${theme.spacing(theme.components.height.sm)});
|
||||
& > div {
|
||||
height: ${theme.spacing(theme.components.height.sm)};
|
||||
width: ${theme.spacing(theme.components.height.sm)};
|
||||
& > button {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
height: inherit;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
& div:last-child > button:not(.stats-button) {
|
||||
${activeButton ? active : defaultToolbarButtonStyle};
|
||||
}
|
||||
`,
|
||||
logDetailsStats: css`
|
||||
padding: 0 ${theme.spacing(1)};
|
||||
`,
|
||||
logDetailsValue: css`
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
line-height: 22px;
|
||||
|
||||
.show-on-hover {
|
||||
display: inline;
|
||||
visibility: hidden;
|
||||
}
|
||||
&:hover {
|
||||
.show-on-hover {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||
state: State = {
|
||||
showFieldsStats: false,
|
||||
fieldCount: 0,
|
||||
fieldStats: null,
|
||||
mouseOver: false,
|
||||
};
|
||||
|
||||
showField = () => {
|
||||
const { onClickShowDetectedField, parsedKey, row } = this.props;
|
||||
const { onClickShowField: onClickShowDetectedField, parsedKey, row } = this.props;
|
||||
if (onClickShowDetectedField) {
|
||||
onClickShowDetectedField(parsedKey);
|
||||
}
|
||||
@ -90,7 +175,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
hideField = () => {
|
||||
const { onClickHideDetectedField, parsedKey, row } = this.props;
|
||||
const { onClickHideField: onClickHideDetectedField, parsedKey, row } = this.props;
|
||||
if (onClickHideDetectedField) {
|
||||
onClickHideDetectedField(parsedKey);
|
||||
}
|
||||
@ -155,11 +240,6 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
hoverValueCopy() {
|
||||
const mouseOver = !this.state.mouseOver;
|
||||
this.setState({ mouseOver });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
theme,
|
||||
@ -167,87 +247,120 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
||||
parsedValue,
|
||||
isLabel,
|
||||
links,
|
||||
showDetectedFields,
|
||||
displayedFields,
|
||||
wrapLogMessage,
|
||||
onClickShowDetectedField,
|
||||
onClickHideDetectedField,
|
||||
onClickFilterLabel,
|
||||
onClickFilterOutLabel,
|
||||
} = this.props;
|
||||
const { showFieldsStats, fieldStats, fieldCount, mouseOver } = this.state;
|
||||
const styles = getStyles(theme);
|
||||
const { showFieldsStats, fieldStats, fieldCount } = this.state;
|
||||
const activeButton = displayedFields?.includes(parsedKey) || showFieldsStats;
|
||||
const styles = getStyles(theme, activeButton);
|
||||
const style = getLogRowStyles(theme);
|
||||
|
||||
const hasDetectedFieldsFunctionality = onClickShowDetectedField && onClickHideDetectedField;
|
||||
const hasFilteringFunctionality = onClickFilterLabel && onClickFilterOutLabel;
|
||||
|
||||
const toggleFieldButton =
|
||||
!isLabel && showDetectedFields && showDetectedFields.includes(parsedKey) ? (
|
||||
<IconButton name="eye" className={styles.showingField} title="Hide this field" onClick={this.hideField} />
|
||||
displayedFields && displayedFields.includes(parsedKey) ? (
|
||||
<ToolbarButton variant="active" tooltip="Hide this field" iconOnly narrow icon="eye" onClick={this.hideField} />
|
||||
) : (
|
||||
<IconButton name="eye" title="Show this field instead of the message" onClick={this.showField} />
|
||||
<ToolbarButton
|
||||
tooltip="Show this field instead of the message"
|
||||
iconOnly
|
||||
narrow
|
||||
icon="eye"
|
||||
onClick={this.showField}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className={cx(style.logDetailsValue, { [styles.noHoverBackground]: showFieldsStats })}>
|
||||
{/* Action buttons - show stats/filter results */}
|
||||
<td className={style.logsDetailsIcon}>
|
||||
<IconButton name="signal" title={'Ad-hoc statistics'} onClick={this.showStats} />
|
||||
</td>
|
||||
|
||||
{hasFilteringFunctionality && isLabel && (
|
||||
<>
|
||||
<td className={style.logsDetailsIcon}>
|
||||
<IconButton name="search-plus" title="Filter for value" onClick={this.filterLabel} />
|
||||
</td>
|
||||
<td className={style.logsDetailsIcon}>
|
||||
<IconButton name="search-minus" title="Filter out value" onClick={this.filterOutLabel} />
|
||||
</td>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasDetectedFieldsFunctionality && !isLabel && (
|
||||
<td className={style.logsDetailsIcon} colSpan={2}>
|
||||
{toggleFieldButton}
|
||||
<>
|
||||
<tr className={cx(style.logDetailsValue)}>
|
||||
<td className={style.logsDetailsIcon}>
|
||||
<ToolbarButtonRow alignment="left" className={styles.toolbarButtonRow}>
|
||||
{hasFilteringFunctionality && (
|
||||
<ToolbarButton
|
||||
iconOnly
|
||||
narrow
|
||||
icon="search-plus"
|
||||
tooltip="Filter for value"
|
||||
onClick={this.filterLabel}
|
||||
/>
|
||||
)}
|
||||
{hasFilteringFunctionality && (
|
||||
<ToolbarButton
|
||||
iconOnly
|
||||
narrow
|
||||
icon="search-minus"
|
||||
tooltip="Filter out value"
|
||||
onClick={this.filterOutLabel}
|
||||
/>
|
||||
)}
|
||||
{displayedFields && toggleFieldButton}
|
||||
<ToolbarButton
|
||||
iconOnly
|
||||
variant={showFieldsStats ? 'active' : 'default'}
|
||||
narrow
|
||||
icon="signal"
|
||||
tooltip="Ad-hoc statistics"
|
||||
className="stats-button"
|
||||
onClick={this.showStats}
|
||||
/>
|
||||
</ToolbarButtonRow>
|
||||
</td>
|
||||
)}
|
||||
|
||||
{/* Key - value columns */}
|
||||
<td className={style.logDetailsLabel}>{parsedKey}</td>
|
||||
<td
|
||||
className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}
|
||||
onMouseEnter={this.hoverValueCopy.bind(this)}
|
||||
onMouseLeave={this.hoverValueCopy.bind(this)}
|
||||
>
|
||||
{parsedValue}
|
||||
{mouseOver && (
|
||||
<ClipboardButton
|
||||
getText={() => parsedValue}
|
||||
title="Copy value to clipboard"
|
||||
fill="text"
|
||||
variant="secondary"
|
||||
icon="copy"
|
||||
size="sm"
|
||||
className={styles.hoverValueCopy}
|
||||
/>
|
||||
)}
|
||||
{links?.map((link) => (
|
||||
<span key={link.title}>
|
||||
|
||||
<DataLinkButton link={link} />
|
||||
</span>
|
||||
))}
|
||||
{showFieldsStats && (
|
||||
<LogLabelStats
|
||||
stats={fieldStats!}
|
||||
label={parsedKey}
|
||||
value={parsedValue}
|
||||
rowCount={fieldCount}
|
||||
isLabel={isLabel}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
{/* Key - value columns */}
|
||||
<td className={style.logDetailsLabel}>{parsedKey}</td>
|
||||
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
|
||||
<div className={styles.logDetailsValue}>
|
||||
{parsedValue}
|
||||
|
||||
<div className={cx('show-on-hover', styles.copyButton)}>
|
||||
<ClipboardButton
|
||||
getText={() => parsedValue}
|
||||
title="Copy value to clipboard"
|
||||
fill="text"
|
||||
variant="secondary"
|
||||
icon="copy"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{links?.map((link) => (
|
||||
<span key={link.title}>
|
||||
|
||||
<DataLinkButton link={link} />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{showFieldsStats && (
|
||||
<tr>
|
||||
<td>
|
||||
<ToolbarButtonRow alignment="left" className={styles.toolbarButtonRow}>
|
||||
<ToolbarButton
|
||||
iconOnly
|
||||
variant={showFieldsStats ? 'active' : 'default'}
|
||||
narrow
|
||||
icon="signal"
|
||||
tooltip="Hide ad-hoc statistics"
|
||||
onClick={this.showStats}
|
||||
/>
|
||||
</ToolbarButtonRow>
|
||||
</td>
|
||||
<td colSpan={2}>
|
||||
<div className={styles.logDetailsStats}>
|
||||
<LogLabelStats
|
||||
stats={fieldStats!}
|
||||
label={parsedKey}
|
||||
value={parsedValue}
|
||||
rowCount={fieldCount}
|
||||
isLabel={isLabel}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
RowContextOptions,
|
||||
} from './LogRowContextProvider';
|
||||
import { LogRowMessage } from './LogRowMessage';
|
||||
import { LogRowMessageDetectedFields } from './LogRowMessageDetectedFields';
|
||||
import { LogRowMessageDisplayedFields } from './LogRowMessageDisplayedFields';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
|
||||
//Components
|
||||
@ -44,10 +44,10 @@ interface Props extends Themeable2 {
|
||||
enableLogDetails: boolean;
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
forceEscape?: boolean;
|
||||
showDetectedFields?: string[];
|
||||
scrollElement?: HTMLDivElement;
|
||||
showRowMenu?: boolean;
|
||||
app?: CoreApp;
|
||||
displayedFields?: string[];
|
||||
getRows: () => LogRowModel[];
|
||||
onClickFilterLabel?: (key: string, value: string) => void;
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
@ -55,8 +55,8 @@ interface Props extends Themeable2 {
|
||||
getRowContext: (row: LogRowModel, options?: RowContextOptions) => Promise<DataQueryResponse>;
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
onClickShowDetectedField?: (key: string) => void;
|
||||
onClickHideDetectedField?: (key: string) => void;
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
onLogRowHover?: (row?: LogRowModel) => void;
|
||||
toggleContextIsOpen?: () => void;
|
||||
}
|
||||
@ -149,8 +149,8 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
getRows,
|
||||
onClickFilterLabel,
|
||||
onClickFilterOutLabel,
|
||||
onClickShowDetectedField,
|
||||
onClickHideDetectedField,
|
||||
onClickShowField,
|
||||
onClickHideField,
|
||||
enableLogDetails,
|
||||
row,
|
||||
showDuplicates,
|
||||
@ -158,7 +158,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
showRowMenu,
|
||||
showLabels,
|
||||
showTime,
|
||||
showDetectedFields,
|
||||
displayedFields,
|
||||
wrapLogMessage,
|
||||
prettifyLogMessage,
|
||||
theme,
|
||||
@ -217,10 +217,10 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
<LogLabels labels={processedRow.uniqueLabels} />
|
||||
</td>
|
||||
)}
|
||||
{showDetectedFields && showDetectedFields.length > 0 ? (
|
||||
<LogRowMessageDetectedFields
|
||||
{displayedFields && displayedFields.length > 0 ? (
|
||||
<LogRowMessageDisplayedFields
|
||||
row={processedRow}
|
||||
showDetectedFields={showDetectedFields!}
|
||||
showDetectedFields={displayedFields!}
|
||||
getFieldLinks={getFieldLinks}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
/>
|
||||
@ -251,13 +251,13 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
getFieldLinks={getFieldLinks}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
getRows={getRows}
|
||||
row={processedRow}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
hasError={hasError}
|
||||
showDetectedFields={showDetectedFields}
|
||||
displayedFields={displayedFields}
|
||||
app={app}
|
||||
/>
|
||||
)}
|
||||
|
@ -13,7 +13,7 @@ export interface Props extends Themeable2 {
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
}
|
||||
|
||||
class UnThemedLogRowMessageDetectedFields extends PureComponent<Props> {
|
||||
class UnThemedLogRowMessageDisplayedFields extends PureComponent<Props> {
|
||||
render() {
|
||||
const { row, showDetectedFields, getFieldLinks, wrapLogMessage } = this.props;
|
||||
const fields = getAllFields(row, getFieldLinks);
|
||||
@ -30,10 +30,14 @@ class UnThemedLogRowMessageDetectedFields extends PureComponent<Props> {
|
||||
return key === parsedKey;
|
||||
});
|
||||
|
||||
if (field) {
|
||||
if (field !== undefined && field !== null) {
|
||||
return `${parsedKey}=${field.value}`;
|
||||
}
|
||||
|
||||
if (row.labels[parsedKey] !== undefined && row.labels[parsedKey] !== null) {
|
||||
return `${parsedKey}=${row.labels[parsedKey]}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((s) => s !== null)
|
||||
@ -43,5 +47,5 @@ class UnThemedLogRowMessageDetectedFields extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export const LogRowMessageDetectedFields = withTheme2(UnThemedLogRowMessageDetectedFields);
|
||||
LogRowMessageDetectedFields.displayName = 'LogRowMessageDetectedFields';
|
||||
export const LogRowMessageDisplayedFields = withTheme2(UnThemedLogRowMessageDisplayedFields);
|
||||
LogRowMessageDisplayedFields.displayName = 'LogRowMessageDisplayedFields';
|
@ -20,6 +20,11 @@ describe('LogRows', () => {
|
||||
prettifyLogMessage={true}
|
||||
timeZone={'utc'}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -62,6 +67,11 @@ describe('LogRows', () => {
|
||||
timeZone={'utc'}
|
||||
previewLimit={1}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -87,6 +97,11 @@ describe('LogRows', () => {
|
||||
prettifyLogMessage={true}
|
||||
timeZone={'utc'}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryAllByRole('row')).toHaveLength(2);
|
||||
@ -107,6 +122,11 @@ describe('LogRows', () => {
|
||||
prettifyLogMessage={true}
|
||||
timeZone={'utc'}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -131,6 +151,11 @@ describe('LogRows', () => {
|
||||
timeZone={'utc'}
|
||||
logsSortOrder={LogsSortOrder.Ascending}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -155,6 +180,11 @@ describe('LogRows', () => {
|
||||
timeZone={'utc'}
|
||||
logsSortOrder={LogsSortOrder.Descending}
|
||||
enableLogDetails={true}
|
||||
displayedFields={[]}
|
||||
onClickFilterLabel={() => {}}
|
||||
onClickFilterOutLabel={() => {}}
|
||||
onClickHideField={() => {}}
|
||||
onClickShowField={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -35,7 +35,7 @@ export interface Props extends Themeable2 {
|
||||
logsSortOrder?: LogsSortOrder | null;
|
||||
previewLimit?: number;
|
||||
forceEscape?: boolean;
|
||||
showDetectedFields?: string[];
|
||||
displayedFields?: string[];
|
||||
app?: CoreApp;
|
||||
scrollElement?: HTMLDivElement;
|
||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||
@ -43,8 +43,8 @@ export interface Props extends Themeable2 {
|
||||
onClickFilterOutLabel?: (key: string, value: string) => void;
|
||||
getRowContext?: (row: LogRowModel, options?: RowContextOptions) => Promise<any>;
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>;
|
||||
onClickShowDetectedField?: (key: string) => void;
|
||||
onClickHideDetectedField?: (key: string) => void;
|
||||
onClickShowField?: (key: string) => void;
|
||||
onClickHideField?: (key: string) => void;
|
||||
onLogRowHover?: (row?: LogRowModel) => void;
|
||||
}
|
||||
|
||||
@ -121,9 +121,9 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
previewLimit,
|
||||
getFieldLinks,
|
||||
logsSortOrder,
|
||||
showDetectedFields,
|
||||
onClickShowDetectedField,
|
||||
onClickHideDetectedField,
|
||||
displayedFields,
|
||||
onClickShowField,
|
||||
onClickHideField,
|
||||
forceEscape,
|
||||
onLogRowHover,
|
||||
app,
|
||||
@ -162,15 +162,15 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
displayedFields={displayedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
enableLogDetails={enableLogDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
@ -193,15 +193,15 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
showDuplicates={showDuplicates}
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
showDetectedFields={showDetectedFields}
|
||||
displayedFields={displayedFields}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
enableLogDetails={enableLogDetails}
|
||||
onClickFilterLabel={onClickFilterLabel}
|
||||
onClickFilterOutLabel={onClickFilterOutLabel}
|
||||
onClickShowDetectedField={onClickShowDetectedField}
|
||||
onClickHideDetectedField={onClickHideDetectedField}
|
||||
onClickShowField={onClickShowField}
|
||||
onClickHideField={onClickHideField}
|
||||
getFieldLinks={getFieldLinks}
|
||||
logsSortOrder={logsSortOrder}
|
||||
forceEscape={forceEscape}
|
||||
|
@ -147,8 +147,9 @@ export const getLogRowStyles = (theme: GrafanaTheme2, logLevel?: LogLevel) => {
|
||||
label: logs-row-details__icon;
|
||||
position: relative;
|
||||
color: ${theme.v1.palette.gray3};
|
||||
padding-top: ${theme.spacing(0.75)};
|
||||
padding-left: ${theme.spacing(0.75)};
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-right: ${theme.spacing(0.75)};
|
||||
`,
|
||||
logDetailsLabel: css`
|
||||
label: logs-row-details__label;
|
||||
|
@ -2,12 +2,6 @@ import memoizeOne from 'memoize-one';
|
||||
|
||||
import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data';
|
||||
|
||||
import { getParser } from '../utils';
|
||||
|
||||
import { MAX_CHARACTERS } from './LogRowMessage';
|
||||
|
||||
const memoizedGetParser = memoizeOne(getParser);
|
||||
|
||||
type FieldDef = {
|
||||
key: string;
|
||||
value: string;
|
||||
@ -24,56 +18,15 @@ export const getAllFields = memoizeOne(
|
||||
row: LogRowModel,
|
||||
getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array<LinkModel<Field>>
|
||||
) => {
|
||||
const logMessageFields = parseMessage(row.entry);
|
||||
const dataframeFields = getDataframeFields(row, getFieldLinks);
|
||||
const fieldsMap = [...dataframeFields, ...logMessageFields].reduce((acc, field) => {
|
||||
// Strip enclosing quotes for hashing. When values are parsed from log line the quotes are kept, but if same
|
||||
// value is in the dataFrame it will be without the quotes. We treat them here as the same value.
|
||||
// We need to handle this scenario:
|
||||
// - we use derived-fields in Loki
|
||||
// - we name the derived field the same as the parsed-field-name
|
||||
// - the same field will appear twice
|
||||
// - in the fields coming from `logMessageFields`
|
||||
// - in the fields coming from `dataframeFields`
|
||||
// - but, in the fields coming from `logMessageFields`, there might be doublequotes around the value
|
||||
// - we want to "merge" data from both sources, so we remove quotes from the beginning and end
|
||||
const value = field.value.replace(/(^")|("$)/g, '');
|
||||
const fieldHash = `${field.key}=${value}`;
|
||||
if (acc[fieldHash]) {
|
||||
acc[fieldHash].links = [...(acc[fieldHash].links || []), ...(field.links || [])];
|
||||
} else {
|
||||
acc[fieldHash] = field;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: FieldDef });
|
||||
|
||||
const allFields = Object.values(fieldsMap);
|
||||
allFields.sort(sortFieldsLinkFirst);
|
||||
|
||||
return allFields;
|
||||
return Object.values(dataframeFields);
|
||||
}
|
||||
);
|
||||
|
||||
const parseMessage = memoizeOne((rowEntry): FieldDef[] => {
|
||||
if (rowEntry.length > MAX_CHARACTERS) {
|
||||
return [];
|
||||
}
|
||||
const parser = memoizedGetParser(rowEntry);
|
||||
if (!parser) {
|
||||
return [];
|
||||
}
|
||||
// Use parser to highlight detected fields
|
||||
const detectedFields = parser.getFields(rowEntry);
|
||||
const fields = detectedFields.map((field) => {
|
||||
const key = parser.getLabelFromField(field);
|
||||
const value = parser.getValueFromField(field);
|
||||
return { key, value };
|
||||
});
|
||||
|
||||
return fields;
|
||||
});
|
||||
|
||||
// creates fields from the dataframe-fields, adding data-links, when field.config.links exists
|
||||
/**
|
||||
* creates fields from the dataframe-fields, adding data-links, when field.config.links exists
|
||||
*/
|
||||
const getDataframeFields = memoizeOne(
|
||||
(
|
||||
row: LogRowModel,
|
||||
@ -94,30 +47,28 @@ const getDataframeFields = memoizeOne(
|
||||
}
|
||||
);
|
||||
|
||||
function sortFieldsLinkFirst(fieldA: FieldDef, fieldB: FieldDef) {
|
||||
if (fieldA.links?.length && !fieldB.links?.length) {
|
||||
return -1;
|
||||
}
|
||||
if (!fieldA.links?.length && fieldB.links?.length) {
|
||||
return 1;
|
||||
}
|
||||
return fieldA.key > fieldB.key ? 1 : fieldA.key < fieldB.key ? -1 : 0;
|
||||
}
|
||||
|
||||
function shouldRemoveField(field: Field, index: number, row: LogRowModel) {
|
||||
// Remove field if it is:
|
||||
// "labels" field that is in Loki used to store all labels
|
||||
if (field.name === 'labels' && field.type === FieldType.other) {
|
||||
return true;
|
||||
}
|
||||
// "id" field which we use for react key
|
||||
if (field.name === 'id') {
|
||||
// id and tsNs are arbitrary added fields in the backend and should be hidden in the UI
|
||||
if (field.name === 'id' || field.name === 'tsNs') {
|
||||
return true;
|
||||
}
|
||||
// entry field which we are showing as the log message
|
||||
if (row.entryFieldIndex === index) {
|
||||
return true;
|
||||
}
|
||||
const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time);
|
||||
if (
|
||||
field.name === firstTimeField?.name &&
|
||||
field.type === FieldType.time &&
|
||||
field.values.get(0) === firstTimeField.values.get(0)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// hidden field
|
||||
if (field.config.custom?.hidden) {
|
||||
return true;
|
||||
|
Loading…
Reference in New Issue
Block a user