Logs: Clean up Explore meta information (#103801)
* LogLabels: add displayMax property * MetaInfoText: prevent label wrapping * LogsMetaRow: remove unescaped content and max characters * LogsMetaRow: use font weight normal * LogLabels: add button to collapse back * LogsMetaRow: set line height * LogsMetaRow: save common logs state * LogsMetaRow: update unit tests * LogLabels: update unit test * LogListContext: add forceEscape option * LogListContext: move forceEscape and hasUnescapedContents * LogListControls: move forceEscape to controls * LogList: read forceEscape from context * i18n * LogListControls: test new control * Update betterer * logsModel: update test * LogLabels: replace characters with icons * Prettier * LogLabels: add aria labels * i18n * Update tests * Update test
This commit is contained in:
@@ -209,7 +209,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
);
|
||||
const [isFlipping, setIsFlipping] = useState<boolean>(false);
|
||||
const [displayedFields, setDisplayedFields] = useState<string[]>([]);
|
||||
const [forceEscape, setForceEscape] = useState<boolean>(false);
|
||||
const [contextOpen, setContextOpen] = useState<boolean>(false);
|
||||
const [contextRow, setContextRow] = useState<LogRowModel | undefined>(undefined);
|
||||
const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState<PopoverContent>(PINNED_LOGS_MESSAGE);
|
||||
@@ -497,10 +496,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
[sortOrderChanged]
|
||||
);
|
||||
|
||||
const onEscapeNewlines = useCallback(() => {
|
||||
setForceEscape(!forceEscape);
|
||||
}, [forceEscape]);
|
||||
|
||||
const onChangeVisualisation = useCallback(
|
||||
(visualisation: LogsVisualisationType) => {
|
||||
setVisualisationType(visualisation);
|
||||
@@ -1000,10 +995,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
meta={logsMeta || []}
|
||||
dedupStrategy={dedupStrategy}
|
||||
dedupCount={dedupCount}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
forceEscape={forceEscape}
|
||||
displayedFields={displayedFields}
|
||||
onEscapeNewlines={onEscapeNewlines}
|
||||
clearDetectedFields={clearDetectedFields}
|
||||
/>
|
||||
</div>
|
||||
@@ -1051,7 +1043,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
enableLogDetails={true}
|
||||
forceEscape={forceEscape}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
@@ -1075,6 +1066,7 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
logsMeta={logsMeta}
|
||||
logOptionsStorageKey={SETTING_KEY_ROOT}
|
||||
onLogOptionsChange={onLogOptionsChange}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -1110,7 +1102,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
showLabels={showLabels}
|
||||
showTime={showTime}
|
||||
enableLogDetails={true}
|
||||
forceEscape={forceEscape}
|
||||
wrapLogMessage={wrapLogMessage}
|
||||
prettifyLogMessage={prettifyLogMessage}
|
||||
timeZone={timeZone}
|
||||
@@ -1159,7 +1150,6 @@ const UnthemedLogs: React.FunctionComponent<Props> = (props: Props) => {
|
||||
dedupStrategy={dedupStrategy}
|
||||
displayedFields={displayedFields}
|
||||
filterLevels={filterLevels}
|
||||
forceEscape={forceEscape}
|
||||
getFieldLinks={getFieldLinks}
|
||||
getRowContextQuery={getRowContextQuery}
|
||||
loadMore={loadMoreLogs}
|
||||
|
||||
@@ -3,11 +3,19 @@ import userEvent from '@testing-library/user-event';
|
||||
import saveAs from 'file-saver';
|
||||
import { ComponentProps } from 'react';
|
||||
|
||||
import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data';
|
||||
import {
|
||||
FieldType,
|
||||
LogLevel,
|
||||
LogsDedupStrategy,
|
||||
LogsMetaItem,
|
||||
LogsMetaKind,
|
||||
standardTransformersRegistry,
|
||||
store,
|
||||
toDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { organizeFieldsTransformer } from '@grafana/data/internal';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
|
||||
import { logRowsToReadableJson } from '../../logs/utils';
|
||||
import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields';
|
||||
|
||||
@@ -26,10 +34,7 @@ const defaultProps: LogsMetaRowProps = {
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
dedupCount: 0,
|
||||
displayedFields: [],
|
||||
hasUnescapedContent: false,
|
||||
forceEscape: false,
|
||||
logRows: [],
|
||||
onEscapeNewlines: jest.fn(),
|
||||
clearDetectedFields: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -49,13 +54,6 @@ describe('LogsMetaRow', () => {
|
||||
expect(await screen.findByText('1234')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a highlighting warning', async () => {
|
||||
setup({ logRows: [{ entry: 'A'.repeat(MAX_CHARACTERS + 1) }] });
|
||||
expect(
|
||||
await screen.findByText('Logs with more than 100,000 characters could not be parsed and highlighted')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the show original line button', () => {
|
||||
setup({ displayedFields: ['test'] });
|
||||
expect(
|
||||
@@ -65,7 +63,7 @@ describe('LogsMetaRow', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the displayedfield', async () => {
|
||||
it('renders the displayed fields', async () => {
|
||||
setup({ displayedFields: ['testField1234'] });
|
||||
expect(await screen.findByText('testField1234')).toBeInTheDocument();
|
||||
});
|
||||
@@ -85,39 +83,6 @@ describe('LogsMetaRow', () => {
|
||||
expect(clearSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('renders a button to remove escaping', () => {
|
||||
setup({ hasUnescapedContent: true, forceEscape: true });
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'Remove escaping',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a button to remove escaping', () => {
|
||||
setup({ hasUnescapedContent: true, forceEscape: false });
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'Escape newlines',
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a button to remove escaping', () => {
|
||||
const escapeSpy = jest.fn();
|
||||
setup({ hasUnescapedContent: true, forceEscape: false, onEscapeNewlines: escapeSpy });
|
||||
fireEvent(
|
||||
screen.getByRole('button', {
|
||||
name: 'Escape newlines',
|
||||
}),
|
||||
new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
expect(escapeSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('renders a button to show the download menu', () => {
|
||||
setup();
|
||||
expect(screen.getByText('Download').closest('button')).toBeInTheDocument();
|
||||
@@ -317,4 +282,62 @@ describe('LogsMetaRow', () => {
|
||||
const text = await blob.text();
|
||||
expect(text).toBe(`"Date","time","message bar"\r\n1970-01-01T00:00:00.001Z,1,INFO 1`);
|
||||
});
|
||||
|
||||
it('renders common labels', async () => {
|
||||
const meta: LogsMetaItem[] = [
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: {
|
||||
exporter: 'OTLP',
|
||||
job: 'cicd-o11y/grafana-deployment-tools',
|
||||
},
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
},
|
||||
];
|
||||
setup({ meta });
|
||||
expect(await screen.findByText(/Common labels/)).toBeInTheDocument();
|
||||
expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument();
|
||||
expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders collapsed common labels', async () => {
|
||||
const meta: LogsMetaItem[] = [
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: {
|
||||
exporter: 'OTLP',
|
||||
job: 'cicd-o11y/grafana-deployment-tools',
|
||||
service_name: 'grafana',
|
||||
service_namespace: 'cicd-o11y',
|
||||
},
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
},
|
||||
];
|
||||
setup({ meta });
|
||||
expect(await screen.findByText(/Common labels/)).toBeInTheDocument();
|
||||
expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument();
|
||||
expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('Expand labels')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders expanded common labels', async () => {
|
||||
jest.spyOn(store, 'getBool').mockReturnValue(true);
|
||||
const meta: LogsMetaItem[] = [
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: {
|
||||
exporter: 'OTLP',
|
||||
job: 'cicd-o11y/grafana-deployment-tools',
|
||||
service_name: 'grafana',
|
||||
service_namespace: 'cicd-o11y',
|
||||
},
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
},
|
||||
];
|
||||
setup({ meta });
|
||||
expect(await screen.findByText(/Common labels/)).toBeInTheDocument();
|
||||
expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument();
|
||||
expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('Collapse labels')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, Labels } from '@grafana/data';
|
||||
import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, Labels, store } from '@grafana/data';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
import { LogLabels, LogLabelsList } from '../../logs/components/LogLabels';
|
||||
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
|
||||
import { LogLabels, LogLabelsList, Props as LogLabelsProps } from '../../logs/components/LogLabels';
|
||||
import { DownloadFormat, downloadLogs } from '../../logs/utils';
|
||||
import { MetaInfoText, MetaItemProps } from '../MetaInfoText';
|
||||
|
||||
import { SETTINGS_KEYS } from './utils/logs';
|
||||
|
||||
const getStyles = () => ({
|
||||
metaContainer: css({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& span': {
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.25em',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -24,25 +29,12 @@ export type Props = {
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
dedupCount: number;
|
||||
displayedFields: string[];
|
||||
hasUnescapedContent: boolean;
|
||||
forceEscape: boolean;
|
||||
logRows: LogRowModel[];
|
||||
onEscapeNewlines: () => void;
|
||||
clearDetectedFields: () => void;
|
||||
};
|
||||
|
||||
export const LogsMetaRow = memo(
|
||||
({
|
||||
meta,
|
||||
dedupStrategy,
|
||||
dedupCount,
|
||||
displayedFields,
|
||||
clearDetectedFields,
|
||||
hasUnescapedContent,
|
||||
forceEscape,
|
||||
onEscapeNewlines,
|
||||
logRows,
|
||||
}: Props) => {
|
||||
({ meta, dedupStrategy, dedupCount, displayedFields, clearDetectedFields, logRows }: Props) => {
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const logsMetaItem: Array<LogsMetaItem | MetaItemProps> = [...meta];
|
||||
@@ -55,14 +47,6 @@ export const LogsMetaRow = memo(
|
||||
kind: LogsMetaKind.Number,
|
||||
});
|
||||
}
|
||||
// Add info about limit for highlighting
|
||||
if (logRows.some((r) => r.entry.length > MAX_CHARACTERS)) {
|
||||
logsMetaItem.push({
|
||||
label: 'Info',
|
||||
value: 'Logs with more than 100,000 characters could not be parsed and highlighted',
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
}
|
||||
|
||||
// Add detected fields info
|
||||
if (displayedFields?.length > 0) {
|
||||
@@ -91,22 +75,6 @@ export const LogsMetaRow = memo(
|
||||
downloadLogs(format, logRows, meta);
|
||||
}
|
||||
|
||||
// Add unescaped content info
|
||||
if (hasUnescapedContent) {
|
||||
logsMetaItem.push({
|
||||
label: 'Your logs might have incorrectly escaped content',
|
||||
value: (
|
||||
<Tooltip
|
||||
content="Fix incorrectly escaped newline and tab sequences in log lines. Manually review the results to confirm that the replacements are correct."
|
||||
placement="right"
|
||||
>
|
||||
<Button variant="secondary" size="sm" onClick={onEscapeNewlines}>
|
||||
{forceEscape ? 'Remove escaping' : 'Escape newlines'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
});
|
||||
}
|
||||
const downloadMenu = (
|
||||
<Menu>
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
@@ -117,6 +85,17 @@ export const LogsMetaRow = memo(
|
||||
<Menu.Item label="csv" onClick={() => download(DownloadFormat.CSV)} />
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const onCommonLabelsToggle = (state: boolean) => {
|
||||
store.set(SETTINGS_KEYS.commonLabels, state);
|
||||
};
|
||||
|
||||
const commonLabelsProps = {
|
||||
onDisplayMaxToggle: onCommonLabelsToggle,
|
||||
displayMax: 3,
|
||||
displayAll: store.getBool(SETTINGS_KEYS.commonLabels, false),
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{logsMetaItem && (
|
||||
@@ -125,7 +104,7 @@ export const LogsMetaRow = memo(
|
||||
metaItems={logsMetaItem.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
value: 'kind' in item ? renderMetaItem(item.value, item.kind) : item.value,
|
||||
value: 'kind' in item ? renderMetaItem(item.value, item.kind, commonLabelsProps) : item.value,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
@@ -145,12 +124,12 @@ export const LogsMetaRow = memo(
|
||||
|
||||
LogsMetaRow.displayName = 'LogsMetaRow';
|
||||
|
||||
function renderMetaItem(value: string | number | Labels, kind: LogsMetaKind) {
|
||||
function renderMetaItem(value: string | number | Labels, kind: LogsMetaKind, logLabelsProps: Partial<LogLabelsProps>) {
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return <>{value}</>;
|
||||
}
|
||||
if (kind === LogsMetaKind.LabelsMap) {
|
||||
return <LogLabels labels={value} />;
|
||||
return <LogLabels labels={value} {...logLabelsProps} />;
|
||||
}
|
||||
if (kind === LogsMetaKind.Error) {
|
||||
return <span className="logs-meta-item__error">{value.toString()}</span>;
|
||||
|
||||
@@ -5,6 +5,7 @@ export const SETTINGS_KEYS = {
|
||||
prettifyLogMessage: 'grafana.explore.logs.prettifyLogMessage',
|
||||
logsSortOrder: 'grafana.explore.logs.sortOrder',
|
||||
logContextWrapLogMessage: 'grafana.explore.logs.logContext.wrapLogMessage',
|
||||
commonLabels: 'grafana.explore.logs.commonLabels',
|
||||
};
|
||||
|
||||
export const SETTING_KEY_ROOT = 'grafana.explore.logs';
|
||||
|
||||
@@ -26,6 +26,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
marginRight: `calc(${theme.spacing(2)} / 2)`,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
whiteSpace: 'nowrap',
|
||||
}),
|
||||
metaValue: css({
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { LogListControls } from './panel/LogListControls';
|
||||
import { ScrollToLogsEvent } from './panel/virtualization';
|
||||
|
||||
export interface ControlledLogRowsProps extends Omit<Props, 'scrollElement'> {
|
||||
hasUnescapedContent?: boolean;
|
||||
loading: boolean;
|
||||
logsMeta?: LogsMetaItem[];
|
||||
loadMoreLogs?: (range: AbsoluteTimeRange) => void;
|
||||
@@ -50,6 +51,7 @@ export type LogRowsComponentProps = Omit<
|
||||
export const ControlledLogRows = ({
|
||||
deduplicatedRows,
|
||||
dedupStrategy,
|
||||
hasUnescapedContent,
|
||||
showLabels,
|
||||
showTime,
|
||||
logsMeta,
|
||||
@@ -65,6 +67,7 @@ export const ControlledLogRows = ({
|
||||
app={rest.app || CoreApp.Unknown}
|
||||
displayedFields={[]}
|
||||
dedupStrategy={dedupStrategy}
|
||||
hasUnescapedContent={hasUnescapedContent}
|
||||
logOptionsStorageKey={logOptionsStorageKey}
|
||||
logs={deduplicatedRows ?? []}
|
||||
logsMeta={logsMeta}
|
||||
@@ -83,8 +86,17 @@ export const ControlledLogRows = ({
|
||||
};
|
||||
|
||||
const LogRowsComponent = ({ loading, loadMoreLogs, deduplicatedRows = [], range, ...rest }: LogRowsComponentProps) => {
|
||||
const { app, dedupStrategy, filterLevels, prettifyJSON, sortOrder, showTime, showUniqueLabels, wrapLogMessage } =
|
||||
useLogListContext();
|
||||
const {
|
||||
app,
|
||||
dedupStrategy,
|
||||
filterLevels,
|
||||
forceEscape,
|
||||
prettifyJSON,
|
||||
sortOrder,
|
||||
showTime,
|
||||
showUniqueLabels,
|
||||
wrapLogMessage,
|
||||
} = useLogListContext();
|
||||
const eventBus = useMemo(() => new EventBusSrv(), []);
|
||||
const scrollElementRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -124,6 +136,7 @@ const LogRowsComponent = ({ loading, loadMoreLogs, deduplicatedRows = [], range,
|
||||
app={app}
|
||||
dedupStrategy={dedupStrategy}
|
||||
deduplicatedRows={filteredLogs}
|
||||
forceEscape={forceEscape}
|
||||
logRows={filteredLogs}
|
||||
logsSortOrder={sortOrder}
|
||||
scrollElement={scrollElementRef.current}
|
||||
|
||||
@@ -35,6 +35,60 @@ describe('<LogLabels />', () => {
|
||||
await userEvent.hover(screen.getByText('foo=bar'));
|
||||
expect(screen.getAllByText('foo=bar')).toHaveLength(1);
|
||||
});
|
||||
describe('displayMax', () => {
|
||||
it('renders up to displayMax labels', () => {
|
||||
render(<LogLabels labels={{ foo: 'bar', baz: '42' }} displayMax={1} />);
|
||||
expect(screen.getByText('foo=bar')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Expand labels')).toBeInTheDocument();
|
||||
expect(screen.queryByText('baz=42')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows to render all labels', async () => {
|
||||
const onDisplayMaxToggle = jest.fn();
|
||||
const { rerender } = render(
|
||||
<LogLabels labels={{ foo: 'bar', baz: '42' }} displayMax={1} onDisplayMaxToggle={onDisplayMaxToggle} />
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Expand labels'));
|
||||
expect(onDisplayMaxToggle).toHaveBeenCalledTimes(1);
|
||||
expect(onDisplayMaxToggle).toHaveBeenCalledWith(true);
|
||||
|
||||
rerender(<LogLabels labels={{ foo: 'bar', baz: '42' }} displayMax={1} onDisplayMaxToggle={onDisplayMaxToggle} />);
|
||||
|
||||
expect(screen.getByText('foo=bar')).toBeInTheDocument();
|
||||
expect(screen.getByText('baz=42')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Collapse labels')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows to collapse labels', async () => {
|
||||
const onDisplayMaxToggle = jest.fn();
|
||||
const { rerender } = render(
|
||||
<LogLabels
|
||||
labels={{ foo: 'bar', baz: '42' }}
|
||||
displayMax={1}
|
||||
displayAll
|
||||
onDisplayMaxToggle={onDisplayMaxToggle}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Collapse labels'));
|
||||
expect(onDisplayMaxToggle).toHaveBeenCalledTimes(1);
|
||||
expect(onDisplayMaxToggle).toHaveBeenCalledWith(false);
|
||||
|
||||
rerender(
|
||||
<LogLabels
|
||||
labels={{ foo: 'bar', baz: '42' }}
|
||||
displayMax={1}
|
||||
displayAll
|
||||
onDisplayMaxToggle={onDisplayMaxToggle}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('foo=bar')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Expand labels')).toBeInTheDocument();
|
||||
expect(screen.queryByText('baz=42')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('<LogLabelsList />', () => {
|
||||
|
||||
@@ -1,54 +1,101 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { memo, forwardRef, useMemo } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, forwardRef, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, Labels } from '@grafana/data';
|
||||
import { Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { Button, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody';
|
||||
|
||||
// Levels are already encoded in color, filename is a Loki-ism
|
||||
const HIDDEN_LABELS = ['detected_level', 'level', 'lvl', 'filename'];
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
labels: Labels;
|
||||
emptyMessage?: string;
|
||||
addTooltip?: boolean;
|
||||
displayMax?: number;
|
||||
displayAll?: boolean;
|
||||
onDisplayMaxToggle?(state: boolean): void;
|
||||
}
|
||||
|
||||
export const LogLabels = memo(({ labels, emptyMessage, addTooltip = true }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const displayLabels = useMemo(
|
||||
() =>
|
||||
Object.keys(labels)
|
||||
.filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label) && labels[label])
|
||||
.map((label) => `${label}=${labels[label]}`),
|
||||
[labels]
|
||||
);
|
||||
export const LogLabels = memo(
|
||||
({
|
||||
labels,
|
||||
emptyMessage,
|
||||
addTooltip = true,
|
||||
displayMax,
|
||||
onDisplayMaxToggle,
|
||||
displayAll: initialDisplayAll = false,
|
||||
}: Props) => {
|
||||
const [displayAll, setDisplayAll] = useState<boolean | undefined>(displayMax ? initialDisplayAll : undefined);
|
||||
const styles = useStyles2(getStyles);
|
||||
const allLabels = useMemo(
|
||||
() =>
|
||||
Object.keys(labels)
|
||||
.filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label) && labels[label])
|
||||
.map((label) => `${label}=${labels[label]}`),
|
||||
[labels]
|
||||
);
|
||||
const displayLabels = useMemo(
|
||||
() => allLabels.slice(0, !displayAll && displayMax ? displayMax : Infinity),
|
||||
[allLabels, displayAll, displayMax]
|
||||
);
|
||||
|
||||
if (displayLabels.length === 0 && emptyMessage) {
|
||||
return (
|
||||
<span className={styles.logsLabels}>
|
||||
<span className={styles.logsLabel}>{emptyMessage}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (displayLabels.length === 0 && emptyMessage) {
|
||||
return (
|
||||
<span className={cx([styles.logsLabels])}>
|
||||
<span className={cx([styles.logsLabel])}>{emptyMessage}</span>
|
||||
<span className={styles.logsLabels}>
|
||||
{displayLabels.map((labelValue) => {
|
||||
return addTooltip ? (
|
||||
<Tooltip content={labelValue} key={labelValue} placement="top">
|
||||
<LogLabel styles={styles}>{labelValue}</LogLabel>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<LogLabel styles={styles} tooltip={labelValue} key={labelValue}>
|
||||
{labelValue}
|
||||
</LogLabel>
|
||||
);
|
||||
})}
|
||||
{displayLabels.length < allLabels.length && !displayAll && (
|
||||
<Button
|
||||
size="sm"
|
||||
fill="outline"
|
||||
variant="secondary"
|
||||
aria-label={t('logs.log-labels.expand', 'Expand labels')}
|
||||
onClick={() => {
|
||||
setDisplayAll(true);
|
||||
onDisplayMaxToggle?.(true);
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" size="xs" />
|
||||
{allLabels.length - displayLabels.length}
|
||||
</Button>
|
||||
)}
|
||||
{displayAll === true && (
|
||||
<Button
|
||||
size="sm"
|
||||
fill="outline"
|
||||
variant="secondary"
|
||||
aria-label={t('logs.log-labels.collapse', 'Collapse labels')}
|
||||
onClick={() => {
|
||||
setDisplayAll(false);
|
||||
onDisplayMaxToggle?.(false);
|
||||
}}
|
||||
>
|
||||
<Icon name="minus" size="xs" />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={cx([styles.logsLabels])}>
|
||||
{displayLabels.map((labelValue) => {
|
||||
return addTooltip ? (
|
||||
<Tooltip content={labelValue} key={labelValue} placement="top">
|
||||
<LogLabel styles={styles}>{labelValue}</LogLabel>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<LogLabel styles={styles} tooltip={labelValue} key={labelValue}>
|
||||
{labelValue}
|
||||
</LogLabel>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
);
|
||||
LogLabels.displayName = 'LogLabels';
|
||||
|
||||
interface LogLabelsArrayProps {
|
||||
@@ -58,7 +105,7 @@ interface LogLabelsArrayProps {
|
||||
export const LogLabelsList = memo(({ labels }: LogLabelsArrayProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return (
|
||||
<span className={cx([styles.logsLabels])}>
|
||||
<span className={styles.logsLabels}>
|
||||
{labels.map((label) => (
|
||||
<LogLabel key={label} styles={styles} tooltip={label}>
|
||||
{label === LOG_LINE_BODY_FIELD_NAME ? 'log line' : label}
|
||||
@@ -77,8 +124,8 @@ interface LogLabelProps {
|
||||
|
||||
const LogLabel = forwardRef<HTMLSpanElement, LogLabelProps>(({ styles, tooltip, children }: LogLabelProps, ref) => {
|
||||
return (
|
||||
<span className={cx([styles.logsLabel])} ref={ref}>
|
||||
<span className={cx([styles.logsLabelValue])} title={tooltip}>
|
||||
<span className={styles.logsLabel} ref={ref}>
|
||||
<span className={styles.logsLabelValue} title={tooltip}>
|
||||
{children}
|
||||
</span>
|
||||
</span>
|
||||
@@ -92,6 +139,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
fontSize: theme.typography.size.xs,
|
||||
alignItems: 'center',
|
||||
}),
|
||||
logsLabel: css({
|
||||
label: 'logs-label',
|
||||
@@ -103,6 +151,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
maxHeight: theme.spacing(2),
|
||||
}),
|
||||
logsLabelValue: css({
|
||||
label: 'logs-label__value',
|
||||
|
||||
@@ -47,7 +47,6 @@ interface Props {
|
||||
displayedFields: string[];
|
||||
eventBus?: EventBus;
|
||||
filterLevels?: LogLevel[];
|
||||
forceEscape?: boolean;
|
||||
getFieldLinks?: GetFieldLinksFn;
|
||||
getRowContextQuery?: GetRowContextQueryFn;
|
||||
grammar?: Grammar;
|
||||
@@ -89,7 +88,6 @@ export const LogList = ({
|
||||
dedupStrategy,
|
||||
eventBus,
|
||||
filterLevels,
|
||||
forceEscape = false,
|
||||
getFieldLinks,
|
||||
getRowContextQuery,
|
||||
grammar,
|
||||
@@ -143,7 +141,6 @@ export const LogList = ({
|
||||
<LogListComponent
|
||||
containerElement={containerElement}
|
||||
eventBus={eventBus}
|
||||
forceEscape={forceEscape}
|
||||
getFieldLinks={getFieldLinks}
|
||||
grammar={grammar}
|
||||
initialScrollPosition={initialScrollPosition}
|
||||
@@ -160,7 +157,6 @@ export const LogList = ({
|
||||
const LogListComponent = ({
|
||||
containerElement,
|
||||
eventBus = new EventBusSrv(),
|
||||
forceEscape = false,
|
||||
getFieldLinks,
|
||||
grammar,
|
||||
initialScrollPosition = 'top',
|
||||
@@ -170,7 +166,7 @@ const LogListComponent = ({
|
||||
timeRange,
|
||||
timeZone,
|
||||
}: LogListComponentProps) => {
|
||||
const { app, displayedFields, filterLevels, showTime, sortOrder, wrapLogMessage } = useLogListContext();
|
||||
const { app, displayedFields, filterLevels, forceEscape, showTime, sortOrder, wrapLogMessage } = useLogListContext();
|
||||
const [processedLogs, setProcessedLogs] = useState<LogListModel[]>([]);
|
||||
const [listHeight, setListHeight] = useState(
|
||||
app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight
|
||||
@@ -197,7 +193,9 @@ const LogListComponent = ({
|
||||
}, [eventBus, logs.length]);
|
||||
|
||||
useEffect(() => {
|
||||
setProcessedLogs(preProcessLogs(logs, { getFieldLinks, escape: forceEscape, order: sortOrder, timeZone }, grammar));
|
||||
setProcessedLogs(
|
||||
preProcessLogs(logs, { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone }, grammar)
|
||||
);
|
||||
}, [forceEscape, getFieldLinks, grammar, logs, sortOrder, timeZone]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -28,9 +28,11 @@ import { GetRowContextQueryFn } from './LogLineMenu';
|
||||
export interface LogListContextData extends Omit<Props, 'logs' | 'logsMeta' | 'showControls'> {
|
||||
downloadLogs: (format: DownloadFormat) => void;
|
||||
filterLevels: LogLevel[];
|
||||
hasUnescapedContent?: boolean;
|
||||
setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void;
|
||||
setDisplayedFields: (displayedFields: string[]) => void;
|
||||
setFilterLevels: (filterLevels: LogLevel[]) => void;
|
||||
setForceEscape: (forceEscape: boolean) => void;
|
||||
setLogListState: Dispatch<SetStateAction<LogListState>>;
|
||||
setPinnedLogs: (pinnedlogs: string[]) => void;
|
||||
setPrettifyJSON: (prettifyJSON: boolean) => void;
|
||||
@@ -47,9 +49,11 @@ export const LogListContext = createContext<LogListContextData>({
|
||||
displayedFields: [],
|
||||
downloadLogs: () => {},
|
||||
filterLevels: [],
|
||||
hasUnescapedContent: false,
|
||||
setDedupStrategy: () => {},
|
||||
setDisplayedFields: () => {},
|
||||
setFilterLevels: () => {},
|
||||
setForceEscape: () => {},
|
||||
setLogListState: () => {},
|
||||
setPinnedLogs: () => {},
|
||||
setPrettifyJSON: () => {},
|
||||
@@ -82,7 +86,9 @@ export type LogListState = Pick<
|
||||
LogListContextData,
|
||||
| 'dedupStrategy'
|
||||
| 'displayedFields'
|
||||
| 'forceEscape'
|
||||
| 'filterLevels'
|
||||
| 'hasUnescapedContent'
|
||||
| 'pinnedLogs'
|
||||
| 'prettifyJSON'
|
||||
| 'showUniqueLabels'
|
||||
@@ -98,6 +104,8 @@ export interface Props {
|
||||
dedupStrategy: LogsDedupStrategy;
|
||||
displayedFields: string[];
|
||||
filterLevels?: LogLevel[];
|
||||
forceEscape?: boolean;
|
||||
hasUnescapedContent?: boolean;
|
||||
getRowContextQuery?: GetRowContextQueryFn;
|
||||
logs: LogRowModel[];
|
||||
logsMeta?: LogsMetaItem[];
|
||||
@@ -125,11 +133,13 @@ export const LogListContextProvider = ({
|
||||
children,
|
||||
dedupStrategy,
|
||||
displayedFields,
|
||||
filterLevels,
|
||||
forceEscape = false,
|
||||
hasUnescapedContent,
|
||||
getRowContextQuery,
|
||||
logs,
|
||||
logsMeta,
|
||||
logOptionsStorageKey,
|
||||
filterLevels,
|
||||
logSupportsContext,
|
||||
onLogOptionsChange,
|
||||
onLogLineHover,
|
||||
@@ -152,6 +162,8 @@ export const LogListContextProvider = ({
|
||||
displayedFields,
|
||||
filterLevels:
|
||||
filterLevels ?? (logOptionsStorageKey ? store.getObject(`${logOptionsStorageKey}.filterLevels`, []) : []),
|
||||
forceEscape,
|
||||
hasUnescapedContent,
|
||||
pinnedLogs,
|
||||
prettifyJSON,
|
||||
showTime,
|
||||
@@ -205,6 +217,12 @@ export const LogListContextProvider = ({
|
||||
}
|
||||
}, [filterLevels, logListState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (logListState.hasUnescapedContent !== hasUnescapedContent) {
|
||||
setLogListState({ ...logListState, hasUnescapedContent });
|
||||
}
|
||||
}, [hasUnescapedContent, logListState]);
|
||||
|
||||
const setDedupStrategy = useCallback(
|
||||
(dedupStrategy: LogsDedupStrategy) => {
|
||||
setLogListState({ ...logListState, dedupStrategy });
|
||||
@@ -221,6 +239,13 @@ export const LogListContextProvider = ({
|
||||
[logListState, onLogOptionsChange]
|
||||
);
|
||||
|
||||
const setForceEscape = useCallback(
|
||||
(forceEscape: boolean) => {
|
||||
setLogListState({ ...logListState, forceEscape });
|
||||
},
|
||||
[logListState]
|
||||
);
|
||||
|
||||
const setFilterLevels = useCallback(
|
||||
(filterLevels: LogLevel[]) => {
|
||||
setLogListState({ ...logListState, filterLevels });
|
||||
@@ -322,6 +347,8 @@ export const LogListContextProvider = ({
|
||||
displayedFields: logListState.displayedFields,
|
||||
downloadLogs,
|
||||
filterLevels: logListState.filterLevels,
|
||||
forceEscape: logListState.forceEscape,
|
||||
hasUnescapedContent: logListState.hasUnescapedContent,
|
||||
getRowContextQuery,
|
||||
logSupportsContext,
|
||||
onLogLineHover,
|
||||
@@ -335,6 +362,7 @@ export const LogListContextProvider = ({
|
||||
setDedupStrategy,
|
||||
setDisplayedFields,
|
||||
setFilterLevels,
|
||||
setForceEscape,
|
||||
setLogListState,
|
||||
setPinnedLogs,
|
||||
setPrettifyJSON,
|
||||
|
||||
@@ -41,6 +41,10 @@ describe('LogListControls', () => {
|
||||
expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Renders legacy controls', () => {
|
||||
@@ -242,4 +246,19 @@ describe('LogListControls', () => {
|
||||
await userEvent.click(await screen.findByText('txt'));
|
||||
expect(downloadLogs).toHaveBeenCalledWith('text', filteredLogs, undefined);
|
||||
});
|
||||
|
||||
test('Controls new lines', async () => {
|
||||
const { rerender } = render(
|
||||
<LogListContextProvider {...contextProps} hasUnescapedContent>
|
||||
<LogListControls eventBus={new EventBusSrv()} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText('Fix incorrectly escaped newline and tab sequences in log lines'));
|
||||
rerender(
|
||||
<LogListContextProvider {...contextProps} hasUnescapedContent>
|
||||
<LogListControls eventBus={new EventBusSrv()} />
|
||||
</LogListContextProvider>
|
||||
);
|
||||
await userEvent.click(screen.getByLabelText('Remove escaping'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,9 +42,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
|
||||
dedupStrategy,
|
||||
downloadLogs,
|
||||
filterLevels,
|
||||
forceEscape,
|
||||
hasUnescapedContent,
|
||||
prettifyJSON,
|
||||
setDedupStrategy,
|
||||
setFilterLevels,
|
||||
setForceEscape,
|
||||
setPrettifyJSON,
|
||||
setShowTime,
|
||||
setShowUniqueLabels,
|
||||
@@ -76,6 +79,11 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
|
||||
);
|
||||
}, [eventBus]);
|
||||
|
||||
const onForceEscapeClick = useCallback(() => {
|
||||
reportInteraction('logs_log_list_controls_force_escape_clicked');
|
||||
setForceEscape(!forceEscape);
|
||||
}, [forceEscape, setForceEscape]);
|
||||
|
||||
const onFilterLevelClick = useCallback(
|
||||
(level?: LogLevel) => {
|
||||
reportInteraction('logs_log_list_controls_level_clicked');
|
||||
@@ -310,6 +318,23 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props)
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
{hasUnescapedContent && (
|
||||
<IconButton
|
||||
name="enter"
|
||||
aria-pressed={forceEscape}
|
||||
className={forceEscape ? styles.controlButtonActive : styles.controlButton}
|
||||
onClick={onForceEscapeClick}
|
||||
tooltip={
|
||||
forceEscape
|
||||
? t('logs.logs-controls.remove-escaping', 'Remove escaping')
|
||||
: t(
|
||||
'logs.logs-controls.escape-newlines',
|
||||
'Fix incorrectly escaped newline and tab sequences in log lines'
|
||||
)
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!config.exploreHideLogsDownload && (
|
||||
|
||||
@@ -13,6 +13,7 @@ export const LogListContext = createContext<LogListContextData>({
|
||||
setDedupStrategy: () => {},
|
||||
setDisplayedFields: () => {},
|
||||
setFilterLevels: () => {},
|
||||
setForceEscape: () => {},
|
||||
setLogListState: () => {},
|
||||
setPinnedLogs: () => {},
|
||||
setPrettifyJSON: () => {},
|
||||
@@ -57,6 +58,7 @@ export const defaultProps = {
|
||||
setDedupStrategy: jest.fn(),
|
||||
setDisplayedFields: jest.fn(),
|
||||
setFilterLevels: jest.fn(),
|
||||
setForceEscape: jest.fn(),
|
||||
setLogListState: jest.fn(),
|
||||
setPinnedLogs: jest.fn(),
|
||||
setShowTime: jest.fn(),
|
||||
@@ -108,6 +110,7 @@ export const LogListContextProvider = ({
|
||||
setDedupStrategy: jest.fn(),
|
||||
setDisplayedFields: jest.fn(),
|
||||
setFilterLevels: jest.fn(),
|
||||
setForceEscape: jest.fn(),
|
||||
setLogListState: jest.fn(),
|
||||
setPinnedLogs: jest.fn(),
|
||||
setPrettifyJSON: jest.fn(),
|
||||
|
||||
@@ -348,6 +348,11 @@ describe('dataFrameToLogsModel', () => {
|
||||
]);
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: {
|
||||
filename: '/var/log/grafana/grafana.log',
|
||||
@@ -355,11 +360,6 @@ describe('dataFrameToLogsModel', () => {
|
||||
},
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
});
|
||||
|
||||
it('given one series should return expected logs model with detected_level', () => {
|
||||
@@ -430,17 +430,17 @@ describe('dataFrameToLogsModel', () => {
|
||||
]);
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: {
|
||||
job: 'grafana',
|
||||
},
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
});
|
||||
|
||||
it('with infinite scrolling enabled it should return expected logs model', () => {
|
||||
@@ -485,7 +485,7 @@ describe('dataFrameToLogsModel', () => {
|
||||
it('given one series with limit as custom meta property should return correct limit', () => {
|
||||
const series: DataFrame[] = getTestDataFrame();
|
||||
const logsModel = dataFrameToLogsModel(series, 1);
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
@@ -516,16 +516,16 @@ describe('dataFrameToLogsModel', () => {
|
||||
queries
|
||||
);
|
||||
expect(logsModel.meta).toEqual([
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: 2,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: '2 lines shown — 21.85% (5h 14min 40sec) of 24h',
|
||||
kind: 1,
|
||||
},
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -554,16 +554,16 @@ describe('dataFrameToLogsModel', () => {
|
||||
queries
|
||||
);
|
||||
expect(logsModel.meta).toEqual([
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: 2,
|
||||
},
|
||||
{
|
||||
label: '',
|
||||
value: '2 reached',
|
||||
kind: 1,
|
||||
},
|
||||
{
|
||||
label: 'Common labels',
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: 2,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -649,15 +649,15 @@ describe('dataFrameToLogsModel', () => {
|
||||
]);
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
});
|
||||
|
||||
it('given one series with labels-field it should work regardless the label-fields position', () => {
|
||||
@@ -768,20 +768,20 @@ describe('dataFrameToLogsModel', () => {
|
||||
expect(logsModel.series).toHaveLength(2);
|
||||
expect(logsModel.meta).toHaveLength(3);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: series[0].fields[1].labels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines returned`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![2]).toMatchObject({
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: 'Error when parsing some of the logs',
|
||||
kind: LogsMetaKind.Error,
|
||||
});
|
||||
expect(logsModel.meta![2]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: series[0].fields[1].labels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
});
|
||||
|
||||
it('given one series without labels should return expected logs model', () => {
|
||||
@@ -1110,15 +1110,15 @@ describe('dataFrameToLogsModel', () => {
|
||||
const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 });
|
||||
expect(logsModel.meta).toHaveLength(2);
|
||||
expect(logsModel.meta![0]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: series[0].fields[1].labels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: '',
|
||||
value: `2 lines shown — 98.44% (5h 14min 40sec) of 5h 19min 40sec`,
|
||||
kind: LogsMetaKind.String,
|
||||
});
|
||||
expect(logsModel.meta![1]).toMatchObject({
|
||||
label: COMMON_LABELS,
|
||||
value: series[0].fields[1].labels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to row index if no id', () => {
|
||||
@@ -1271,9 +1271,9 @@ describe('logSeriesToLogsModel', () => {
|
||||
|
||||
const logsModel = dataFrameToLogsModel(logSeries, 0);
|
||||
expect(logsModel.meta).toMatchObject([
|
||||
{ kind: 2, label: COMMON_LABELS, value: { foo: 'bar', level: 'dbug' } },
|
||||
{ kind: 0, label: LIMIT_LABEL, value: 2000 },
|
||||
{ kind: 1, label: 'Total bytes processed', value: '194 kB' },
|
||||
{ kind: 2, label: COMMON_LABELS, value: { foo: 'bar', level: 'dbug' } },
|
||||
]);
|
||||
expect(logsModel.rows).toHaveLength(3);
|
||||
expect(logsModel.rows).toMatchObject([
|
||||
|
||||
@@ -473,13 +473,6 @@ export function logSeriesToLogsModel(
|
||||
|
||||
// Meta data to display in status
|
||||
const meta: LogsMetaItem[] = [];
|
||||
if (size(commonLabels) > 0) {
|
||||
meta.push({
|
||||
label: COMMON_LABELS,
|
||||
value: commonLabels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
}
|
||||
// Data sources that set up searchWords on backend use meta.custom.limit.
|
||||
// Data sources that set up searchWords through frontend can use meta.limit.
|
||||
const limits = logSeries.filter((series) => series?.meta?.custom?.limit ?? series?.meta?.limit);
|
||||
@@ -538,6 +531,14 @@ export function logSeriesToLogsModel(
|
||||
});
|
||||
}
|
||||
|
||||
if (size(commonLabels) > 0) {
|
||||
meta.push({
|
||||
label: COMMON_LABELS,
|
||||
value: commonLabels,
|
||||
kind: LogsMetaKind.LabelsMap,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
hasUniqueLabels,
|
||||
meta,
|
||||
|
||||
@@ -5269,6 +5269,10 @@
|
||||
"tooltip-show-log-line": "Show log line"
|
||||
}
|
||||
},
|
||||
"log-labels": {
|
||||
"collapse": "Collapse labels",
|
||||
"expand": "Expand labels"
|
||||
},
|
||||
"log-line-menu": {
|
||||
"copy-link": "Copy link to log line",
|
||||
"copy-log": "Copy log line",
|
||||
@@ -5321,11 +5325,13 @@
|
||||
"txt": "txt"
|
||||
},
|
||||
"enable-highlighting": "Enable highlighting",
|
||||
"escape-newlines": "Fix incorrectly escaped newline and tab sequences in log lines",
|
||||
"hide-timestamps": "Hide timestamps",
|
||||
"hide-unique-labels": "Hide unique labels",
|
||||
"newest-first": "Newest logs first",
|
||||
"oldest-first": "Oldest logs first",
|
||||
"prettify-json": "Expand JSON logs",
|
||||
"remove-escaping": "Remove escaping",
|
||||
"scroll-bottom": "Scroll to bottom",
|
||||
"scroll-top": "Scroll to top",
|
||||
"show-timestamps": "Show timestamps",
|
||||
|
||||
Reference in New Issue
Block a user