/usr/share/grafana/public/app/features/transformers/joinByLabels
import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, Field, FieldType, SynchronousDataTransformerInfo } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getDistinctLabels } from '../utils'; export interface JoinByLabelsTransformOptions { value: string; // something must be defined join?: string[]; } export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo<JoinByLabelsTransformOptions> = () => ({ id: DataTransformerID.joinByLabels, name: t('transformers.get-join-by-labels-transformer.name.join-by-labels', 'Join by labels'), description: t( 'transformers.get-join-by-labels-transformer.description.flatten-labeled-results-table-joined-labels', 'Flatten labeled results into a table joined by labels.' ), defaultOptions: {}, operator: (options, ctx) => (source) => source.pipe(map((data) => getJoinByLabelsTransformer().transformer(options, ctx)(data))), transformer: (options: JoinByLabelsTransformOptions) => { return (data: DataFrame[]) => { if (!data || !data.length) { return data; } return [joinByLabels(options, data)]; }; }, }); interface JoinValues { keys: string[]; values: Record<string, number[]>; } export function joinByLabels(options: JoinByLabelsTransformOptions, data: DataFrame[]): DataFrame { if (!options.value?.length) { return getErrorFrame('No value label configured'); } const distinctLabels = getDistinctLabels(data); if (distinctLabels.size < 1) { return getErrorFrame('No labels in result'); } if (!distinctLabels.has(options.value)) { return getErrorFrame('Value label not found'); } let join = options.join?.length ? options.join : Array.from(distinctLabels); join = join.filter((f) => f !== options.value); const names = new Set<string>(); const found = new Map<string, JoinValues>(); const inputFields: Record<string, Field> = {}; for (const frame of data) { for (const field of frame.fields) { if (field.labels && field.type !== FieldType.time) { const keys = join.map((v) => field.labels![v]); const key = keys.join(','); let item = found.get(key); if (!item) { item = { keys, values: {}, }; found.set(key, item); } const name = field.labels[options.value]; const vals = field.values; const old = item.values[name]; if (old) { item.values[name] = old.concat(vals); } else { item.values[name] = vals; } if (!inputFields[name]) { inputFields[name] = field; // keep the config } names.add(name); } } } const allNames = Array.from(names); const joinValues = join.map((): string[] => []); const nameValues = allNames.map((): number[] => []); for (const item of found.values()) { let valueOffset = -1; let done = false; while (!done) { valueOffset++; done = true; for (let i = 0; i < join.length; i++) { joinValues[i].push(item.keys[i]); } for (let i = 0; i < allNames.length; i++) { const name = allNames[i]; const values = item.values[name] ?? []; nameValues[i].push(values[valueOffset]); if (values.length > valueOffset + 1) { done = false; } } } } const frame: DataFrame = { fields: [], length: nameValues[0].length, refId: `${DataTransformerID.joinByLabels}-${data.map((frame) => frame.refId).join('-')}`, }; for (let i = 0; i < join.length; i++) { frame.fields.push({ name: join[i], config: {}, type: FieldType.string, values: joinValues[i], }); } for (let i = 0; i < allNames.length; i++) { const old = inputFields[allNames[i]]; frame.fields.push({ name: allNames[i], config: {}, type: old.type ?? FieldType.number, values: nameValues[i], }); } return frame; } function getErrorFrame(text: string): DataFrame { return { meta: { notices: [{ severity: 'error', text }], }, fields: [{ name: 'Error', type: FieldType.string, config: {}, values: [text] }], length: 0, }; }
.
Edit
..
Edit
JoinByLabelsTransformerEditor.tsx
Edit
joinByLabels.test.ts
Edit
joinByLabels.ts
Edit