mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Transformations: support a rows mode in labels to fields (#41020)
This commit is contained in:
parent
aac3f88603
commit
70e92ef987
@ -192,20 +192,33 @@ In the example below, I added two fields together and named them Sum.
|
|||||||
|
|
||||||
## Labels to fields
|
## Labels to fields
|
||||||
|
|
||||||
This transformation changes time series results that include labels or tags into to a table structure where each label becomes its own field.
|
This transformation changes time series results that include labels or tags into to a table structure where each label keys and values
|
||||||
|
are included in the table result. The labels can be displayed either as columns or as row values.
|
||||||
|
|
||||||
Given a query result of two time series:
|
Given a query result of two time series:
|
||||||
|
|
||||||
- Series 1: labels Server=Server A, Datacenter=EU
|
- Series 1: labels Server=Server A, Datacenter=EU
|
||||||
- Series 2: labels Server=Server B, Datacenter=EU
|
- Series 2: labels Server=Server B, Datacenter=EU
|
||||||
|
|
||||||
This would result in a table like this:
|
In "Columns" mode, the result looks like this:
|
||||||
|
|
||||||
| Time | Server | Datacenter | Value |
|
| Time | Server | Datacenter | Value |
|
||||||
| ------------------- | -------- | ---------- | ----- |
|
| ------------------- | -------- | ---------- | ----- |
|
||||||
| 2020-07-07 11:34:20 | Server A | EU | 1 |
|
| 2020-07-07 11:34:20 | Server A | EU | 1 |
|
||||||
| 2020-07-07 11:34:20 | Server B | EU | 2 |
|
| 2020-07-07 11:34:20 | Server B | EU | 2 |
|
||||||
|
|
||||||
|
In "Rows" mode, the result has a table for each series and show each label value like this:
|
||||||
|
|
||||||
|
| label | value |
|
||||||
|
| ---------- | -------- |
|
||||||
|
| Server | Server A |
|
||||||
|
| Datacenter | EU |
|
||||||
|
|
||||||
|
| label | value |
|
||||||
|
| ---------- | -------- |
|
||||||
|
| Server | Server B |
|
||||||
|
| Datacenter | EU |
|
||||||
|
|
||||||
### Value field name
|
### Value field name
|
||||||
|
|
||||||
If you selected Server as the **Value field name**, then you would get one field for every value of the Server label.
|
If you selected Server as the **Value field name**, then you would get one field for every value of the Server label.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
|
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
|
||||||
import { LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields';
|
import { LabelsToFieldsMode, LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields';
|
||||||
import { DataFrame, DataTransformerConfig, FieldDTO, FieldType } from '../../types';
|
import { DataFrame, DataTransformerConfig, FieldDTO, FieldType } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { toDataFrame, toDataFrameDTO } from '../../dataframe';
|
import { toDataFrame, toDataFrameDTO } from '../../dataframe';
|
||||||
@ -176,6 +176,61 @@ describe('Labels as Columns', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filter labels from source', async () => {
|
||||||
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: {
|
||||||
|
keepLabels: ['foo', 'bar'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', x: 'hide', y: 'z' } },
|
||||||
|
{ name: 'b', type: FieldType.number, values: [2, 4], labels: { bar: 'thing', a: 'nope' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => {
|
||||||
|
expect(received[0][0].fields.map((f) => ({ [f.name]: f.values.toArray() }))).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"time": Array [
|
||||||
|
1000,
|
||||||
|
2000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"a": Array [
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"b": Array [
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"foo": Array [
|
||||||
|
"thing",
|
||||||
|
"thing",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"bar": Array [
|
||||||
|
"thing",
|
||||||
|
"thing",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('data frame with labels and multiple fields with different labels', async () => {
|
it('data frame with labels and multiple fields with different labels', async () => {
|
||||||
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
id: DataTransformerID.labelsToFields,
|
id: DataTransformerID.labelsToFields,
|
||||||
@ -207,6 +262,131 @@ describe('Labels as Columns', () => {
|
|||||||
expect(result.fields).toEqual(expected);
|
expect(result.fields).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Show labels as rows', async () => {
|
||||||
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: {
|
||||||
|
mode: LabelsToFieldsMode.Rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', bar: 'a', zaz: 'xyz' } },
|
||||||
|
{ name: 'b', type: FieldType.number, values: [2, 4], labels: { foo: 'thing', bar: 'b' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => {
|
||||||
|
expect(
|
||||||
|
received[0].map((f) => ({ name: f.name, fields: f.fields.map((v) => ({ [v.name]: v.values.toArray() })) }))
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"label": Array [
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
"zaz",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"value": Array [
|
||||||
|
"thing",
|
||||||
|
"a",
|
||||||
|
"xyz",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "a {bar=\\"a\\", foo=\\"thing\\", zaz=\\"xyz\\"}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"label": Array [
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"value": Array [
|
||||||
|
"thing",
|
||||||
|
"b",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "b {bar=\\"b\\", foo=\\"thing\\"}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Show labels as rows (and filter)', async () => {
|
||||||
|
const cfg: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: {
|
||||||
|
mode: LabelsToFieldsMode.Rows,
|
||||||
|
keepLabels: ['zaz', 'bar'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const source = toDataFrame({
|
||||||
|
name: 'A',
|
||||||
|
fields: [
|
||||||
|
{ name: 'time', type: FieldType.time, values: [1000, 2000] },
|
||||||
|
{ name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', bar: 'a', zaz: 'xyz' } },
|
||||||
|
{ name: 'b', type: FieldType.number, values: [2, 4], labels: { foo: 'thing', bar: 'b' } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => {
|
||||||
|
expect(
|
||||||
|
received[0].map((f) => ({ name: f.name, fields: f.fields.map((v) => ({ [v.name]: v.values.toArray() })) }))
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"label": Array [
|
||||||
|
"zaz",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"value": Array [
|
||||||
|
"xyz",
|
||||||
|
"a",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "a {bar=\\"a\\", foo=\\"thing\\", zaz=\\"xyz\\"}",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"fields": Array [
|
||||||
|
Object {
|
||||||
|
"label": Array [
|
||||||
|
"zaz",
|
||||||
|
"bar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"value": Array [
|
||||||
|
undefined,
|
||||||
|
"b",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"name": "b {bar=\\"b\\", foo=\\"thing\\"}",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function toSimpleObject(frame: DataFrame) {
|
function toSimpleObject(frame: DataFrame) {
|
||||||
|
@ -3,10 +3,20 @@ import { map } from 'rxjs/operators';
|
|||||||
import { DataFrame, Field, FieldType, SynchronousDataTransformerInfo } from '../../types';
|
import { DataFrame, Field, FieldType, SynchronousDataTransformerInfo } from '../../types';
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { ArrayVector } from '../../vector';
|
import { ArrayVector } from '../../vector';
|
||||||
|
import { getFieldDisplayName } from '../..';
|
||||||
|
|
||||||
|
export enum LabelsToFieldsMode {
|
||||||
|
Columns = 'columns', // default mode
|
||||||
|
Rows = 'rows',
|
||||||
|
}
|
||||||
export interface LabelsToFieldsOptions {
|
export interface LabelsToFieldsOptions {
|
||||||
/*
|
mode?: LabelsToFieldsMode;
|
||||||
* If set this will use this label's value as the value field name.
|
|
||||||
|
/** When empty, this will keep all labels, otherise it will keep only labels matching the value */
|
||||||
|
keepLabels?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When in column mode and if set this will use this label's value as the value field name.
|
||||||
*/
|
*/
|
||||||
valueLabel?: string;
|
valueLabel?: string;
|
||||||
}
|
}
|
||||||
@ -14,18 +24,23 @@ export interface LabelsToFieldsOptions {
|
|||||||
export const labelsToFieldsTransformer: SynchronousDataTransformerInfo<LabelsToFieldsOptions> = {
|
export const labelsToFieldsTransformer: SynchronousDataTransformerInfo<LabelsToFieldsOptions> = {
|
||||||
id: DataTransformerID.labelsToFields,
|
id: DataTransformerID.labelsToFields,
|
||||||
name: 'Labels to fields',
|
name: 'Labels to fields',
|
||||||
description: 'Extract time series labels to fields (columns)',
|
description: 'Extract time series labels to fields (columns or rows)',
|
||||||
defaultOptions: {},
|
defaultOptions: {},
|
||||||
|
|
||||||
operator: (options) => (source) => source.pipe(map((data) => labelsToFieldsTransformer.transformer(options)(data))),
|
operator: (options) => (source) => source.pipe(map((data) => labelsToFieldsTransformer.transformer(options)(data))),
|
||||||
|
|
||||||
transformer: (options: LabelsToFieldsOptions) => (data: DataFrame[]) => {
|
transformer: (options: LabelsToFieldsOptions) => (data: DataFrame[]) => {
|
||||||
|
// Show each label as a field row
|
||||||
|
if (options.mode === LabelsToFieldsMode.Rows) {
|
||||||
|
return convertLabelsToRows(data, options.keepLabels);
|
||||||
|
}
|
||||||
|
|
||||||
const result: DataFrame[] = [];
|
const result: DataFrame[] = [];
|
||||||
|
const keepLabels = options.keepLabels?.length ? new Set(options.keepLabels) : undefined;
|
||||||
|
|
||||||
for (const frame of data) {
|
for (const frame of data) {
|
||||||
const newFields: Field[] = [];
|
const newFields: Field[] = [];
|
||||||
const uniqueLabels: Record<string, Set<string>> = {};
|
const uniqueLabels: Record<string, Set<string>> = {};
|
||||||
|
|
||||||
for (const field of frame.fields) {
|
for (const field of frame.fields) {
|
||||||
if (!field.labels) {
|
if (!field.labels) {
|
||||||
newFields.push(field);
|
newFields.push(field);
|
||||||
@ -45,6 +60,10 @@ export const labelsToFieldsTransformer: SynchronousDataTransformerInfo<LabelsToF
|
|||||||
newFields.push(sansLabels);
|
newFields.push(sansLabels);
|
||||||
|
|
||||||
for (const labelName of Object.keys(field.labels)) {
|
for (const labelName of Object.keys(field.labels)) {
|
||||||
|
if (keepLabels && !keepLabels.has(labelName)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// if we should use this label as the value field name store it and skip adding this as a separate field
|
// if we should use this label as the value field name store it and skip adding this as a separate field
|
||||||
if (options.valueLabel === labelName) {
|
if (options.valueLabel === labelName) {
|
||||||
sansLabels.name = field.labels[labelName];
|
sansLabels.name = field.labels[labelName];
|
||||||
@ -77,3 +96,38 @@ export const labelsToFieldsTransformer: SynchronousDataTransformerInfo<LabelsToF
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function convertLabelsToRows(data: DataFrame[], keepLabels?: string[]): DataFrame[] {
|
||||||
|
const result: DataFrame[] = [];
|
||||||
|
for (const frame of data) {
|
||||||
|
for (const field of frame.fields) {
|
||||||
|
if (field.labels) {
|
||||||
|
const keys: string[] = [];
|
||||||
|
const vals: string[] = [];
|
||||||
|
if (keepLabels) {
|
||||||
|
for (const key of keepLabels) {
|
||||||
|
keys.push(key);
|
||||||
|
vals.push(field.labels[key]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, val] of Object.entries(field.labels)) {
|
||||||
|
keys.push(key);
|
||||||
|
vals.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vals.length) {
|
||||||
|
result.push({
|
||||||
|
...frame,
|
||||||
|
name: getFieldDisplayName(field, frame, data),
|
||||||
|
fields: [
|
||||||
|
{ name: 'label', type: FieldType.string, config: {}, values: new ArrayVector(keys) },
|
||||||
|
{ name: 'value', type: FieldType.string, config: {}, values: new ArrayVector(vals) },
|
||||||
|
],
|
||||||
|
length: vals.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
DataTransformerID,
|
DataTransformerID,
|
||||||
SelectableValue,
|
SelectableValue,
|
||||||
@ -6,52 +6,116 @@ import {
|
|||||||
TransformerRegistryItem,
|
TransformerRegistryItem,
|
||||||
TransformerUIProps,
|
TransformerUIProps,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Select } from '@grafana/ui';
|
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill } from '@grafana/ui';
|
||||||
|
|
||||||
import { LabelsToFieldsOptions } from '@grafana/data/src/transformations/transformers/labelsToFields';
|
import {
|
||||||
|
LabelsToFieldsMode,
|
||||||
|
LabelsToFieldsOptions,
|
||||||
|
} from '@grafana/data/src/transformations/transformers/labelsToFields';
|
||||||
|
|
||||||
|
const modes: Array<SelectableValue<LabelsToFieldsMode>> = [
|
||||||
|
{ value: LabelsToFieldsMode.Columns, label: 'Columns' },
|
||||||
|
{ value: LabelsToFieldsMode.Rows, label: 'Rows' },
|
||||||
|
];
|
||||||
|
|
||||||
export const LabelsAsFieldsTransformerEditor: React.FC<TransformerUIProps<LabelsToFieldsOptions>> = ({
|
export const LabelsAsFieldsTransformerEditor: React.FC<TransformerUIProps<LabelsToFieldsOptions>> = ({
|
||||||
input,
|
input,
|
||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
let labelNames: Array<SelectableValue<string>> = [];
|
const labelWidth = 20;
|
||||||
let uniqueLabels: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
for (const frame of input) {
|
const { labelNames, selected } = useMemo(() => {
|
||||||
for (const field of frame.fields) {
|
let labelNames: Array<SelectableValue<string>> = [];
|
||||||
if (!field.labels) {
|
let uniqueLabels: Record<string, boolean> = {};
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const labelName of Object.keys(field.labels)) {
|
for (const frame of input) {
|
||||||
if (!uniqueLabels[labelName]) {
|
for (const field of frame.fields) {
|
||||||
labelNames.push({ value: labelName, label: labelName });
|
if (!field.labels) {
|
||||||
uniqueLabels[labelName] = true;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const labelName of Object.keys(field.labels)) {
|
||||||
|
if (!uniqueLabels[labelName]) {
|
||||||
|
labelNames.push({ value: labelName, label: labelName });
|
||||||
|
uniqueLabels[labelName] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const selected = new Set(options.keepLabels?.length ? options.keepLabels : Object.keys(uniqueLabels));
|
||||||
|
return { labelNames, selected };
|
||||||
|
}, [options.keepLabels, input]);
|
||||||
|
|
||||||
const onValueLabelChange = (value: SelectableValue<string> | null) => {
|
const onValueLabelChange = (value: SelectableValue<string> | null) => {
|
||||||
onChange({ valueLabel: value?.value });
|
onChange({ ...options, valueLabel: value?.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleSelection = (v: string) => {
|
||||||
|
if (selected.has(v)) {
|
||||||
|
selected.delete(v);
|
||||||
|
} else {
|
||||||
|
selected.add(v);
|
||||||
|
}
|
||||||
|
if (selected.size === labelNames.length || !selected.size) {
|
||||||
|
const { keepLabels, ...rest } = options;
|
||||||
|
onChange(rest);
|
||||||
|
} else {
|
||||||
|
onChange({ ...options, keepLabels: [...selected] });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-inline">
|
<div>
|
||||||
<div className="gf-form">
|
<InlineFieldRow>
|
||||||
<div className="gf-form-label width-8">Value field name</div>
|
<InlineField label={'Mode'} labelWidth={labelWidth}>
|
||||||
<Select
|
<RadioButtonGroup
|
||||||
menuShouldPortal
|
options={modes}
|
||||||
isClearable={true}
|
value={options.mode ?? LabelsToFieldsMode.Columns}
|
||||||
allowCustomValue={false}
|
onChange={(v) => onChange({ ...options, mode: v })}
|
||||||
placeholder="(Optional) Select label"
|
/>
|
||||||
options={labelNames}
|
</InlineField>
|
||||||
className="min-width-18 gf-form-spacing"
|
</InlineFieldRow>
|
||||||
value={options?.valueLabel}
|
<InlineFieldRow>
|
||||||
onChange={onValueLabelChange}
|
<InlineField label={'Labels'} labelWidth={labelWidth}>
|
||||||
/>
|
<>
|
||||||
</div>
|
{labelNames.map((o, i) => {
|
||||||
|
const label = o.label!;
|
||||||
|
return (
|
||||||
|
<FilterPill
|
||||||
|
key={`${label}/${i}`}
|
||||||
|
onClick={() => onToggleSelection(label)}
|
||||||
|
label={label}
|
||||||
|
selected={selected.has(label)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
{options.mode !== LabelsToFieldsMode.Rows && (
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineField
|
||||||
|
label={'Value field name'}
|
||||||
|
labelWidth={labelWidth}
|
||||||
|
tooltip="Replace the value field name with a label"
|
||||||
|
htmlFor="labels-to-fields-as-name"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
menuShouldPortal
|
||||||
|
inputId="labels-to-fields-as-name"
|
||||||
|
isClearable={true}
|
||||||
|
allowCustomValue={false}
|
||||||
|
placeholder="(Optional) Select label"
|
||||||
|
options={labelNames}
|
||||||
|
value={options?.valueLabel}
|
||||||
|
onChange={onValueLabelChange}
|
||||||
|
className="min-width-16"
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user