2025-04-01 10:38:02 +09:00

270 lines
8.0 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { lastValueFrom } from 'rxjs';
import {
applyFieldOverrides,
CustomTransformOperator,
DataFrame,
DataFrameType,
DataTransformerConfig,
Field,
FieldType,
guessFieldTypeForField,
LogsSortOrder,
sortDataFrame,
SplitOpen,
TimeRange,
transformDataFrame,
ValueLinkConfig,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { AdHocFilterItem, Table } from '@grafana/ui';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR } from '@grafana/ui/src/components/Table/types';
import { LogsFrame } from 'app/features/logs/logsFrame';
import { getFieldLinksForExplore } from '../utils/links';
import { FieldNameMeta } from './LogsTableWrap';
interface Props {
dataFrame: DataFrame;
width: number;
timeZone: string;
splitOpen: SplitOpen;
range: TimeRange;
logsSortOrder: LogsSortOrder;
columnsWithMeta: Record<string, FieldNameMeta>;
height: number;
onClickFilterLabel?: (key: string, value: string, frame?: DataFrame) => void;
onClickFilterOutLabel?: (key: string, value: string, frame?: DataFrame) => void;
logsFrame: LogsFrame | null;
}
export function LogsTable(props: Props) {
const { timeZone, splitOpen, range, logsSortOrder, width, dataFrame, columnsWithMeta, logsFrame } = props;
const [tableFrame, setTableFrame] = useState<DataFrame | undefined>(undefined);
const timeIndex = logsFrame?.timeField.index;
const prepareTableFrame = useCallback(
(frame: DataFrame): DataFrame => {
if (!frame.length) {
return frame;
}
const sortedFrame = sortDataFrame(frame, timeIndex, logsSortOrder === LogsSortOrder.Descending);
const [frameWithOverrides] = applyFieldOverrides({
data: [sortedFrame],
timeZone,
theme: config.theme2,
replaceVariables: (v: string) => v,
fieldConfig: {
defaults: {
custom: {},
},
overrides: [],
},
});
// `getLinks` and `applyFieldOverrides` are taken from TableContainer.tsx
for (const field of frameWithOverrides.fields) {
field.getLinks = (config: ValueLinkConfig) => {
return getFieldLinksForExplore({
field,
rowIndex: config.valueRowIndex!,
splitOpenFn: splitOpen,
range: range,
dataFrame: sortedFrame!,
});
};
field.config = {
...field.config,
custom: {
inspect: true,
filterable: true, // This sets the columns to be filterable
width: getInitialFieldWidth(field),
...field.config.custom,
},
// This sets the individual field value as filterable
filterable: isFieldFilterable(field, logsFrame?.bodyField.name ?? '', logsFrame?.timeField.name ?? ''),
};
// If it's a string, then try to guess for a better type for numeric support in viz
field.type = field.type === FieldType.string ? (guessFieldTypeForField(field) ?? FieldType.string) : field.type;
}
return frameWithOverrides;
},
[logsSortOrder, timeZone, splitOpen, range, logsFrame?.bodyField.name, logsFrame?.timeField.name, timeIndex]
);
useEffect(() => {
const prepare = async () => {
if (!logsFrame?.timeField.name || !logsFrame?.bodyField.name) {
setTableFrame(undefined);
return;
}
// create extract JSON transformation for every field that is `json.RawMessage`
const transformations: Array<DataTransformerConfig | CustomTransformOperator> = getLogsExtractFields(dataFrame);
let labelFilters = buildLabelFilters(columnsWithMeta);
// Add the label filters to the transformations
const transform = getLabelFiltersTransform(labelFilters);
if (transform) {
transformations.push(transform);
} else {
// If no fields are filtered, filter the default fields, so we don't render all columns
transformations.push({
id: 'organize',
options: {
indexByName: {
[logsFrame.bodyField.name]: 0,
[logsFrame.timeField.name]: 1,
},
includeByName: {
[logsFrame.bodyField.name]: true,
[logsFrame.timeField.name]: true,
},
},
});
}
if (transformations.length > 0) {
const transformedDataFrame = await lastValueFrom(transformDataFrame(transformations, [dataFrame]));
const tableFrame = prepareTableFrame(transformedDataFrame[0]);
setTableFrame(tableFrame);
} else {
setTableFrame(prepareTableFrame(dataFrame));
}
};
prepare();
}, [
columnsWithMeta,
dataFrame,
logsSortOrder,
prepareTableFrame,
logsFrame?.bodyField.name,
logsFrame?.timeField.name,
]);
if (!tableFrame) {
return null;
}
const onCellFilterAdded = (filter: AdHocFilterItem) => {
const { value, key, operator } = filter;
const { onClickFilterLabel, onClickFilterOutLabel } = props;
if (!onClickFilterLabel || !onClickFilterOutLabel) {
return;
}
if (operator === FILTER_FOR_OPERATOR) {
onClickFilterLabel(key, value, dataFrame);
}
if (operator === FILTER_OUT_OPERATOR) {
onClickFilterOutLabel(key, value, dataFrame);
}
};
return (
<Table
data={tableFrame}
width={width}
onCellFilterAdded={props.onClickFilterLabel && props.onClickFilterOutLabel ? onCellFilterAdded : undefined}
height={props.height}
footerOptions={{ show: true, reducer: ['count'], countRows: true }}
initialSortBy={[
{ displayName: logsFrame?.timeField.name || '', desc: logsSortOrder === LogsSortOrder.Descending },
]}
/>
);
}
const isFieldFilterable = (field: Field, bodyName: string, timeName: string) => {
if (!bodyName || !timeName) {
return false;
}
if (bodyName === field.name) {
return false;
}
if (timeName === field.name) {
return false;
}
if (field.config.links?.length) {
return false;
}
return true;
};
// TODO: explore if `logsFrame.ts` can help us with getting the right fields
// TODO Why is typeInfo not defined on the Field interface?
export function getLogsExtractFields(dataFrame: DataFrame) {
return dataFrame.fields
.filter((field: Field & { typeInfo?: { frame: string } }) => {
const isFieldLokiLabels =
field.typeInfo?.frame === 'json.RawMessage' &&
field.name === 'labels' &&
dataFrame?.meta?.type !== DataFrameType.LogLines;
const isFieldDataplaneLabels =
field.name === 'labels' && field.type === FieldType.other && dataFrame?.meta?.type === DataFrameType.LogLines;
return isFieldLokiLabels || isFieldDataplaneLabels;
})
.flatMap((field: Field) => {
return [
{
id: 'extractFields',
options: {
format: 'json',
keepTime: false,
replace: false,
source: field.name,
},
},
];
});
}
function buildLabelFilters(columnsWithMeta: Record<string, FieldNameMeta>) {
// Create object of label filters to include columns selected by the user
let labelFilters: Record<string, number> = {};
Object.keys(columnsWithMeta)
.filter((key) => columnsWithMeta[key].active)
.forEach((key) => {
const index = columnsWithMeta[key].index;
// Index should always be defined for any active column
if (index !== undefined) {
labelFilters[key] = index;
}
});
return labelFilters;
}
function getLabelFiltersTransform(labelFilters: Record<string, number>) {
let labelFiltersInclude: Record<string, boolean> = {};
for (const key in labelFilters) {
labelFiltersInclude[key] = true;
}
if (Object.keys(labelFilters).length > 0) {
return {
id: 'organize',
options: {
indexByName: labelFilters,
includeByName: labelFiltersInclude,
},
};
}
return null;
}
function getInitialFieldWidth(field: Field): number | undefined {
if (field.type === FieldType.time) {
return 200;
}
return undefined;
}