mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:38:30 -06:00
Logs Panel: Add CSV to download options (#82480)
* add CSV download to logs panel
This commit is contained in:
parent
0f47a6fa10
commit
d071f4170d
@ -3,10 +3,12 @@ import userEvent from '@testing-library/user-event';
|
||||
import saveAs from 'file-saver';
|
||||
import React, { ComponentProps } from 'react';
|
||||
|
||||
import { FieldType, LogLevel, LogsDedupStrategy, toDataFrame } from '@grafana/data';
|
||||
import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data';
|
||||
import { organizeFieldsTransformer } from '@grafana/data/src/transformations/transformers/organize';
|
||||
|
||||
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
|
||||
import { logRowsToReadableJson } from '../../logs/utils';
|
||||
import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields';
|
||||
|
||||
import { LogsMetaRow } from './LogsMetaRow';
|
||||
|
||||
@ -200,4 +202,112 @@ describe('LogsMetaRow', () => {
|
||||
const text = await blob.text();
|
||||
expect(text).toBe(JSON.stringify(logRowsToReadableJson(rows)));
|
||||
});
|
||||
|
||||
it('renders a button to download CSV', async () => {
|
||||
const transformers = [extractFieldsTransformer, organizeFieldsTransformer];
|
||||
standardTransformersRegistry.setInit(() => {
|
||||
return transformers.map((t) => {
|
||||
return {
|
||||
id: t.id,
|
||||
aliasIds: t.aliasIds,
|
||||
name: t.name,
|
||||
transformation: t,
|
||||
description: t.description,
|
||||
editor: () => null,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const rows = [
|
||||
{
|
||||
rowIndex: 1,
|
||||
entryFieldIndex: 0,
|
||||
dataFrame: toDataFrame({
|
||||
name: 'logs',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: ['1970-01-01T00:00:00Z'],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FieldType.string,
|
||||
values: ['INFO 1'],
|
||||
labels: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
entry: 'test entry',
|
||||
hasAnsi: false,
|
||||
hasUnescapedContent: false,
|
||||
labels: {
|
||||
foo: 'bar',
|
||||
},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 10,
|
||||
timeEpochNs: '123456789',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '2',
|
||||
},
|
||||
{
|
||||
rowIndex: 2,
|
||||
entryFieldIndex: 1,
|
||||
dataFrame: toDataFrame({
|
||||
name: 'logs',
|
||||
refId: 'B',
|
||||
fields: [
|
||||
{
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: ['1970-01-02T00:00:00Z'],
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: FieldType.string,
|
||||
values: ['INFO 1'],
|
||||
labels: {
|
||||
foo: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
entry: 'test entry',
|
||||
hasAnsi: false,
|
||||
hasUnescapedContent: false,
|
||||
labels: {
|
||||
foo: 'bar',
|
||||
},
|
||||
logLevel: LogLevel.info,
|
||||
raw: '',
|
||||
timeEpochMs: 10,
|
||||
timeEpochNs: '123456789',
|
||||
timeFromNow: '',
|
||||
timeLocal: '',
|
||||
timeUtc: '',
|
||||
uid: '2',
|
||||
},
|
||||
];
|
||||
setup({ logRows: rows });
|
||||
|
||||
await userEvent.click(screen.getByText('Download').closest('button')!);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('menuitem', {
|
||||
name: 'csv',
|
||||
})
|
||||
);
|
||||
expect(saveAs).toBeCalled();
|
||||
|
||||
const blob = (saveAs as unknown as jest.Mock).mock.lastCall[0];
|
||||
expect(blob.type).toBe('text/csv;charset=utf-8');
|
||||
const text = await blob.text();
|
||||
expect(text).toBe(`"time","message bar"\r\n1970-01-02T00:00:00Z,INFO 1`);
|
||||
});
|
||||
});
|
||||
|
@ -1,17 +1,31 @@
|
||||
import { css } from '@emotion/css';
|
||||
import saveAs from 'file-saver';
|
||||
import React from 'react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
|
||||
import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, dateTimeFormat } from '@grafana/data';
|
||||
import {
|
||||
LogsDedupStrategy,
|
||||
LogsMetaItem,
|
||||
LogsMetaKind,
|
||||
LogRowModel,
|
||||
CoreApp,
|
||||
dateTimeFormat,
|
||||
transformDataFrame,
|
||||
DataTransformerConfig,
|
||||
CustomTransformOperator,
|
||||
} from '@grafana/data';
|
||||
import { DataFrame } from '@grafana/data/';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { downloadLogsModelAsTxt } from '../../inspector/utils/download';
|
||||
import { downloadDataFrameAsCsv, downloadLogsModelAsTxt } from '../../inspector/utils/download';
|
||||
import { LogLabels } from '../../logs/components/LogLabels';
|
||||
import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage';
|
||||
import { logRowsToReadableJson } from '../../logs/utils';
|
||||
import { MetaInfoText, MetaItemProps } from '../MetaInfoText';
|
||||
|
||||
import { getLogsExtractFields } from './LogsTable';
|
||||
|
||||
const getStyles = () => ({
|
||||
metaContainer: css`
|
||||
flex: 1;
|
||||
@ -35,6 +49,7 @@ export type Props = {
|
||||
enum DownloadFormat {
|
||||
Text = 'text',
|
||||
Json = 'json',
|
||||
CSV = 'csv',
|
||||
}
|
||||
|
||||
export const LogsMetaRow = React.memo(
|
||||
@ -51,7 +66,7 @@ export const LogsMetaRow = React.memo(
|
||||
}: Props) => {
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const downloadLogs = (format: DownloadFormat) => {
|
||||
const downloadLogs = async (format: DownloadFormat) => {
|
||||
reportInteraction('grafana_logs_download_logs_clicked', {
|
||||
app: CoreApp.Explore,
|
||||
format,
|
||||
@ -67,10 +82,30 @@ export const LogsMetaRow = React.memo(
|
||||
const blob = new Blob([JSON.stringify(jsonLogs)], {
|
||||
type: 'application/json;charset=utf-8',
|
||||
});
|
||||
|
||||
const fileName = `Explore-logs-${dateTimeFormat(new Date())}.json`;
|
||||
saveAs(blob, fileName);
|
||||
break;
|
||||
case DownloadFormat.CSV:
|
||||
const dataFrameMap = new Map<string, DataFrame>();
|
||||
logRows.forEach((row) => {
|
||||
if (row.dataFrame?.refId && !dataFrameMap.has(row.dataFrame?.refId)) {
|
||||
dataFrameMap.set(row.dataFrame?.refId, row.dataFrame);
|
||||
}
|
||||
});
|
||||
dataFrameMap.forEach(async (dataFrame) => {
|
||||
const transforms: Array<DataTransformerConfig | CustomTransformOperator> = getLogsExtractFields(dataFrame);
|
||||
transforms.push({
|
||||
id: 'organize',
|
||||
options: {
|
||||
excludeByName: {
|
||||
['labels']: true,
|
||||
['labelTypes']: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const transformedDataFrame = await lastValueFrom(transformDataFrame(transforms, [dataFrame]));
|
||||
downloadDataFrameAsCsv(transformedDataFrame[0], `Explore-logs-${dataFrame.refId}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -131,6 +166,7 @@ export const LogsMetaRow = React.memo(
|
||||
<Menu>
|
||||
<Menu.Item label="txt" onClick={() => downloadLogs(DownloadFormat.Text)} />
|
||||
<Menu.Item label="json" onClick={() => downloadLogs(DownloadFormat.Json)} />
|
||||
<Menu.Item label="csv" onClick={() => downloadLogs(DownloadFormat.CSV)} />
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
|
@ -105,7 +105,7 @@ export function LogsTable(props: Props) {
|
||||
}
|
||||
|
||||
// create extract JSON transformation for every field that is `json.RawMessage`
|
||||
const transformations: Array<DataTransformerConfig | CustomTransformOperator> = extractFields(dataFrame);
|
||||
const transformations: Array<DataTransformerConfig | CustomTransformOperator> = getLogsExtractFields(dataFrame);
|
||||
|
||||
let labelFilters = buildLabelFilters(columnsWithMeta);
|
||||
|
||||
@ -197,7 +197,7 @@ const isFieldFilterable = (field: Field, bodyName: string, timeName: string) =>
|
||||
|
||||
// TODO: explore if `logsFrame.ts` can help us with getting the right fields
|
||||
// TODO Why is typeInfo not defined on the Field interface?
|
||||
function extractFields(dataFrame: DataFrame) {
|
||||
export function getLogsExtractFields(dataFrame: DataFrame) {
|
||||
return dataFrame.fields
|
||||
.filter((field: Field & { typeInfo?: { frame: string } }) => {
|
||||
const isFieldLokiLabels =
|
||||
|
Loading…
Reference in New Issue
Block a user