Merge pull request #42 from CatalystCode/filter-channels

Filters
This commit is contained in:
Mor Shemesh 2017-04-24 23:35:02 +03:00 коммит произвёл GitHub
Родитель c4702b1d06 ffb66498a5
Коммит 4a95285161
13 изменённых файлов: 487 добавлений и 21 удалений

Просмотреть файл

@ -385,10 +385,76 @@ return {
return { queryTimespan, granularity };
}
},
{
id: "filters",
type: "ApplicationInsights/Query",
dependencies: {
timespan: "timespan",
queryTimespan: "timespan:queryTimespan",
granularity: "timespan:granularity"
},
params: {
table: "customEvents",
queries: {
filterChannels: {
query: () => `` +
` where name == 'Activity' | ` +
` extend channel=customDimensions.channel | ` +
` summarize channel_count=count() by tostring(channel) | ` +
` order by channel_count`,
mappings: {
channel: (val) => val || "unknown",
channel_count: (val) => val || 0
},
calculated: (filterChannels) => {
const filters = filterChannels.map((x) => x.channel);
let { selectedValues } = filterChannels;
if (selectedValues === undefined) {
selectedValues = [];
}
return {
"channels-count": filterChannels,
"channels-filters": filters,
"channels-selected": selectedValues,
};
}
},
filterIntents: {
query: () => `` +
` extend intent=customDimensions.intent, cslen = customDimensions.callstack_length | ` +
` where name startswith 'message.intent' and (cslen == 0 or strlen(cslen) == 0) and strlen(intent) > 0 | ` +
` summarize intent_count=count() by tostring(intent) | ` +
` order by intent_count`,
mappings: {
intent: (val) => val || "unknown",
intent_count: (val) => val || 0
},
calculated: (filterIntents) => {
const intents = filterIntents.map((x) => x.intent);
let { selectedValues } = filterIntents;
if (selectedValues === undefined) {
selectedValues = [];
}
return {
"intents-count": filterIntents,
"intents-filters": intents,
"intents-selected": selectedValues,
};
}
}
}
}
},
{
id: 'ai',
type: "ApplicationInsights/Query",
dependencies: { timespan: "timespan", queryTimespan: "timespan:queryTimespan", granularity: "timespan:granularity" },
dependencies: {
timespan: "timespan",
queryTimespan: "timespan:queryTimespan",
granularity: "timespan:granularity",
selectedChannels: "filters:channels-selected",
selectedIntents: "filters:intents-selected"
},
params: {
table: "customEvents",
queries: {
@ -401,6 +467,10 @@ return {
"successful": (val) => val === 'true',
"event_count": (val) => val || 0
},
filters: [{
dependency: "selectedChannels",
queryProperty: "customDimensions.channel"
}],
calculated: (conversions) => {
// Conversion Handling
@ -438,6 +508,10 @@ return {
"channel": (val) => val || "unknown",
"count": (val) => val || 0,
},
filters: [{
dependency: "selectedChannels",
queryProperty: "customDimensions.channel"
}],
calculated: (timeline, dependencies) => {
// Timeline handling
@ -489,6 +563,10 @@ return {
"intent": (val) => val || "Unknown",
"count": (val) => val || 0,
},
filters: [{
dependency: "selectedIntents",
queryProperty: "customDimensions.intent"
}],
calculated: (intents) => {
return {
"intents-bars": [ 'count' ]
@ -497,6 +575,10 @@ return {
},
users: {
query: `summarize totalUsers=count() by user_Id`,
filters: [{
dependency: "selectedChannels",
queryProperty: "customDimensions.channel"
}],
calculated: (users) => {
let result = 0;
if (users.length === 1 && users[0].totalUsers > 0) {
@ -520,6 +602,10 @@ return {
duration: (val) => val || 0,
channel: (val) => val || 'unknown'
},
filters: [{
dependency: "selectedChannels",
queryProperty: "customDimensions.channel"
}],
calculated: (channelActivity) => {
var groupedValues = _.chain(channelActivity).groupBy('channel').value();
return {
@ -605,6 +691,34 @@ return {
dependencies: { selectedValue: "timespan", values: "timespan:values" },
actions: { onChange: "timespan:updateSelectedValue" },
first: true
},
{
type: "MenuFilter",
title: "Channels",
subtitle: "Select channels",
icon: "forum",
dependencies: {
selectedValues: "filters:channels-selected",
values: "filters:channels-filters"
},
actions: {
onChange: "filters:updateSelectedValues:channels-selected"
},
first: true
},
{
type: "MenuFilter",
title: "Intents",
subtitle: "Select intents",
icon: "textsms",
dependencies: {
selectedValues: "filters:intents-selected",
values: "filters:intents-filters"
},
actions: {
onChange: "filters:updateSelectedValues:intents-selected"
},
first: true
}
],
elements: [

Просмотреть файл

@ -78,6 +78,9 @@ export default class ElementConnector {
key={idx}
dependencies={element.dependencies}
actions={element.actions}
title={element.title}
subtitle={element.subtitle}
icon={element.icon}
/>
)
});

Просмотреть файл

@ -0,0 +1,64 @@
import * as React from 'react';
import { GenericComponent } from './GenericComponent';
import Checkbox from 'react-md/lib/SelectionControls/Checkbox';
const style = {
checkbox: {
float: "left",
paddingTop: "24px"
}
}
export default class CheckboxFilter extends GenericComponent<any, any> {
state = {
values: [],
selectedValues: []
};
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(newValue, checked, event) {
var { selectedValues } = this.state;
let newSelectedValues = selectedValues.slice(0);
const idx = selectedValues.findIndex((x) => x === newValue);
if (idx === -1 && checked) {
newSelectedValues.push(newValue);
} else if (idx > -1 && !checked) {
newSelectedValues.splice(idx, 1);
} else {
console.warn("Unexpected checked filter state:", newValue, checked);
}
this.trigger('onChange', newSelectedValues);
}
render() {
var { title } = this.props;
var { selectedValues, values } = this.state;
values = values || [];
let checkboxes = values.map((value, idx) => {
return (<Checkbox
key={idx}
id={idx}
name={value}
label={value}
onChange={this.onChange.bind(null, value)}
style={style.checkbox}
checked={selectedValues.find((x) => x === value) !== undefined} />);
})
return (
<div id="filters">
<div style={style.checkbox}><label>{title}</label></div>
{checkboxes}
</div>
);
}
}

Просмотреть файл

@ -0,0 +1,207 @@
import * as React from 'react';
import { GenericComponent } from './GenericComponent';
import Button from 'react-md/lib/Buttons';
import Portal from 'react-md/lib/Helpers/Portal';
import AccessibleFakeButton from 'react-md/lib/Helpers/AccessibleFakeButton';
import AccessibleFakeInkedButton from 'react-md/lib/Helpers/AccessibleFakeInkedButton';
import List from 'react-md/lib/Lists/List';
import ListItemControl from 'react-md/lib/Lists/ListItemControl';
import Checkbox from 'react-md/lib/SelectionControls/Checkbox';
import ListItem from 'react-md/lib/Lists/ListItem';
import FontIcon from 'react-md/lib/FontIcons';
import './generic.css';
const styles = {
button: {
userSelect: 'none',
},
container: {
position: 'relative',
float: 'left',
zIndex: 17,
},
animateOpen: {
transition: '.3s',
transform: 'scale(1.0,1.0)',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
},
animateClose: {
transform: 'scale(1.0,0)',
transition: '0s',
},
list: {
position: 'absolute',
top: '0px',
left: '0px',
}
};
// using styles from the select field menu
const classNames = {
menu: ['md-inline-block', 'md-menu-container', 'md-menu-container--menu-below', 'md-select-field-menu',
'md-select-field-menu--stretch', 'md-select-field--toolbar', ''],
label: ['md-floating-label', 'md-floating-label--floating', ''],
};
export default class MenuFilter extends GenericComponent<any, any> {
static defaultProps = {
title: '',
subtitle: 'Select filter',
icon: 'more_vert',
selectAll: 'Enable filters',
selectNone: 'Clear filters'
};
state = {
overlay: false,
values: [],
selectedValues: [],
originalSelectedValues: []
};
constructor(props: any) {
super(props);
this.onChange = this.onChange.bind(this);
this.toggleOverlay = this.toggleOverlay.bind(this);
this.hideOverlay = this.hideOverlay.bind(this);
this.selectAll = this.selectAll.bind(this);
this.selectNone = this.selectNone.bind(this);
}
toggleOverlay() {
const { overlay, selectedValues } = this.state;
this.setState({ overlay: !overlay, originalSelectedValues: selectedValues });
if (overlay) {
this.triggerChanges();
}
}
hideOverlay() {
this.setState({ overlay: false });
this.triggerChanges();
}
triggerChanges() {
const { selectedValues } = this.state;
if (!this.didSelectionChange()) {
return;
}
this.trigger('onChange', selectedValues);
}
didSelectionChange(): boolean {
const { selectedValues, originalSelectedValues } = this.state;
if (!selectedValues || !originalSelectedValues) {
return false;
}
if (selectedValues.length !== originalSelectedValues.length
|| selectedValues.slice(0).sort().join() !== originalSelectedValues.slice(0).sort().join()) {
return true;
}
return false;
}
onChange(newValue: any, checked: boolean, event: any) {
var { selectedValues } = this.state;
let newSelectedValues = selectedValues.slice(0);
const idx = selectedValues.findIndex((x) => x === newValue);
if (idx === -1 && checked) {
newSelectedValues.push(newValue);
} else if (idx > -1 && !checked) {
newSelectedValues.splice(idx, 1);
} else {
console.warn('Unexpected checked filter state:', newValue, checked);
}
this.setState({ selectedValues: newSelectedValues });
}
selectAll() {
this.setState({ selectedValues: this.state.values });
}
selectNone() {
this.setState({ selectedValues: [] });
}
render() {
var { title, subtitle, icon } = this.props;
var { selectedValues, values, overlay } = this.state;
values = values || [];
let listItems = values.map((value, idx) => {
return (
<ListItemControl
key={idx + title}
primaryAction={(
<Checkbox
id={idx + value}
name={value}
label={value}
onChange={this.onChange.bind(null, value)}
checked={selectedValues.find((x) => x === value) !== undefined}
/>
)}
/>
);
});
if (values.length > 1) {
const selectAll = this.props.selectAll;
const selectNone = this.props.selectNone;
const iconAll = <FontIcon>done_all</FontIcon>;
const iconNone = <FontIcon disabled>check_box_outline_blank</FontIcon>;
listItems.push(<ListItem key="all" primaryText={selectAll} onClick={this.selectAll} rightIcon={iconAll} />);
listItems.push(<ListItem key="none" primaryText={selectNone} onClick={this.selectNone} rightIcon={iconNone} />);
}
const paperStyle = overlay ? classNames.menu.join(' ') + 'md-paper md-paper--1' : classNames.menu.join(' ');
const labelStyle = overlay ? classNames.label.join(' ') + 'md-floating-label--active' : classNames.label.join(' ');
const containerStyle = overlay ? { ...styles.container, ...styles.animateOpen }
: { ...styles.container, ...styles.animateClose };
let selectText = subtitle || 'Select';
if (selectedValues === undefined) {
selectText = subtitle || 'Select';
} else if (selectedValues.length === 1) {
selectText = selectedValues[0];
} else if (selectedValues.length > 1) {
selectText = `${selectedValues.length} selected`;
}
return (
<div className="filters">
<AccessibleFakeInkedButton
className={paperStyle}
onClick={this.toggleOverlay}
aria-haspopup="true"
aria-expanded={overlay}
style={styles.button}
>
<label className={labelStyle}>{title}</label>
<div className="md-icon-separator md-text-field md-select-field--btn md-text-field--floating-margin">
<span className="md-value md-icon-text">{selectText}</span>
<FontIcon>arrow_drop_down</FontIcon>
</div>
</AccessibleFakeInkedButton>
<div className="md-multiselect-menu" style={containerStyle}>
<List className="md-paper md-paper--1" style={styles.list}>
{listItems}
</List>
</div>
<Portal visible={overlay}>
<AccessibleFakeButton
className="md-overlay"
onClick={this.hideOverlay}
/>
</Portal>
</div>
);
}
}

Просмотреть файл

@ -60,4 +60,24 @@
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;
}

Просмотреть файл

@ -1,10 +1,13 @@
import PieData from './PieData';
import TextFilter from './TextFilter';
import Timeline from './Timeline';
import Scatter from './Scatter';
import BarData from './BarData';
import Area from './Area';
import Scorecard from './Scorecard';
// filters
import TextFilter from './TextFilter';
import CheckboxFilter from './CheckboxFilter';
import MenuFilter from './MenuFilter';
// dialog views
import Table from './Table';
import Detail from './Detail';
@ -12,12 +15,14 @@ import SplitPanel from './SplitPanel';
export default {
PieData,
TextFilter,
Timeline,
Scatter,
BarData,
Area,
Scorecard,
TextFilter,
CheckboxFilter,
MenuFilter,
Table,
Detail,
SplitPanel,

Просмотреть файл

@ -148,12 +148,17 @@ export class DataSourceConnector {
static triggerAction(action: string, params: IStringDictionary, args: IDictionary) {
var actionLocation = action.split(':');
if (actionLocation.length !== 2) {
if (actionLocation.length !== 2 && actionLocation.length !== 3) {
throw new Error(`Action triggers should be in format of "dataSource:action", this is not met by ${action}`);
}
var dataSourceName = actionLocation[0];
var actionName = actionLocation[1];
var selectedValuesProperty = "selectedValues";
if (actionLocation.length === 3) {
selectedValuesProperty = actionLocation[2];
args = { [selectedValuesProperty]: args };
}
if (dataSourceName === 'dialog') {
@ -161,7 +166,7 @@ export class DataSourceConnector {
DialogsActions.openDialog(actionName, extrapolation.dependencies);
} else {
var dataSource = DataSourceConnector.dataSources[dataSourceName];
if (!dataSource) {
throw new Error(`Data source ${dataSourceName} was not found`)

Просмотреть файл

@ -74,4 +74,8 @@ export default class ApplicationInsightsEvents extends DataSourcePlugin<IEventsP
// return callback(null, ActionsCommon.prepareResult('value', json));
// });
}
updateSelectedValues(dependencies, callback) {
}
}

Просмотреть файл

@ -13,9 +13,15 @@ interface IQueryParams {
mappings?: (string|object)[];
table?: string;
queries?: IDictionary;
filters?: Array<IFilterParams>;
calculated?: (results: any) => object;
}
interface IFilterParams {
dependency: string;
queryProperty: string;
}
export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryParams> {
type = 'ApplicationInsights-Query';
@ -41,7 +47,6 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
* @param {function} callback
*/
updateDependencies(dependencies) {
var emptyDependency = _.find(_.keys(this._props.dependencies), dependencyKey => {
return typeof dependencies[dependencyKey] === 'undefined';
});
@ -68,30 +73,28 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
let mappings: Array<any> = [];
let queries: IDictionary = {};
let table: string = null;
let filters: Array<IFilterParams> = params.filters;
// Checking if this is a single query or a fork query
let query: string;
let isForked = !params.query && params.table;
let isForked = !params.query && !!params.table;
if (!isForked) {
query = this.compileQuery(params.query, dependencies);
let queryKey = this._props.id;
query = this.compileQueryWithFilters(params.query, dependencies, isForked, queryKey, filters);
mappings.push(params.mappings);
} else {
queries = params.queries || {};
table = params.table;
query = ` ${params.table} | fork `;
_.keys(queries).forEach(queryKey => {
_.keys(queries).every(queryKey => {
let queryParams = queries[queryKey];
filters = queryParams.filters || [];
tableNames.push(queryKey);
mappings.push(queryParams.mappings);
let subquery = this.compileQuery(queryParams.query, dependencies);
query += ` (${subquery}) `;
query += this.compileQueryWithFilters(queryParams.query, dependencies, isForked, queryKey, filters);
return true;
});
}
var queryspan = queryTimespan;
@ -146,6 +149,13 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
}
}
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
if ( Array.isArray(selectedValues) ){
return _.extend(dependencies, {"selectedValues":selectedValues});
}
return _.extend(dependencies, selectedValues);
}
private mapAllTables(results: IQueryResults, mappings: Array<IDictionary>): any[][] {
if (!results || !results.Tables || !results.Tables.length) {
@ -186,6 +196,30 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
return typeof query === 'function' ? query(dependencies) : query;
}
private compileQueryWithFilters(query: any, dependencies: any, isForked: boolean, queryKey: string, filters: IFilterParams[]): string {
let q = this.compileQuery(query, dependencies);
// Don't filter a filter query, or no filters specified
if (queryKey.startsWith("filter") || filters === undefined || filters.length === 0) {
return this.formatQuery(q, isForked);
}
// Apply selected filters to connected query
filters.every((filter) => {
const { dependency, queryProperty } = filter;
const selectedFilters = dependencies[dependency] || [];
if (selectedFilters.length > 0) {
const filter = "where " + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ') + " | ";
q = ` ${filter} \n ${q} `;
return true;
}
return false;
});
return this.formatQuery(q, isForked);
}
private formatQuery(query: string, isForked: boolean = true) {
return isForked ? ` (${query}) \n\n` : query;
}
private validateParams(props: any, params: any): void {
if (!props.dependencies.queryTimespan) {

Просмотреть файл

@ -18,7 +18,7 @@ export default class Constant extends DataSourcePlugin<IConstantParams> {
var props = this._props;
var params = options.params;
props.actions.push.apply(props.actions, [ 'initialize', 'updateSelectedValue' ]);
props.actions.push.apply(props.actions, [ 'initialize', 'updateSelectedValue', 'updateSelectedValues' ]);
}
initialize() {
@ -42,4 +42,8 @@ export default class Constant extends DataSourcePlugin<IConstantParams> {
updateSelectedValue(dependencies: IDictionary, selectedValue: any) {
return { selectedValue };
}
updateSelectedValues(dependencies: IDictionary, selectedValues: any) {
return { selectedValues };
}
}

Просмотреть файл

@ -49,7 +49,7 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
id: '',
dependencies: {} as any,
dependables: [],
actions: [ 'updateDependencies', 'failure' ],
actions: [ 'updateDependencies', 'failure', 'updateSelectedValues' ],
params: <T>{},
calculated: {}
};
@ -68,9 +68,11 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
props.calculated = options.calculated || {};
this.updateDependencies = this.updateDependencies.bind(this);
this.updateSelectedValues = this.updateSelectedValues.bind(this);
}
abstract updateDependencies (dependencies: IDictionary, args: IDictionary, callback: (result: any) => void): void;
abstract updateSelectedValues (dependencies: IDictionary, selectedValues: any, callback: (result: any) => void): void;
bind (actionClass: any) {
actionClass.type = this.type;

3
src/types.d.ts поставляемый
Просмотреть файл

@ -57,6 +57,9 @@ interface IFilter {
type: string
dependencies?: { [id: string]: string }
actions?: { [id: string]: string }
title?: string
subtitle?: string
icon?: string
first: boolean
}

Просмотреть файл

@ -21,6 +21,7 @@
"jsdoc-format": true,
"jsx-no-lambda": false,
"jsx-no-multiline-js": false,
"jsx-boolean-value": false,
"label-position": true,
"max-line-length": [ true, 120 ],
"member-ordering": [
@ -42,7 +43,7 @@
"timeEnd",
"trace"
],
"no-consecutive-blank-lines": true,
"no-consecutive-blank-lines": [true],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
@ -53,7 +54,7 @@
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-use-before-declare": false,
"one-line": [
true,
"check-catch",
@ -66,7 +67,7 @@
"semicolon": [true, "always"],
"switch-default": true,
"trailing-comma": false,
"trailing-comma": [false],
"triple-equals": [ true, "allow-null-check" ],
"typedef": [