Merge pull request #294 from CatalystCode/add-dialog-template
Add dialog template
This commit is contained in:
Коммит
e05ec65b1e
|
@ -10,6 +10,10 @@ interface IDataSource {
|
|||
id: string,
|
||||
dependencies?: IStringDictionary,
|
||||
params?: IDictionary,
|
||||
format?: string | {
|
||||
type: string,
|
||||
args?: IDictionary
|
||||
}
|
||||
calculated?: (state: any, dependencies?: any, prevState?: any) => IDictionary
|
||||
}
|
||||
|
||||
|
|
|
@ -181,11 +181,11 @@ class SettingsStore extends AbstractStoreModel<ISettingsStoreState> implements I
|
|||
const forkedQueryComponents = dependencyProperty.split('-');
|
||||
const params = datasources[datasource].config.params;
|
||||
|
||||
const isForked = !params.query && !!params.table;
|
||||
const isForked = params && !params.query && !!params.table;
|
||||
|
||||
if (!isForked) {
|
||||
// unforked
|
||||
queryFn = params.query;
|
||||
queryFn = params && params.query || 'n/a';
|
||||
} else {
|
||||
// forked
|
||||
if (!params.queries[queryId] && forkedQueryComponents.length === 2) {
|
||||
|
|
|
@ -9,8 +9,6 @@ import Snackbar from 'react-md/lib/Snackbars';
|
|||
import SpinnerStore, { ISpinnerStoreState } from './SpinnerStore';
|
||||
import SpinnerActions from './SpinnerActions';
|
||||
|
||||
import {Toast, ToastActions, IToast} from '../Toast';
|
||||
|
||||
interface ISpinnerState extends ISpinnerStoreState {
|
||||
}
|
||||
|
||||
|
@ -22,34 +20,6 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
|
|||
this.state = SpinnerStore.getState();
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this._429ApplicationInsights = this._429ApplicationInsights.bind(this);
|
||||
|
||||
var self = this;
|
||||
var openOriginal = XMLHttpRequest.prototype.open;
|
||||
var sendOriginal = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) {
|
||||
SpinnerActions.startRequestLoading.defer(null);
|
||||
openOriginal.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(data: any) {
|
||||
let _xhr: XMLHttpRequest = this;
|
||||
_xhr.onreadystatechange = (response) => {
|
||||
|
||||
// readyState === 4: means the response is complete
|
||||
if (_xhr.readyState === 4) {
|
||||
SpinnerActions.endRequestLoading.defer(null);
|
||||
|
||||
if (_xhr.status === 429) {
|
||||
self._429ApplicationInsights();
|
||||
}
|
||||
}
|
||||
};
|
||||
sendOriginal.apply(_xhr, arguments);
|
||||
};
|
||||
|
||||
// Todo: Add timeout to requests - if no reply received, turn spinner off
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -61,11 +31,6 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
|
|||
SpinnerStore.unlisten(this.onChange);
|
||||
}
|
||||
|
||||
_429ApplicationInsights() {
|
||||
let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
|
||||
ToastActions.addToast(toast);
|
||||
}
|
||||
|
||||
onChange(state: ISpinnerState) {
|
||||
this.setState(state);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import alt, { AbstractStoreModel } from '../../alt';
|
||||
|
||||
import {Toast, ToastActions, IToast} from '../Toast';
|
||||
import spinnerActions from './SpinnerActions';
|
||||
|
||||
export interface ISpinnerStoreState {
|
||||
|
@ -7,6 +8,35 @@ export interface ISpinnerStoreState {
|
|||
requestLoading?: number;
|
||||
}
|
||||
|
||||
const openOriginal = XMLHttpRequest.prototype.open;
|
||||
const sendOriginal = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) {
|
||||
spinnerActions.startRequestLoading.defer(null);
|
||||
openOriginal.apply(this, arguments);
|
||||
};
|
||||
|
||||
XMLHttpRequest.prototype.send = function(data: any) {
|
||||
let _xhr: XMLHttpRequest = this;
|
||||
_xhr.onreadystatechange = (response) => {
|
||||
|
||||
// readyState === 4: means the response is complete
|
||||
if (_xhr.readyState === 4) {
|
||||
spinnerActions.endRequestLoading.defer(null);
|
||||
|
||||
if (_xhr.status === 429) {
|
||||
_429ApplicationInsights();
|
||||
}
|
||||
}
|
||||
};
|
||||
sendOriginal.apply(_xhr, arguments);
|
||||
};
|
||||
|
||||
function _429ApplicationInsights() {
|
||||
let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
|
||||
ToastActions.addToast(toast);
|
||||
}
|
||||
|
||||
class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISpinnerStoreState {
|
||||
|
||||
pageLoading: number;
|
||||
|
@ -43,6 +73,6 @@ class SpinnerStore extends AbstractStoreModel<ISpinnerStoreState> implements ISp
|
|||
}
|
||||
}
|
||||
|
||||
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore, 'SpinnerStore');
|
||||
const spinnerStore = alt.createStore<ISpinnerStoreState>(SpinnerStore as any, 'SpinnerStore');
|
||||
|
||||
export default spinnerStore;
|
||||
|
|
|
@ -30,7 +30,7 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
static editor = settings;
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
values: GenericComponent.sourceFormat(source, 'values'),
|
||||
values: GenericComponent.sourceFormat(source, 'bar-values'),
|
||||
bars: GenericComponent.sourceFormat(source, 'bars')
|
||||
};
|
||||
}
|
||||
|
|
|
@ -63,6 +63,10 @@ export default class Dialog extends React.PureComponent<IDialogProps, IDialogSta
|
|||
DialogsStore.listen(this.onChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
DialogsStore.unlisten(this.onChange);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const { dialogData } = this.props;
|
||||
var { dialogId, dialogArgs } = this.state;
|
||||
|
|
|
@ -234,18 +234,30 @@ export class DataSourceConnector {
|
|||
sourceDS.store.listen((state) => {
|
||||
|
||||
Object.keys(this.dataSources).forEach(checkDSId => {
|
||||
var checkDS = this.dataSources[checkDSId];
|
||||
var dependencies = checkDS.plugin.getDependencies() || {};
|
||||
let checkDS = this.dataSources[checkDSId];
|
||||
let dependencies = checkDS.plugin.getDependencies() || {};
|
||||
|
||||
let populatedDependencies = {};
|
||||
let connected = _.find(_.keys(dependencies), dependencyKey => {
|
||||
let dependencyValue = dependencies[dependencyKey] || '';
|
||||
return (dependencyValue === sourceDS.id || dependencyValue.startsWith(sourceDS.id + ':'));
|
||||
if (typeof dependencyValue === 'string' && dependencyValue.length > 0) {
|
||||
if (dependencyValue === sourceDS.id) {
|
||||
let defaultProperty = sourceDS.plugin.defaultProperty || 'value';
|
||||
populatedDependencies[dependencyKey] = state[defaultProperty];
|
||||
return true;
|
||||
} else if (dependencyValue.startsWith(sourceDS.id + ':')) {
|
||||
let property = dependencyValue.substr(sourceDS.id.length + 1);
|
||||
populatedDependencies[dependencyKey] = _.get(state, property);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (connected) {
|
||||
|
||||
// Todo: add check that all dependencies are met
|
||||
checkDS.action.updateDependencies.defer(state);
|
||||
checkDS.action.updateDependencies.defer(populatedDependencies);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -338,7 +350,7 @@ export class DataSourceConnector {
|
|||
|
||||
private static callibrateResult(result: any, plugin: IDataSourcePlugin, dependencies: IDictionary): any {
|
||||
|
||||
var defaultProperty = plugin.defaultProperty || 'value';
|
||||
let defaultProperty = plugin.defaultProperty || 'value';
|
||||
|
||||
// In case result is not an object, push result into an object
|
||||
if (typeof result !== 'object') {
|
||||
|
@ -353,24 +365,26 @@ export class DataSourceConnector {
|
|||
let state = DataSourceConnector.dataSources[plugin._props.id].store.getState();
|
||||
state = _.extend(state, result);
|
||||
|
||||
if (typeof calculated === 'function') {
|
||||
let additionalValues = calculated(state, dependencies) || {};
|
||||
Object.assign(result, additionalValues);
|
||||
}
|
||||
|
||||
if (Array.isArray(calculated)) {
|
||||
calculated.forEach(calc => {
|
||||
let additionalValues = calc(state, dependencies) || {};
|
||||
Object.assign(result, additionalValues);
|
||||
});
|
||||
}
|
||||
|
||||
state = _.extend(state, result);
|
||||
|
||||
let format = plugin.getFormat();
|
||||
let formatExtract = DataSourceConnector.handleDataFormat(format, plugin, state, dependencies);
|
||||
if (formatExtract) {
|
||||
Object.assign(result, formatExtract);
|
||||
}
|
||||
|
||||
if (typeof calculated === 'function') {
|
||||
let additionalValues = calculated(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => { result[key] = additionalValues[key]; });
|
||||
}
|
||||
|
||||
if (Array.isArray(calculated)) {
|
||||
calculated.forEach(calc => {
|
||||
let additionalValues = calc(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => { result[key] = additionalValues[key]; });
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import * as _ from 'lodash';
|
||||
import { IDataSource } from '../DataSourceConnector';
|
||||
import { ToastActions } from '../../components/Toast';
|
||||
import { DataFormatTypes, IDataFormat } from '../../utils/data-formats';
|
||||
|
@ -142,7 +143,7 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
}
|
||||
|
||||
getParams(): T {
|
||||
return this._props.params;
|
||||
return _.cloneDeep(this._props.params);
|
||||
}
|
||||
|
||||
getFormat(): string | IDataFormat {
|
||||
|
|
|
@ -8,8 +8,8 @@ interface ISampleParams {
|
|||
|
||||
export default class Sample extends DataSourcePlugin<ISampleParams> {
|
||||
|
||||
type = 'Constant';
|
||||
defaultProperty = 'selectedValue';
|
||||
type = 'Sample';
|
||||
defaultProperty = 'values';
|
||||
|
||||
constructor(options: IOptions<ISampleParams>, connections: IDict<IStringDictionary>) {
|
||||
super(options, connections);
|
||||
|
@ -21,7 +21,7 @@ export default class Sample extends DataSourcePlugin<ISampleParams> {
|
|||
}
|
||||
|
||||
initialize() {
|
||||
let { samples } = <any> this._props.params;
|
||||
let { samples } = _.cloneDeep(this._props.params);
|
||||
return samples || {};
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,10 @@ export default class Sample extends DataSourcePlugin<ISampleParams> {
|
|||
* updateDependencies - called when dependencies are created
|
||||
*/
|
||||
dependenciesUpdated(dependencies: IDictionary, args: IDictionary, callback: (result: any) => void) {
|
||||
var result = _.extend(dependencies, args);
|
||||
let result = _.extend(dependencies, args);
|
||||
let { samples } = this.getParams();
|
||||
|
||||
_.extend(result, samples);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
return callback(result);
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { IDataSourceDictionary } from '../../data-sources';
|
||||
import { setupTests } from '../utils/setup';
|
||||
import Sample from '../../data-sources/plugins/Sample';
|
||||
|
||||
import * as testCases from './format-values';
|
||||
import * as formats from '../../utils/data-formats';
|
||||
|
||||
describe('Data Source: Application Insights: Forked Query', () => {
|
||||
|
||||
let mockPlugin = new Sample(<any>{
|
||||
params: {
|
||||
values: [ 'value 1', 'value 2', 'value 3' ],
|
||||
samples: {
|
||||
values: [ 'value1', 'value2', 'value3' ]
|
||||
}
|
||||
}
|
||||
}, {});
|
||||
|
||||
Object.keys(testCases.tests).forEach(testFormat => {
|
||||
|
||||
it ('Check data format ' + testFormat, () => {
|
||||
|
||||
let test = testCases.tests[testFormat];
|
||||
let values = test.state;
|
||||
|
||||
let result = formats[testFormat](test.format, test.state, {}, mockPlugin, {});
|
||||
expect(result).toMatchObject(test.expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,91 @@
|
|||
|
||||
export interface IFormatTest {
|
||||
format: any,
|
||||
state: any,
|
||||
expected: any
|
||||
};
|
||||
|
||||
export const tests: IDict<IFormatTest> = {
|
||||
bars: {
|
||||
format: {
|
||||
type: 'bars',
|
||||
args: {
|
||||
barsField: 'barField',
|
||||
seriesField: 'seriesField'
|
||||
}
|
||||
},
|
||||
state: {
|
||||
values: [
|
||||
{ count: 10, barField: 'bar 1', seriesField: 'series1Value' },
|
||||
{ count: 15, barField: 'bar 2', seriesField: 'series1Value' },
|
||||
{ count: 20, barField: 'bar 1', seriesField: 'series2Value' },
|
||||
{ count: 44, barField: 'bar 3', seriesField: 'series2Value' },
|
||||
]
|
||||
},
|
||||
expected: {
|
||||
'bars': ['series1Value', 'series2Value'],
|
||||
'bar-values': [
|
||||
{'series1Value': 10, 'series2Value': 20, 'value': 'bar 1'},
|
||||
{'series1Value': 15, 'value': 'bar 2'},
|
||||
{'series2Value': 44, 'value': 'bar 3'}
|
||||
]
|
||||
}
|
||||
},
|
||||
filter: {
|
||||
format: 'filter',
|
||||
state: {
|
||||
values: [
|
||||
{ value: 'value 1' },
|
||||
{ value: 'value 2' },
|
||||
{ value: 'value 3' }
|
||||
]
|
||||
},
|
||||
expected: {
|
||||
'values-all': [ 'value 1', 'value 2', 'value 3' ],
|
||||
'values-selected': [ ],
|
||||
}
|
||||
},
|
||||
flags: {
|
||||
format: 'flags',
|
||||
state: { values: [ 'value 1', 'value 2', 'value 3' ] },
|
||||
expected: {
|
||||
'value 1': false,
|
||||
'value 2': false,
|
||||
'value 3': false,
|
||||
'values-all': [ 'value 1', 'value 2', 'value 3' ],
|
||||
'values-selected': [],
|
||||
}
|
||||
},
|
||||
retention: {
|
||||
format: 'retention',
|
||||
state: {
|
||||
values: [
|
||||
{
|
||||
totalUnique: 10,
|
||||
totalUniqueUsersIn24hr: 5,
|
||||
totalUniqueUsersIn7d: 7,
|
||||
totalUniqueUsersIn30d: 10,
|
||||
returning24hr: 3,
|
||||
returning7d: 3,
|
||||
returning30d: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
expected: {
|
||||
"returning": 0,
|
||||
"returning24hr": 3,
|
||||
"returning30d": 6,
|
||||
"returning7d": 3,
|
||||
"total": 0,
|
||||
"totalUnique": 10,
|
||||
"totalUniqueUsersIn24hr": 5,
|
||||
"totalUniqueUsersIn30d": 10,
|
||||
"totalUniqueUsersIn7d": 7,
|
||||
"values": [
|
||||
{ "retention": "60%", "returning": 3, "timespan": "24 hours", "unique": 5 },
|
||||
{ "retention": "43%", "returning": 3, "timespan": "7 days", "unique": 7 },
|
||||
{ "retention": "60%", "returning": 6, "timespan": "30 days", "unique": 10 }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
|
@ -32,6 +32,17 @@ describe('SplitPanel', () => {
|
|||
})
|
||||
|
||||
it('Render inside a Card (+ Table inside a Card)', () => {
|
||||
dataSources['samples'].action.updateDependencies({
|
||||
groups: [
|
||||
{ title: "value1", subtitle: "subvalue1", count: 60 },
|
||||
{ title: "value2", subtitle: "subvalue2", count: 60 },
|
||||
],
|
||||
values: [
|
||||
{ id: "value1", count: 60 },
|
||||
{ id: "value2", count: 10 },
|
||||
{ id: "value3", count: 30 }
|
||||
]
|
||||
});
|
||||
let card = TestUtils.scryRenderedComponentsWithType(splitpanel, Card);
|
||||
expect(card).toHaveLength(2);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,9 @@ describe('Table', () => {
|
|||
})
|
||||
|
||||
it('Render inside a Card', () => {
|
||||
dataSources['samples'].action.updateDependencies({
|
||||
'table-values': dataSources['samples'].store.state['values']
|
||||
});
|
||||
let card = TestUtils.scryRenderedComponentsWithType(table, Card);
|
||||
expect(card.length).toBe(1);
|
||||
});
|
||||
|
@ -46,7 +49,7 @@ describe('Table', () => {
|
|||
|
||||
it('Rows == 0', () => {
|
||||
dataSources['samples'].action.updateDependencies({
|
||||
values: []
|
||||
'table-values': []
|
||||
});
|
||||
let rows = TestUtils.scryRenderedComponentsWithType(table, TableRow);
|
||||
expect(rows.length).toBe(1);
|
||||
|
|
|
@ -6,17 +6,7 @@ dashboard.dataSources.push(
|
|||
id: "samples",
|
||||
type: "Sample",
|
||||
params: {
|
||||
samples: {
|
||||
groups: [
|
||||
{ title: "value1", subtitle: "subvalue1", count: 60 },
|
||||
{ title: "value2", subtitle: "subvalue2", count: 60 },
|
||||
],
|
||||
values: [
|
||||
{ id: "value1", count: 60 },
|
||||
{ id: "value2", count: 10 },
|
||||
{ id: "value3", count: 30 }
|
||||
]
|
||||
}
|
||||
samples: { }
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ dashboard.elements.push({
|
|||
size: { w: 1, h: 1 },
|
||||
title: 'Table',
|
||||
subtitle: 'Table',
|
||||
dependencies: { values: 'samples:values' },
|
||||
dependencies: { values: 'samples:table-values' },
|
||||
props: {
|
||||
cols: [
|
||||
{
|
||||
|
|
|
@ -63,7 +63,7 @@ export function bars(
|
|||
const threshold = args.threshold || 0;
|
||||
const othersValue = args.othersValue || 'Others';
|
||||
|
||||
let values: any[] = state.values;
|
||||
let values: any[] = state.values || [];
|
||||
|
||||
// Concating values with '...'
|
||||
if (values && values.length && valueMaxLength && (seriesField || barsField)) {
|
||||
|
@ -98,11 +98,11 @@ export function bars(
|
|||
});
|
||||
|
||||
result[prefix + 'bars'] = _.keys(series);
|
||||
result[prefix + 'values'] = _.values(barValues);
|
||||
result[prefix + 'bar-values'] = _.values(barValues);
|
||||
|
||||
} else {
|
||||
result[prefix + 'bars'] = [ valueField ];
|
||||
result[prefix + 'values'] = values;
|
||||
result[prefix + 'bar-values'] = values;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -53,14 +53,12 @@ export function filter (
|
|||
// is going to be reset into an empty set.
|
||||
// For this reason, using previous state to copy filter
|
||||
const filters = values.map(x => x[field] || unknown);
|
||||
let selectedValues = [];
|
||||
if (prevState[prefix + 'values-selected'] !== undefined) {
|
||||
selectedValues = prevState[prefix + 'values-selected'];
|
||||
}
|
||||
|
||||
let result = {};
|
||||
result[prefix + 'values-all'] = filters;
|
||||
result[prefix + 'values-selected'] = selectedValues;
|
||||
|
||||
if (prevState[prefix + 'values-selected'] === undefined) {
|
||||
result[prefix + 'values-selected'] = [];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Applies selected filters to sample JSON data
|
||||
* Returns filtered data using a prefix.
|
||||
*
|
||||
* @param format {
|
||||
* type: 'filtered_samples',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the filtered sample data (defaults to 'filtered').
|
||||
* }
|
||||
* }
|
||||
* @param state Current received state from data source
|
||||
* @param dependencies Dependencies for the plugin
|
||||
* @param plugin The entire plugin (for id generation, params etc...)
|
||||
* @param prevState The previous state to compare for changing filters
|
||||
*/
|
||||
export function filtered_samples (
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
let result = {};
|
||||
|
||||
if (typeof format === 'string') {
|
||||
return formatWarn('format should be an object with args', 'bars', plugin);
|
||||
}
|
||||
|
||||
const args = format.args || {};
|
||||
const prefix = args['prefix'] || 'filtered';
|
||||
|
||||
const params = plugin.getParams();
|
||||
const filters = params.filters;
|
||||
|
||||
// Check for active filters selected
|
||||
const hasSelectedFilters = filters.some(filter => (
|
||||
dependencies[filter.dependency] && dependencies[filter.dependency].length > 0)
|
||||
);
|
||||
|
||||
// Apply filter to sample data
|
||||
Object.keys(dependencies).forEach(key => {
|
||||
// Skip filters
|
||||
if (filters.find(f => f.dependency === key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply filter data
|
||||
const values = dependencies[key];
|
||||
let filteredData = {};
|
||||
|
||||
// Return early if no active filters
|
||||
if (!hasSelectedFilters) {
|
||||
filteredData[prefix + '_' + key] = values;
|
||||
Object.assign(result, filteredData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get active selected filters
|
||||
const activeFilters = filters.reduce((acc, filter) => {
|
||||
if (dependencies[filter.dependency].length > 0) {
|
||||
acc.push(filter);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Apply active selected filters to sample data
|
||||
const filteredValues = values.filter(value => {
|
||||
return activeFilters.every(filter => {
|
||||
const queryProperty = filter.queryProperty;
|
||||
const selectedFilters = dependencies[filter.dependency];
|
||||
return value[queryProperty] === selectedFilters.find(selectedFilter => selectedFilter === value[queryProperty]);
|
||||
});
|
||||
});
|
||||
|
||||
filteredData[prefix + '_' + key] = filteredValues;
|
||||
Object.assign(result, filteredData);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
|
@ -52,7 +52,7 @@ export function flags(
|
|||
params.values.forEach(key => { flags[key] = state.selectedValue === key; });
|
||||
|
||||
flags[prefix + 'values-all'] = params.values;
|
||||
flags[prefix + 'values-selected'] = state.selectedValue;
|
||||
flags[prefix + 'values-selected'] = state.selectedValue || [];
|
||||
|
||||
return flags;
|
||||
}
|
|
@ -5,4 +5,5 @@ export * from './formats/flags';
|
|||
export * from './formats/retention';
|
||||
export * from './formats/scorecard';
|
||||
export * from './formats/timeline';
|
||||
export * from './formats/timespan';
|
||||
export * from './formats/timespan';
|
||||
export * from './formats/filtered-samples';
|
|
@ -0,0 +1,104 @@
|
|||
/// <reference path="../../../client/@types/types.d.ts"/>
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// The following line is important to keep in that format so it can be rendered into the page
|
||||
export const config: IDashboardConfig = /*return*/ {
|
||||
id: "sample_with_dialog",
|
||||
name: "Sample with Dialog",
|
||||
icon: "extension",
|
||||
url: "sample_with_dialog",
|
||||
description: "A basic sample to understand a basic dashboard",
|
||||
preview: "/images/sample.png",
|
||||
category: "Samples",
|
||||
html: `<div>
|
||||
This is a basic sample dashboard, with JSON based sample data source, to show how data from different data sources
|
||||
can be manipulated and connected to visual components.
|
||||
</div>`,
|
||||
config: {
|
||||
connections: { },
|
||||
layout: {
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
rowHeight: 30,
|
||||
verticalCompact: false,
|
||||
cols: { lg: 12,md: 10,sm: 6,xs: 4,xxs: 2 },
|
||||
breakpoints: { lg: 1200,md: 996,sm: 768,xs: 480,xxs: 0 }
|
||||
}
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
id: "samples",
|
||||
type: "Sample",
|
||||
params: {
|
||||
samples: {
|
||||
values: [
|
||||
{ count: 10,barField: "bar 1",seriesField: "series1Value" },
|
||||
{ count: 15,barField: "bar 2",seriesField: "series1Value" },
|
||||
{ count: 20,barField: "bar 1",seriesField: "series2Value" },
|
||||
{ count: 44,barField: "bar 3",seriesField: "series2Value" }
|
||||
]
|
||||
}
|
||||
},
|
||||
format: {
|
||||
type: "bars",
|
||||
args: { valueField: "count",barsField: "barField",seriesField: "seriesField",threshold: 10 }
|
||||
}
|
||||
}
|
||||
],
|
||||
filters: [],
|
||||
elements: [
|
||||
{
|
||||
id: "bar_sample1",
|
||||
type: "BarData",
|
||||
title: "Bars Sample 1",
|
||||
subtitle: "Description of bars sample 1",
|
||||
source: "samples",
|
||||
size: { w: 6,h: 8 },
|
||||
actions: { onBarClick: { action: "dialog:sample_dlg",params: { title: "args:value",param1: "args:value" } } }
|
||||
}
|
||||
],
|
||||
dialogs: [
|
||||
{
|
||||
id: "sample_dlg",
|
||||
params: ["title","param1"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "dialog-data",
|
||||
type: "Sample",
|
||||
dependencies: { param1_dependency: "dialog_sample_dlg:param1" },
|
||||
params: {
|
||||
samples: {
|
||||
values: [
|
||||
{ count: 199,barField: "bar 1",seriesField: "series1Value" },
|
||||
{ count: 105,barField: "bar 1",seriesField: "series1Value" },
|
||||
{ count: 203,barField: "bar 2",seriesField: "series2Value" },
|
||||
{ count: 445,barField: "bar 2",seriesField: "series2Value" }
|
||||
]
|
||||
}
|
||||
},
|
||||
calculated: (state, dependencies) => {
|
||||
let { values } = state;
|
||||
let { param1_dependency } = dependencies;
|
||||
return {
|
||||
values: _.filter(values, { barField: param1_dependency })
|
||||
};
|
||||
},
|
||||
format: {
|
||||
type: "bars",
|
||||
args: { valueField: "count",barsField: "barField",seriesField: "seriesField",threshold: 10 }
|
||||
}
|
||||
}
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: "dialog-sample-bars",
|
||||
type: "BarData",
|
||||
title: "Sample bars in dialog",
|
||||
subtitle: "Sample bars in dialog with some param filter",
|
||||
size: { w: 6,h: 8 },
|
||||
source: "dialog-data"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/// <reference path="../../../client/@types/types.d.ts"/>
|
||||
import * as _ from 'lodash';
|
||||
|
||||
// The following line is important to keep in that format so it can be rendered into the page
|
||||
export const config: IDashboardConfig = /*return*/ {
|
||||
id: 'basic_sample_filters',
|
||||
name: 'Basic Sample with Filters',
|
||||
icon: 'extension',
|
||||
url: 'basic_sample_filters',
|
||||
description: 'A basic sample to understand a basic dashboard',
|
||||
preview: '/images/sample.png',
|
||||
category: 'Samples',
|
||||
html: `
|
||||
<div>
|
||||
This is a basic sample dashboard, with JSON based sample data source, to show how data from different data sources
|
||||
can be filtered and connected to visual components.
|
||||
</div>
|
||||
`,
|
||||
config: {
|
||||
connections: {},
|
||||
layout: {
|
||||
isDraggable: true,
|
||||
isResizable: true,
|
||||
rowHeight: 30,
|
||||
verticalCompact: false,
|
||||
cols: { lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 },
|
||||
breakpoints: { lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }
|
||||
}
|
||||
},
|
||||
dataSources: [
|
||||
{
|
||||
id: 'filter1',
|
||||
type: 'Sample',
|
||||
format: 'filter',
|
||||
params: {
|
||||
samples: {
|
||||
'values': [
|
||||
{value: 'en'},
|
||||
{value: 'fr'},
|
||||
{value: 'de'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'filter2',
|
||||
type: 'Sample',
|
||||
format: 'filter',
|
||||
params: {
|
||||
samples: {
|
||||
'values': [
|
||||
{value: 'skype'},
|
||||
{value: 'messenger'},
|
||||
{value: 'slack'}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'samples',
|
||||
type: 'Sample',
|
||||
dependencies: {
|
||||
selectedFilter1: 'filter1:values-selected',
|
||||
selectedFilter2: 'filter2:values-selected',
|
||||
},
|
||||
params: {
|
||||
samples: {
|
||||
data_for_pie: [
|
||||
{ name: 'skype-en', value: 9, locale: 'en', channel: 'skype' },
|
||||
{ name: 'skype-fr', value: 8, locale: 'fr', channel: 'skype' },
|
||||
{ name: 'skype-de', value: 7, locale: 'de', channel: 'skype' },
|
||||
{ name: 'messenger-en', value: 6, locale: 'en', channel: 'messenger' },
|
||||
{ name: 'messenger-fr', value: 5, locale: 'fr', channel: 'messenger' },
|
||||
{ name: 'messenger-de', value: 4, locale: 'de', channel: 'messenger' },
|
||||
{ name: 'slack-en', value: 3, locale: 'en', channel: 'slack' },
|
||||
{ name: 'slack-fr', value: 2, locale: 'fr', channel: 'slack' },
|
||||
{ name: 'slack-de', value: 1, locale: 'de', channel: 'slack' }
|
||||
]
|
||||
},
|
||||
filters: [
|
||||
{ dependency: 'selectedFilter1', queryProperty: 'locale' },
|
||||
{ dependency: 'selectedFilter2', queryProperty: 'channel' }
|
||||
],
|
||||
},
|
||||
format: {
|
||||
type: 'filtered_samples'
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scorecard',
|
||||
type: 'Sample',
|
||||
dependencies: {
|
||||
filteredData: 'samples:filtered_data_for_pie',
|
||||
},
|
||||
calculated: (state, dependencies) => {
|
||||
const {filteredData} = state;
|
||||
const filteredDataValue = filteredData && filteredData.reduce((a, c) => a + c.value, 0) || 0;
|
||||
const filteredDataSubvalue = filteredData && filteredData.length || 0;
|
||||
return {
|
||||
'filtered_data_value': filteredDataValue,
|
||||
'filtered_data_subvalue': filteredDataSubvalue,
|
||||
};
|
||||
}
|
||||
}
|
||||
],
|
||||
filters: [
|
||||
{
|
||||
type: 'MenuFilter',
|
||||
title: 'Locale',
|
||||
subtitle: 'Select locale',
|
||||
icon: 'forum',
|
||||
source: 'filter1',
|
||||
actions: { onChange: 'filter1:updateSelectedValues:values-selected' },
|
||||
first: true
|
||||
},
|
||||
{
|
||||
type: 'MenuFilter',
|
||||
title: 'Channel',
|
||||
subtitle: 'Select channel',
|
||||
icon: 'forum',
|
||||
source: 'filter2',
|
||||
actions: { onChange: 'filter2:updateSelectedValues:values-selected' },
|
||||
first: true
|
||||
},
|
||||
],
|
||||
elements: [
|
||||
{
|
||||
id: 'pie_sample1',
|
||||
type: 'PieData',
|
||||
title: 'Pie Sample 1',
|
||||
subtitle: 'Description of pie sample 1',
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: { values: 'samples:filtered_data_for_pie' },
|
||||
props: { entityType: 'Sessions', showLegend: true }
|
||||
},
|
||||
{
|
||||
id: 'pie_sample2',
|
||||
type: 'PieData',
|
||||
title: 'Pie Sample 2',
|
||||
subtitle: 'Hover on the values to see the difference from sample 1',
|
||||
size: { w: 5, h: 8 },
|
||||
dependencies: { values: 'samples:filtered_data_for_pie' },
|
||||
props: { entityType: 'Sessions', showLegend: true, compact: true }
|
||||
},
|
||||
{
|
||||
id: 'scorecard1',
|
||||
type: 'Scorecard',
|
||||
title: 'Value',
|
||||
size: { w: 1, h: 3 },
|
||||
dependencies: {
|
||||
value: 'scorecard:filtered_data_value',
|
||||
color: '::#2196F3',
|
||||
icon: '::av_timer'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'scorecard2',
|
||||
type: 'Scorecard',
|
||||
title: 'Same Value',
|
||||
size: { w: 1, h: 3 },
|
||||
dependencies: {
|
||||
value: 'scorecard:filtered_data_value',
|
||||
color: '::#2196F3',
|
||||
icon: '::av_timer',
|
||||
subvalue: 'scorecard:filtered_data_subvalue'
|
||||
},
|
||||
props: {
|
||||
subheading: 'segments'
|
||||
}
|
||||
}
|
||||
],
|
||||
dialogs: []
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
.md-toolbar .md-select-field--toolbar.md-select-field--toolbar {
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px; }
|
||||
.md-toolbar .md-select-field--toolbar.md-select-field--toolbar .md-floating-label {
|
||||
padding-left: 6px; }
|
||||
.md-toolbar .md-select-field--toolbar.md-select-field--toolbar .md-text-field {
|
||||
padding-bottom: 0;
|
||||
padding-left: 6px;
|
||||
padding-right: 16px;
|
||||
padding-top: 0;
|
||||
margin: 20px 0 0 0; }
|
|
@ -1,88 +0,0 @@
|
|||
.md-card .widgets {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px; }
|
||||
|
||||
.md-card-scorecard .scorecard {
|
||||
width: 100px;
|
||||
height: 90px;
|
||||
white-space: nowrap;
|
||||
float: left;
|
||||
margin: 10px 8px 20px; }
|
||||
.md-card-scorecard .scorecard.clickable-card {
|
||||
cursor: pointer; }
|
||||
.md-card-scorecard .scorecard.clickable-card:hover {
|
||||
background-color: #ECEFF1; }
|
||||
.md-card-scorecard .scorecard.color-bottom .md-subheading-2 {
|
||||
margin-bottom: 3px; }
|
||||
.md-card-scorecard .scorecard.color-bottom .scorecard-subheading {
|
||||
padding-bottom: 4px;
|
||||
border-bottom: solid 4px transparent; }
|
||||
.md-card-scorecard .scorecard.color-left {
|
||||
padding-left: 5px;
|
||||
border-left: solid 3px transparent; }
|
||||
.md-card-scorecard .scorecard .md-icon {
|
||||
float: left;
|
||||
padding: 3px 3px 0 0; }
|
||||
.md-card-scorecard .scorecard .md-headline {
|
||||
margin-bottom: 0;
|
||||
font-weight: 500; }
|
||||
.md-card-scorecard .scorecard .md-subheading-2 {
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden; }
|
||||
.md-card-scorecard .scorecard b {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 0;
|
||||
font-weight: 500; }
|
||||
|
||||
/* MenuFilter */
|
||||
@media screen and (min-width: 320px) {
|
||||
.md-multiselect-menu {
|
||||
margin-top: 50px; } }
|
||||
|
||||
@media screen and (min-width: 1025px) {
|
||||
.md-multiselect-menu {
|
||||
margin-top: 58px; } }
|
||||
|
||||
.md-value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
|
||||
/* Table */
|
||||
.table > .primary {
|
||||
display: block;
|
||||
color: black; }
|
||||
|
||||
.table > .secondary {
|
||||
color: grey; }
|
||||
|
||||
td.text {
|
||||
white-space: normal; }
|
||||
|
||||
td.summary {
|
||||
max-width: 240px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
|
||||
/* Sentiment */
|
||||
.sentiment_very_dissatisfied {
|
||||
color: red !important; }
|
||||
|
||||
.sentiment_dissatisfied {
|
||||
color: orange !important; }
|
||||
|
||||
.sentiment_neutral {
|
||||
color: grey !important; }
|
||||
|
||||
.sentiment_satisfied {
|
||||
color: yellow !important; }
|
||||
|
||||
.sentiment_very_satisfied {
|
||||
color: green !important; }
|
||||
|
||||
.error_outline {
|
||||
color: lightgrey !important; }
|
||||
|
||||
.tooltip {
|
||||
visibility: visible !important; }
|
3857
src/index.css
3857
src/index.css
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче