mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TabelPanel: add support for organizing fields/columns. (#23135)
* Added draft on transformers to sort and hide fields. * added structure for the UI. * draft on sorting/filtering UI. * simplified the datastructure a bit. * added draft on drag and drop support. * added some super simple styling. Nothing final still waiting for a proper design on this. * updated lockfile after merge. * changed so we use the new path for button. * added one more test. * Ignore feature toggle * Moved editor to app * Added top description * Minor update * Did some renaming and simplified the code a bit. * fixed so we dont use capital naming on the transformer. * changed to an vertical drag and drop design. * added support to rename fields. Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
139753358d
commit
1f717f514a
@ -7,7 +7,10 @@ import { filterFieldsByNameTransformer, FilterFieldsByNameTransformerOptions } f
|
||||
import { noopTransformer } from './transformers/noop';
|
||||
import { DataTransformerConfig, DataTransformerInfo } from '../types/transformations';
|
||||
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
|
||||
import { orderFieldsTransformer } from './transformers/order';
|
||||
import { organizeFieldsTransformer } from './transformers/organize';
|
||||
import { seriesToColumnsTransformer } from './transformers/seriesToColumns';
|
||||
import { renameFieldsTransformer } from './transformers/rename';
|
||||
|
||||
// Initalize the Registry
|
||||
|
||||
@ -67,9 +70,12 @@ export const transformersRegistry = new TransformerRegistry(() => [
|
||||
filterFieldsByNameTransformer,
|
||||
filterFramesTransformer,
|
||||
filterFramesByRefIdTransformer,
|
||||
orderFieldsTransformer,
|
||||
organizeFieldsTransformer,
|
||||
appendTransformer,
|
||||
reduceTransformer,
|
||||
seriesToColumnsTransformer,
|
||||
renameFieldsTransformer,
|
||||
]);
|
||||
|
||||
export { ReduceTransformerOptions, FilterFieldsByNameTransformerOptions };
|
||||
|
@ -3,6 +3,9 @@ export enum DataTransformerID {
|
||||
append = 'append', // Merge all series together
|
||||
// rotate = 'rotate', // Columns to rows
|
||||
reduce = 'reduce', // Run calculations on fields
|
||||
order = 'order', // order fields based on user configuration
|
||||
organize = 'organize', // order, rename and filter based on user configuration
|
||||
rename = 'rename', // rename field based on user configuration
|
||||
|
||||
seriesToColumns = 'seriesToColumns', // former table transform timeseries_to_columns
|
||||
filterFields = 'filterFields', // Pick some fields (keep all frames)
|
||||
|
@ -0,0 +1,148 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataTransformerConfig,
|
||||
DataTransformerID,
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
transformDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { OrderFieldsTransformerOptions } from './order';
|
||||
|
||||
describe('Order Transformer', () => {
|
||||
describe('when consistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should order according to config', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
humidity: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ordered = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(ordered.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'temperature',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'humidity',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inconsistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should append fields missing in config at the end', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
humidity: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ordered = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(ordered.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'humidity',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when transforming with empty configuration', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should keep the same order as in the incoming data', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
indexByName: {},
|
||||
},
|
||||
};
|
||||
|
||||
const ordered = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(ordered.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'humidity',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import { DataTransformerID } from './ids';
|
||||
import { DataTransformerInfo } from '../../types/transformations';
|
||||
import { DataFrame } from '../..';
|
||||
import { Field } from '../../types';
|
||||
|
||||
export interface OrderFieldsTransformerOptions {
|
||||
indexByName: Record<string, number>;
|
||||
}
|
||||
|
||||
export const orderFieldsTransformer: DataTransformerInfo<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
name: 'Order fields by name',
|
||||
description: 'Order fields based on configuration given by user',
|
||||
defaultOptions: {
|
||||
indexByName: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a modified copy of the series. If the transform is not or should not
|
||||
* be applied, just return the input series
|
||||
*/
|
||||
transformer: (options: OrderFieldsTransformerOptions) => {
|
||||
const orderer = createFieldsOrderer(options.indexByName);
|
||||
|
||||
return (data: DataFrame[]) => {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.map(frame => ({
|
||||
...frame,
|
||||
fields: orderer(frame.fields),
|
||||
}));
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const createFieldsComparer = (indexByName: Record<string, number>) => (a: string, b: string) => {
|
||||
return indexOfField(a, indexByName) - indexOfField(b, indexByName);
|
||||
};
|
||||
|
||||
const createFieldsOrderer = (indexByName: Record<string, number>) => (fields: Field[]) => {
|
||||
if (!Array.isArray(fields) || fields.length === 0) {
|
||||
return fields;
|
||||
}
|
||||
if (!indexByName || Object.keys(indexByName).length === 0) {
|
||||
return fields;
|
||||
}
|
||||
const comparer = createFieldsComparer(indexByName);
|
||||
return fields.sort((a, b) => comparer(a.name, b.name));
|
||||
};
|
||||
|
||||
const indexOfField = (fieldName: string, indexByName: Record<string, number>) => {
|
||||
if (Number.isInteger(indexByName[fieldName])) {
|
||||
return indexByName[fieldName];
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
};
|
@ -0,0 +1,105 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataTransformerConfig,
|
||||
DataTransformerID,
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
transformDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { OrganizeFieldsTransformerOptions } from './organize';
|
||||
|
||||
describe('OrganizeFields Transformer', () => {
|
||||
describe('when consistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should order and filter according to config', () => {
|
||||
const cfg: DataTransformerConfig<OrganizeFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.organize,
|
||||
options: {
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
humidity: 1,
|
||||
},
|
||||
excludeByName: {
|
||||
time: true,
|
||||
},
|
||||
renameByName: {
|
||||
humidity: 'renamed_humidity',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const organized = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(organized.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'temperature',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'renamed_humidity',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inconsistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should append fields missing in config at the end', () => {
|
||||
const cfg: DataTransformerConfig<OrganizeFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.organize,
|
||||
options: {
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
humidity: 1,
|
||||
},
|
||||
excludeByName: {
|
||||
humidity: true,
|
||||
},
|
||||
renameByName: {
|
||||
time: 'renamed_time',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const organized = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(organized.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'renamed_time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,53 @@
|
||||
import { DataTransformerID } from './ids';
|
||||
import { DataTransformerInfo } from '../../types/transformations';
|
||||
import { OrderFieldsTransformerOptions, orderFieldsTransformer } from './order';
|
||||
import { filterFieldsByNameTransformer } from './filterByName';
|
||||
import { DataFrame } from '../..';
|
||||
import { RenameFieldsTransformerOptions, renameFieldsTransformer } from './rename';
|
||||
|
||||
export interface OrganizeFieldsTransformerOptions
|
||||
extends OrderFieldsTransformerOptions,
|
||||
RenameFieldsTransformerOptions {
|
||||
excludeByName: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export const organizeFieldsTransformer: DataTransformerInfo<OrganizeFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.organize,
|
||||
name: 'Organize fields by name',
|
||||
description: 'Order, filter and rename fields based on configuration given by user',
|
||||
defaultOptions: {
|
||||
excludeByName: {},
|
||||
indexByName: {},
|
||||
renameByName: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a modified copy of the series. If the transform is not or should not
|
||||
* be applied, just return the input series
|
||||
*/
|
||||
transformer: (options: OrganizeFieldsTransformerOptions) => {
|
||||
const rename = renameFieldsTransformer.transformer(options);
|
||||
const order = orderFieldsTransformer.transformer(options);
|
||||
const filter = filterFieldsByNameTransformer.transformer({
|
||||
exclude: mapToExcludeRegexp(options.excludeByName),
|
||||
});
|
||||
|
||||
return (data: DataFrame[]) => rename(order(filter(data)));
|
||||
},
|
||||
};
|
||||
|
||||
const mapToExcludeRegexp = (excludeByName: Record<string, boolean>): string | undefined => {
|
||||
if (!excludeByName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fieldsToExclude = Object.keys(excludeByName)
|
||||
.filter(name => excludeByName[name])
|
||||
.join('|');
|
||||
|
||||
if (fieldsToExclude.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `^(${fieldsToExclude})$`;
|
||||
};
|
@ -0,0 +1,148 @@
|
||||
import {
|
||||
ArrayVector,
|
||||
DataTransformerConfig,
|
||||
DataTransformerID,
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
transformDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { RenameFieldsTransformerOptions } from './rename';
|
||||
|
||||
describe('Rename Transformer', () => {
|
||||
describe('when consistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'temperature', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should rename according to config', () => {
|
||||
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.rename,
|
||||
options: {
|
||||
renameByName: {
|
||||
time: 'Total time',
|
||||
humidity: 'Moistiness',
|
||||
temperature: 'how cold is it?',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renamed = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(renamed.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'Total time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'how cold is it?',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'Moistiness',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when inconsistent data is received', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should not rename fields missing in config', () => {
|
||||
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.rename,
|
||||
options: {
|
||||
renameByName: {
|
||||
time: 'ttl',
|
||||
temperature: 'temp',
|
||||
humidity: 'hum',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renamed = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(renamed.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'ttl',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'hum',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when transforming with empty configuration', () => {
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||
{ name: 'pressure', type: FieldType.number, values: [10.3, 10.4, 10.5, 10.6] },
|
||||
{ name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] },
|
||||
],
|
||||
});
|
||||
|
||||
it('should keep the same names as in the incoming data', () => {
|
||||
const cfg: DataTransformerConfig<RenameFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.rename,
|
||||
options: {
|
||||
renameByName: {},
|
||||
},
|
||||
};
|
||||
|
||||
const renamed = transformDataFrame([cfg], [data])[0];
|
||||
|
||||
expect(renamed.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([3000, 4000, 5000, 6000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10.3, 10.4, 10.5, 10.6]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'humidity',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([10000.3, 10000.4, 10000.5, 10000.6]),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import { DataTransformerID } from './ids';
|
||||
import { DataTransformerInfo } from '../../types/transformations';
|
||||
import { DataFrame, Field } from '../..';
|
||||
|
||||
export interface RenameFieldsTransformerOptions {
|
||||
renameByName: Record<string, string>;
|
||||
}
|
||||
|
||||
export const renameFieldsTransformer: DataTransformerInfo<RenameFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.rename,
|
||||
name: 'Rename fields by name',
|
||||
description: 'Rename fields based on configuration given by user',
|
||||
defaultOptions: {
|
||||
renameByName: {},
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a modified copy of the series. If the transform is not or should not
|
||||
* be applied, just return the input series
|
||||
*/
|
||||
transformer: (options: RenameFieldsTransformerOptions) => {
|
||||
const renamer = createRenamer(options.renameByName);
|
||||
|
||||
return (data: DataFrame[]) => {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return data.map(frame => ({
|
||||
...frame,
|
||||
fields: renamer(frame.fields),
|
||||
}));
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const createRenamer = (renameByName: Record<string, string>) => (fields: Field[]): Field[] => {
|
||||
if (!renameByName || Object.keys(renameByName).length === 0) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
return fields.map(field => {
|
||||
const renameTo = renameByName[field.name];
|
||||
|
||||
if (typeof renameTo !== 'string' || renameTo.length === 0) {
|
||||
return field;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
name: renameTo,
|
||||
};
|
||||
});
|
||||
};
|
@ -32,6 +32,7 @@
|
||||
"@grafana/slate-react": "0.22.9-grafana",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@torkelo/react-select": "3.0.8",
|
||||
"@types/react-beautiful-dnd": "12.1.2",
|
||||
"@types/react-color": "3.0.1",
|
||||
"@types/react-select": "3.0.8",
|
||||
"@types/react-table": "7.0.12",
|
||||
@ -51,6 +52,7 @@
|
||||
"rc-slider": "9.2.3",
|
||||
"rc-time-picker": "^3.7.3",
|
||||
"react": "16.12.0",
|
||||
"react-beautiful-dnd": "13.0.0",
|
||||
"react-calendar": "2.19.2",
|
||||
"react-color": "2.18.0",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
|
@ -12,7 +12,7 @@ type Justify = 'flex-start' | 'flex-end' | 'space-between' | 'center';
|
||||
type Align = 'normal' | 'flex-start' | 'flex-end' | 'center';
|
||||
|
||||
export interface LayoutProps {
|
||||
children: React.ReactNode[];
|
||||
children: React.ReactNode[] | React.ReactNode;
|
||||
orientation?: Orientation;
|
||||
spacing?: Spacing;
|
||||
justify?: Justify;
|
||||
|
@ -0,0 +1,221 @@
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
import { TransformerUIRegistyItem, TransformerUIProps } from './types';
|
||||
import { DataTransformerID, transformersRegistry, DataFrame, GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { Button } from '../Button';
|
||||
import { createFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
|
||||
import { VerticalGroup } from '../Layout/Layout';
|
||||
import { Input } from '../Forms/Input/Input';
|
||||
|
||||
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
|
||||
|
||||
const OrganizeFieldsTransformerEditor: React.FC<OrganizeFieldsTransformerEditorProps> = props => {
|
||||
const { options, input, onChange } = props;
|
||||
const { indexByName, excludeByName, renameByName } = options;
|
||||
|
||||
const fieldNames = useMemo(() => fieldNamesFromInput(input), [input]);
|
||||
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
|
||||
|
||||
const onToggleVisibility = useCallback(
|
||||
(field: string, shouldExclude: boolean) => {
|
||||
onChange({
|
||||
...options,
|
||||
excludeByName: {
|
||||
...excludeByName,
|
||||
[field]: shouldExclude,
|
||||
},
|
||||
});
|
||||
},
|
||||
[onChange, excludeByName, indexByName]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = result.source.index;
|
||||
const endIndex = result.destination.index;
|
||||
|
||||
if (startIndex === endIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange({
|
||||
...options,
|
||||
indexByName: reorderToIndex(fieldNames, startIndex, endIndex),
|
||||
});
|
||||
},
|
||||
[onChange, indexByName, excludeByName, fieldNames]
|
||||
);
|
||||
|
||||
const onRenameField = useCallback(
|
||||
(from: string, to: string) => {
|
||||
onChange({
|
||||
...options,
|
||||
renameByName: {
|
||||
...options.renameByName,
|
||||
[from]: to,
|
||||
},
|
||||
});
|
||||
},
|
||||
[onChange, fieldNames, renameByName]
|
||||
);
|
||||
|
||||
return (
|
||||
<VerticalGroup>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
|
||||
{provided => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedFieldNames.map((fieldName, index) => {
|
||||
return (
|
||||
<DraggableFieldName
|
||||
fieldName={fieldName}
|
||||
index={index}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
onRenameField={onRenameField}
|
||||
visible={!excludeByName[fieldName]}
|
||||
key={fieldName}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</VerticalGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface DraggableFieldProps {
|
||||
fieldName: string;
|
||||
index: number;
|
||||
visible: boolean;
|
||||
onToggleVisibility: (fieldName: string, isVisible: boolean) => void;
|
||||
onRenameField: (from: string, to: string) => void;
|
||||
}
|
||||
|
||||
const DraggableFieldName: React.FC<DraggableFieldProps> = ({
|
||||
fieldName,
|
||||
index,
|
||||
visible,
|
||||
onToggleVisibility,
|
||||
onRenameField,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getFieldNameStyles(theme);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={fieldName} index={index}>
|
||||
{provided => (
|
||||
<div
|
||||
className={styles.container}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<div className={styles.left}>
|
||||
<i className={cx('fa fa-ellipsis-v', styles.draggable)} />
|
||||
<Button
|
||||
className={styles.toggle}
|
||||
variant="link"
|
||||
size="md"
|
||||
icon={visible ? 'fa fa-eye' : 'fa fa-eye-slash'}
|
||||
onClick={() => onToggleVisibility(fieldName, visible)}
|
||||
/>
|
||||
<span className={styles.name}>{fieldName}</span>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
<Input
|
||||
placeholder={`Rename ${fieldName}`}
|
||||
onChange={event => onRenameField(fieldName, event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
const getFieldNameStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
`,
|
||||
left: css`
|
||||
width: 35%;
|
||||
padding: 0 8px;
|
||||
border-radius: 3px;
|
||||
background-color: ${theme.isDark ? theme.colors.grayBlue : theme.colors.gray6};
|
||||
border: 1px solid ${theme.isDark ? theme.colors.dark6 : theme.colors.gray5};
|
||||
`,
|
||||
right: css`
|
||||
width: 65%;
|
||||
margin-left: 8px;
|
||||
`,
|
||||
toggle: css`
|
||||
padding: 5px;
|
||||
margin: 0 5px;
|
||||
`,
|
||||
draggable: css`
|
||||
font-size: ${theme.typography.size.md};
|
||||
opacity: 0.4;
|
||||
`,
|
||||
name: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
font-weight: ${theme.typography.weight.semibold};
|
||||
`,
|
||||
}));
|
||||
|
||||
const reorderToIndex = (fieldNames: string[], startIndex: number, endIndex: number) => {
|
||||
const result = Array.from(fieldNames);
|
||||
const [removed] = result.splice(startIndex, 1);
|
||||
result.splice(endIndex, 0, removed);
|
||||
|
||||
return result.reduce((nameByIndex, fieldName, index) => {
|
||||
nameByIndex[fieldName] = index;
|
||||
return nameByIndex;
|
||||
}, {} as Record<string, number>);
|
||||
};
|
||||
|
||||
const orderFieldNamesByIndex = (fieldNames: string[], indexByName: Record<string, number> = {}): string[] => {
|
||||
if (!indexByName || Object.keys(indexByName).length === 0) {
|
||||
return fieldNames;
|
||||
}
|
||||
const comparer = createFieldsComparer(indexByName);
|
||||
return fieldNames.sort(comparer);
|
||||
};
|
||||
|
||||
const fieldNamesFromInput = (input: DataFrame[]): string[] => {
|
||||
if (!Array.isArray(input)) {
|
||||
return [] as string[];
|
||||
}
|
||||
|
||||
return Object.keys(
|
||||
input.reduce((names, frame) => {
|
||||
if (!frame || !Array.isArray(frame.fields)) {
|
||||
return names;
|
||||
}
|
||||
|
||||
return frame.fields.reduce((names, field) => {
|
||||
names[field.name] = null;
|
||||
return names;
|
||||
}, names);
|
||||
}, {} as Record<string, null>)
|
||||
);
|
||||
};
|
||||
|
||||
export const organizeFieldsTransformRegistryItem: TransformerUIRegistyItem<OrganizeFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.organize,
|
||||
component: OrganizeFieldsTransformerEditor,
|
||||
transformer: transformersRegistry.get(DataTransformerID.organize),
|
||||
name: 'Organize fields',
|
||||
description: 'UI for organizing fields',
|
||||
};
|
@ -3,11 +3,13 @@ import { reduceTransformRegistryItem } from './ReduceTransformerEditor';
|
||||
import { filterFieldsByNameTransformRegistryItem } from './FilterByNameTransformerEditor';
|
||||
import { filterFramesByRefIdTransformRegistryItem } from './FilterByRefIdTransformerEditor';
|
||||
import { TransformerUIRegistyItem } from './types';
|
||||
import { organizeFieldsTransformRegistryItem } from './OrganizeFieldsTransformerEditor';
|
||||
|
||||
export const transformersUIRegistry = new Registry<TransformerUIRegistyItem<any>>(() => {
|
||||
return [
|
||||
reduceTransformRegistryItem,
|
||||
filterFieldsByNameTransformRegistryItem,
|
||||
filterFramesByRefIdTransformRegistryItem,
|
||||
organizeFieldsTransformRegistryItem,
|
||||
];
|
||||
});
|
||||
|
@ -103,8 +103,6 @@ export { DataLinkInput } from './DataLinks/DataLinkInput';
|
||||
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
|
||||
export { SeriesIcon } from './Legend/SeriesIcon';
|
||||
export { transformersUIRegistry } from './TransformersUI/transformers';
|
||||
export { TransformationRow } from './TransformersUI/TransformationRow';
|
||||
export { TransformationsEditor } from './TransformersUI/TransformationsEditor';
|
||||
export { JSONFormatter } from './JSONFormatter/JSONFormatter';
|
||||
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
||||
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { config } from 'app/core/config';
|
||||
import { css } from 'emotion';
|
||||
import { TabsBar, Tab, stylesFactory, TabContent, TransformationsEditor } from '@grafana/ui';
|
||||
import { TabsBar, Tab, stylesFactory, TabContent } from '@grafana/ui';
|
||||
import { DataTransformerConfig, LoadingState, PanelData } from '@grafana/data';
|
||||
import { PanelEditorTab, PanelEditorTabId } from './types';
|
||||
import { DashboardModel } from '../../state';
|
||||
@ -9,6 +9,7 @@ import { QueriesTab } from '../../panel_editor/QueriesTab';
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
import { AlertTab } from 'app/features/alerting/AlertTab';
|
||||
import { VisualizationTab } from './VisualizationTab';
|
||||
import { TransformationsEditor } from '../TransformationsEditor/TransformationsEditor';
|
||||
|
||||
interface PanelEditorTabsProps {
|
||||
panel: PanelModel;
|
||||
|
@ -17,7 +17,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod
|
||||
const panel = dashboard.initPanelEditor(sourcePanel);
|
||||
|
||||
const queryRunner = panel.getQueryRunner();
|
||||
const querySubscription = queryRunner.getData().subscribe({
|
||||
const querySubscription = queryRunner.getData(false).subscribe({
|
||||
next: (data: PanelData) => dispatch(setEditorPanelData(data)),
|
||||
});
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { css } from 'emotion';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { JSONFormatter } from '../JSONFormatter/JSONFormatter';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { JSONFormatter, ThemeContext } from '@grafana/ui';
|
||||
import { GrafanaTheme, DataFrame } from '@grafana/data';
|
||||
|
||||
interface TransformationRowProps {
|
||||
name: string;
|
||||
@ -13,6 +11,39 @@ interface TransformationRowProps {
|
||||
input: DataFrame[];
|
||||
}
|
||||
|
||||
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const [viewDebug, setViewDebug] = useState(false);
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.iconRow}>
|
||||
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-bug" />
|
||||
</div>
|
||||
<div onClick={onRemove} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-trash" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.editor}>
|
||||
{editor}
|
||||
{viewDebug && (
|
||||
<div>
|
||||
<JSONFormatter json={input} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
title: css`
|
||||
display: flex;
|
||||
@ -50,36 +81,3 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
padding: 8px;
|
||||
`,
|
||||
});
|
||||
|
||||
export const TransformationRow = ({ onRemove, editor, name, input }: TransformationRowProps) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const [viewDebug, setViewDebug] = useState(false);
|
||||
const styles = getStyles(theme);
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
margin-bottom: 10px;
|
||||
`}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.iconRow}>
|
||||
<div onClick={() => setViewDebug(!viewDebug)} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-bug" />
|
||||
</div>
|
||||
<div onClick={onRemove} className={styles.icon}>
|
||||
<i className="fa fa-fw fa-trash" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.editor}>
|
||||
{editor}
|
||||
{viewDebug && (
|
||||
<div>
|
||||
<JSONFormatter json={input} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,22 +1,21 @@
|
||||
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
|
||||
import { Select } from '../Forms/Legacy/Select/Select';
|
||||
import { transformersUIRegistry } from './transformers';
|
||||
import React from 'react';
|
||||
import { TransformationRow } from './TransformationRow';
|
||||
import { Button } from '../Button';
|
||||
import { css } from 'emotion';
|
||||
import React from 'react';
|
||||
import { transformersUIRegistry } from '@grafana/ui/src/components/TransformersUI/transformers';
|
||||
import { DataTransformerID, DataTransformerConfig, DataFrame, transformDataFrame } from '@grafana/data';
|
||||
import { Button, Select } from '@grafana/ui';
|
||||
import { TransformationRow } from './TransformationRow';
|
||||
|
||||
interface TransformationsEditorState {
|
||||
updateCounter: number;
|
||||
}
|
||||
|
||||
interface TransformationsEditorProps {
|
||||
interface Props {
|
||||
onChange: (transformations: DataTransformerConfig[]) => void;
|
||||
transformations: DataTransformerConfig[];
|
||||
dataFrames: DataFrame[];
|
||||
}
|
||||
|
||||
export class TransformationsEditor extends React.PureComponent<TransformationsEditorProps, TransformationsEditorState> {
|
||||
interface State {
|
||||
updateCounter: number;
|
||||
}
|
||||
|
||||
export class TransformationsEditor extends React.PureComponent<Props, State> {
|
||||
state = { updateCounter: 0 };
|
||||
|
||||
onTransformationAdd = () => {
|
||||
@ -116,12 +115,16 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<div className="panel-editor__content">
|
||||
<p className="muted text-center" style={{ padding: '8px' }}>
|
||||
Transformations allow you to combine, re-order, hide and rename specific parts the the data set before being
|
||||
visualized.
|
||||
</p>
|
||||
{this.renderTransformationEditors()}
|
||||
<Button variant="secondary" icon="fa fa-plus" onClick={this.onTransformationAdd}>
|
||||
Add transformation
|
||||
</Button>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import { ReplaySubject, Unsubscribable, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// Services & Utils
|
||||
import { config } from 'app/core/config';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
@ -70,37 +69,38 @@ export class PanelQueryRunner {
|
||||
return this.subject.pipe(
|
||||
map((data: PanelData) => {
|
||||
let processedData = data;
|
||||
// apply transformations
|
||||
if (transform && this.hasTransformations()) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
|
||||
};
|
||||
|
||||
// Apply transformations
|
||||
|
||||
if (transform) {
|
||||
const transformations = this.dataConfigSource.getTransformations();
|
||||
|
||||
if (transformations && transformations.length > 0) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: transformDataFrame(this.dataConfigSource.getTransformations(), data.series),
|
||||
};
|
||||
}
|
||||
}
|
||||
// apply overrides
|
||||
if (this.hasFieldOverrideOptions()) {
|
||||
|
||||
// Apply field defaults & overrides
|
||||
const fieldConfig = this.dataConfigSource.getFieldOverrideOptions();
|
||||
|
||||
if (fieldConfig) {
|
||||
processedData = {
|
||||
...processedData,
|
||||
series: applyFieldOverrides({
|
||||
data: processedData.series,
|
||||
...this.dataConfigSource.getFieldOverrideOptions(),
|
||||
...fieldConfig,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return processedData;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
hasTransformations = () => {
|
||||
const transformations = this.dataConfigSource.getTransformations();
|
||||
return config.featureToggles.transformations && transformations && transformations.length > 0;
|
||||
};
|
||||
|
||||
hasFieldOverrideOptions = () => {
|
||||
return this.dataConfigSource.getFieldOverrideOptions();
|
||||
};
|
||||
|
||||
async run(options: QueryRunnerOptions) {
|
||||
const {
|
||||
queries,
|
||||
|
@ -70,7 +70,7 @@
|
||||
}
|
||||
|
||||
.panel-editor__content {
|
||||
padding: 15px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.panel-in-fullscreen {
|
||||
|
88
yarn.lock
88
yarn.lock
@ -5794,6 +5794,13 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-beautiful-dnd@12.1.2":
|
||||
version "12.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-12.1.2.tgz#dfd1bdb072e92c1363e5f7a4c1842eaf95f77b21"
|
||||
integrity sha512-h+0mA4cHmzL4BhyCniB6ZSSZhfO9LpXXbnhdAfa2k7klS03woiOT+Dh5AchY6eoQXk3vQVtqn40YY3u+MwFs8A==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-color@3.0.1", "@types/react-color@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.1.tgz#5433e2f503ea0e0831cbc6fd0c20f8157d93add0"
|
||||
@ -8138,16 +8145,6 @@ browserslist@^4.8.5:
|
||||
electron-to-chromium "^1.3.341"
|
||||
node-releases "^1.1.47"
|
||||
|
||||
browserslist@^4.9.1:
|
||||
version "4.11.0"
|
||||
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.0.tgz#aef4357b10a8abda00f97aac7cd587b2082ba1ad"
|
||||
integrity sha512-WqEC7Yr5wUH5sg6ruR++v2SGOQYpyUdYYd4tZoAq1F7y+QXoLoYGXVbxhtaIqWmAJjtNTRjVD3HuJc1OXTel2A==
|
||||
dependencies:
|
||||
caniuse-lite "^1.0.30001035"
|
||||
electron-to-chromium "^1.3.380"
|
||||
node-releases "^1.1.52"
|
||||
pkg-up "^3.1.0"
|
||||
|
||||
bs-logger@0.x:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8"
|
||||
@ -8445,6 +8442,11 @@ caniuse-api@^3.0.0:
|
||||
lodash.memoize "^4.1.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-db@1.0.30000772:
|
||||
version "1.0.30000772"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000772.tgz#51aae891768286eade4a3d8319ea76d6a01b512b"
|
||||
integrity sha1-UarokXaChureSj2DGep21qAbUSs=
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000999:
|
||||
version "1.0.30000999"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz#427253a69ad7bea4aa8d8345687b8eec51ca0e43"
|
||||
@ -8465,11 +8467,6 @@ caniuse-lite@^1.0.30001023:
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001027.tgz#283e2ef17d94889cc216a22c6f85303d78ca852d"
|
||||
integrity sha512-7xvKeErvXZFtUItTHgNtLgS9RJpVnwBlWX8jSo/BO8VsF6deszemZSkJJJA1KOKrXuzZH4WALpAJdq5EyfgMLg==
|
||||
|
||||
caniuse-lite@^1.0.30001035:
|
||||
version "1.0.30001036"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001036.tgz#930ea5272010d8bf190d859159d757c0b398caf0"
|
||||
integrity sha512-jU8CIFIj2oR7r4W+5AKcsvWNVIb6Q6OZE3UsrXrZBHFtreT4YgTeOJtTucp+zSedEpTi3L5wASSP0LYIE3if6w==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
|
||||
@ -9702,6 +9699,13 @@ css-blank-pseudo@^0.1.4:
|
||||
dependencies:
|
||||
postcss "^7.0.5"
|
||||
|
||||
css-box-model@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.0.tgz#3a26377b4162b3200d2ede4b064ec5b6a75186d0"
|
||||
integrity sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==
|
||||
dependencies:
|
||||
tiny-invariant "^1.0.6"
|
||||
|
||||
css-color-names@0.0.4, css-color-names@^0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0"
|
||||
@ -10930,6 +10934,11 @@ electron-to-chromium@^1.3.341:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.349.tgz#663f26a69d348a462df47b4d7ab162a2f29bbcb7"
|
||||
integrity sha512-uEb2zs6EJ6OZIqaMsCSliYVgzE/f7/s1fLWqtvRtHg/v5KBF2xds974fUnyatfxIDgkqzQVwFtam5KExqywx0Q==
|
||||
|
||||
electron-to-chromium@^1.3.378:
|
||||
version "1.3.395"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.395.tgz#2c531a0477bcc41deb440877d1f27509ed286aed"
|
||||
integrity sha512-kdn2cX6hZXDdz/O2Q8tZscITlsSv1a/7bOq/fQs7QAJ9iaRlnhZPccarNhxZv1tXgmgwCnKp/1lJNYLOG8Dxiw==
|
||||
|
||||
electron-to-chromium@^1.3.380:
|
||||
version "1.3.381"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.381.tgz#952678ff91a5f36175a3832358a6dd2de3bf62b7"
|
||||
@ -16686,7 +16695,7 @@ mem@^4.0.0:
|
||||
mimic-fn "^2.0.0"
|
||||
p-is-promise "^2.0.0"
|
||||
|
||||
memoize-one@5.1.1, memoize-one@^5.0.0:
|
||||
memoize-one@5.1.1, "memoize-one@>=3.1.1 <6", memoize-one@^5.0.0, memoize-one@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
@ -20080,6 +20089,11 @@ quick-lru@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
|
||||
integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
|
||||
|
||||
raf-schd@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||
integrity sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ==
|
||||
|
||||
raf@^3.1.0, raf@^3.4.0, raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
@ -20318,6 +20332,19 @@ react-addons-create-fragment@^15.6.2:
|
||||
loose-envify "^1.3.1"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
react-beautiful-dnd@13.0.0:
|
||||
version "13.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#f70cc8ff82b84bc718f8af157c9f95757a6c3b40"
|
||||
integrity sha512-87It8sN0ineoC3nBW0SbQuTFXM6bUqM62uJGY4BtTf0yzPl8/3+bHMWkgIe0Z6m8e+gJgjWxefGRVfpE3VcdEg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.4"
|
||||
css-box-model "^1.2.0"
|
||||
memoize-one "^5.1.1"
|
||||
raf-schd "^4.0.2"
|
||||
react-redux "^7.1.1"
|
||||
redux "^4.0.4"
|
||||
use-memo-one "^1.1.1"
|
||||
|
||||
react-calendar@2.19.2:
|
||||
version "2.19.2"
|
||||
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.19.2.tgz#496e78eb11a00aee1ae6b5d02d221ed1ca2db952"
|
||||
@ -20694,6 +20721,17 @@ react-redux@7.1.1:
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-redux@^7.1.1:
|
||||
version "7.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.0.tgz#f970f62192b3981642fec46fd0db18a074fe879d"
|
||||
integrity sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.5.5"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-resizable@^1.9.0:
|
||||
version "1.10.1"
|
||||
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.10.1.tgz#f0c2cf1d83b3470b87676ce6d6b02bbe3f4d8cd4"
|
||||
@ -21207,6 +21245,14 @@ redux@^3.6.0:
|
||||
loose-envify "^1.1.0"
|
||||
symbol-observable "^1.0.3"
|
||||
|
||||
redux@^4.0.4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
|
||||
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
symbol-observable "^1.2.0"
|
||||
|
||||
reflect.ownkeys@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"
|
||||
@ -23920,6 +23966,11 @@ tiny-invariant@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
|
||||
integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==
|
||||
|
||||
tiny-invariant@^1.0.6:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||
|
||||
tiny-warning@^0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-0.0.3.tgz#1807eb4c5f81784a6354d58ea1d5024f18c6c81f"
|
||||
@ -24718,6 +24769,11 @@ use-callback-ref@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.1.tgz#898759ccb9e14be6c7a860abafa3ffbd826c89bb"
|
||||
integrity sha512-C3nvxh0ZpaOxs9RCnWwAJ+7bJPwQI8LHF71LzbQ3BvzH5XkdtlkMadqElGevg5bYBDFip4sAnD4m06zAKebg1w==
|
||||
|
||||
use-memo-one@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.1.tgz#39e6f08fe27e422a7d7b234b5f9056af313bd22c"
|
||||
integrity sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ==
|
||||
|
||||
use-sidecar@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.2.tgz#e72f582a75842f7de4ef8becd6235a4720ad8af6"
|
||||
|
Loading…
Reference in New Issue
Block a user