Merge da0e0bee3c
into 5bc8048632
This commit is contained in:
Коммит
819f6da37f
|
@ -1,7 +1,7 @@
|
|||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/*.css": { "when": "$(basename).scss"}
|
||||
"**/*.css": true
|
||||
},
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true,
|
||||
|
@ -14,5 +14,7 @@
|
|||
"**/node_modules": true,
|
||||
"**/build": true,
|
||||
"**/coverage": true
|
||||
}
|
||||
},
|
||||
"tslint.configFile": "client/tslint.json",
|
||||
"tslint.nodePath": "client/node_modules"
|
||||
}
|
|
@ -118,6 +118,7 @@ interface IElement {
|
|||
title?: string;
|
||||
subtitle?: string;
|
||||
theme?: string[];
|
||||
source?: string | IStringDictionary;
|
||||
dependencies?: IStringDictionary;
|
||||
props?: IDictionary;
|
||||
actions?: IDictionary;
|
||||
|
@ -125,6 +126,7 @@ interface IElement {
|
|||
|
||||
interface IFilter {
|
||||
type: string,
|
||||
source?: string,
|
||||
dependencies?: IStringDictionary,
|
||||
actions?: IStringDictionary,
|
||||
title?: string,
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import { Media } from 'react-md/lib/Media';
|
||||
import { Card, CardTitle } from 'react-md/lib/Cards';
|
||||
import TooltipFontIcon from './TooltipFontIcon';
|
||||
import Button from 'react-md/lib/Buttons';
|
||||
|
||||
export default ({children = {}, title = '', subtitle = ''}) => {
|
||||
const titleNode = <span key={0}>{title}</span>;
|
||||
const tooltipNode = (
|
||||
<TooltipFontIcon
|
||||
key={1}
|
||||
tooltipLabel={subtitle}
|
||||
tooltipPosition="top"
|
||||
forceIconFontSize={true}
|
||||
forceIconSize={16}
|
||||
className="card-icon"
|
||||
>
|
||||
info
|
||||
</TooltipFontIcon>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardTitle title={''} subtitle={[titleNode, tooltipNode]} />
|
||||
<Media>
|
||||
{children}
|
||||
</Media>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
import * as React from 'react';
|
||||
import { Media } from 'react-md/lib/Media';
|
||||
import { Card as MDCard, CardTitle } from 'react-md/lib/Cards';
|
||||
import TooltipFontIcon from '../Tooltip/TooltipFontIcon';
|
||||
import Button from 'react-md/lib/Buttons';
|
||||
import { Settings, SettingsActions } from './Settings';
|
||||
import { SpinnerActions } from '../Spinner';
|
||||
|
||||
const styles = {
|
||||
noTitle: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
background: 'transparent',
|
||||
} as React.CSSProperties,
|
||||
noTitleContent: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
} as React.CSSProperties
|
||||
};
|
||||
|
||||
interface ICardProps {
|
||||
id?: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
widgets?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
titleStyle?: React.CSSProperties;
|
||||
contentStyle?: React.CSSProperties;
|
||||
hideTitle?: boolean;
|
||||
}
|
||||
|
||||
interface ICardState {
|
||||
hover: boolean;
|
||||
}
|
||||
|
||||
export default class Card extends React.PureComponent<ICardProps, ICardState> {
|
||||
|
||||
static defaultProps = {
|
||||
hideTitle: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
hover: false,
|
||||
};
|
||||
|
||||
constructor(props: ICardProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, title, subtitle, children, className, style, titleStyle, contentStyle, hideTitle } = this.props;
|
||||
const { hover } = this.state;
|
||||
|
||||
let elements: React.ReactNode[] = [];
|
||||
if (title && !hideTitle) {
|
||||
elements.push(
|
||||
<span key={0}>{title}</span>
|
||||
);
|
||||
}
|
||||
if (subtitle) {
|
||||
elements.push(
|
||||
<TooltipFontIcon
|
||||
key={1}
|
||||
tooltipLabel={subtitle}
|
||||
tooltipPosition="top"
|
||||
forceIconFontSize={true}
|
||||
forceIconSize={16}
|
||||
className="card-icon"
|
||||
>
|
||||
info
|
||||
</TooltipFontIcon>
|
||||
);
|
||||
}
|
||||
if (hover) {
|
||||
elements.push( this.renderWidgets() );
|
||||
}
|
||||
|
||||
// NB: Fix for Card scroll content when no title
|
||||
let cardTitleStyle = titleStyle || {};
|
||||
let cardContentStyle = contentStyle || {};
|
||||
if (hideTitle) {
|
||||
Object.assign(cardTitleStyle, styles.noTitle);
|
||||
Object.assign(cardContentStyle, styles.noTitleContent);
|
||||
}
|
||||
|
||||
return (
|
||||
<MDCard
|
||||
onMouseOver={() => this.setState({ hover: true })}
|
||||
onMouseLeave={() => this.setState({ hover: false })}
|
||||
className={className}
|
||||
style={style}>
|
||||
<CardTitle title="" subtitle={elements} style={cardTitleStyle} />
|
||||
<Media style={cardContentStyle}>
|
||||
{children}
|
||||
</Media>
|
||||
</MDCard>
|
||||
);
|
||||
}
|
||||
|
||||
renderWidgets() {
|
||||
const { id, title, widgets } = this.props;
|
||||
|
||||
const settingsButton = (
|
||||
<Button
|
||||
icon
|
||||
key="settings"
|
||||
onClick={() => SettingsActions.openDialog(title, id)}
|
||||
className="card-settings-btn"
|
||||
>
|
||||
settings
|
||||
</Button>
|
||||
);
|
||||
|
||||
return !widgets ? (
|
||||
<div className="card-settings" key="widgets">
|
||||
{settingsButton}
|
||||
</div>
|
||||
) : (
|
||||
<div className="card-settings" key="widgets">
|
||||
<span>{widgets}</span>
|
||||
<span>{settingsButton}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import Dialog from 'react-md/lib/Dialogs';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
import Toolbar from 'react-md/lib/Toolbars';
|
||||
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
|
||||
import SelectField from 'react-md/lib/SelectFields';
|
||||
|
||||
import AceEditor, { EditorProps, Annotation } from 'react-ace';
|
||||
import * as brace from 'brace';
|
||||
import 'brace/mode/text';
|
||||
import 'brace/mode/json';
|
||||
import 'brace/theme/github';
|
||||
|
||||
import SettingsActions from './SettingsActions';
|
||||
import SettingsStore, { IExportData } from './SettingsStore';
|
||||
import { Toast, ToastActions, IToast } from '../../Toast';
|
||||
|
||||
const editorProps: EditorProps = {
|
||||
$blockScrolling: 1
|
||||
};
|
||||
|
||||
interface ISettingsProps {
|
||||
offsetHeight?: number;
|
||||
dashboard: IDashboardConfig;
|
||||
}
|
||||
|
||||
interface ISettingsState {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
elementId: string;
|
||||
dashboard?: IDashboardConfig;
|
||||
selectedIndex: number;
|
||||
exportData?: IExportData[];
|
||||
result?: string;
|
||||
}
|
||||
|
||||
export default class Edit extends React.PureComponent<ISettingsProps, ISettingsState> {
|
||||
|
||||
static defaultProps = {
|
||||
offsetHeight: 64 // set to height of header / toolbar
|
||||
};
|
||||
|
||||
constructor(props: ISettingsProps) {
|
||||
super(props);
|
||||
|
||||
this.state = SettingsStore.getState();
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.copyData = this.copyData.bind(this);
|
||||
this.copyQuery = this.copyQuery.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SettingsStore.listen(this.onChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
SettingsStore.unlisten(this.onChange);
|
||||
}
|
||||
|
||||
onChange(state: ISettingsState) {
|
||||
const { visible, elementId, title, selectedIndex, exportData } = state;
|
||||
this.setState({ visible, elementId, title, selectedIndex, exportData });
|
||||
}
|
||||
|
||||
openDialog = (title: string, elementId: string) => {
|
||||
SettingsActions.openDialog(title, elementId);
|
||||
}
|
||||
|
||||
closeDialog = () => {
|
||||
SettingsActions.closeDialog();
|
||||
}
|
||||
|
||||
copyData() {
|
||||
const { exportData, selectedIndex} = this.state;
|
||||
if (!exportData) {
|
||||
return;
|
||||
}
|
||||
const selected = exportData[selectedIndex];
|
||||
const text = selected.isJSON ? JSON.stringify(selected.data, null, 2) : selected.data.toString();
|
||||
this.copyToClipboard(text);
|
||||
}
|
||||
|
||||
copyQuery() {
|
||||
const { exportData, selectedIndex} = this.state;
|
||||
if (!exportData) {
|
||||
return;
|
||||
}
|
||||
const selected = exportData[selectedIndex];
|
||||
const text = selected.query;
|
||||
this.copyToClipboard(text);
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps: any, nextState: any) {
|
||||
const { visible } = this.state;
|
||||
const { dashboard } = this.props;
|
||||
if (nextState.visible === true && visible !== nextState.visible) {
|
||||
SettingsActions.getExportData(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visible, title, elementId, selectedIndex, exportData } = this.state;
|
||||
const { offsetHeight, dashboard } = this.props;
|
||||
|
||||
const aceHeight = 'calc(100vh - ' + (offsetHeight * 4) + 'px)';
|
||||
const titleStyle = { height: offsetHeight + 'px)' } as React.CSSProperties;
|
||||
|
||||
let actions = null;
|
||||
let json = '';
|
||||
let query = '';
|
||||
|
||||
let mode = 'text';
|
||||
|
||||
let dataActions = null;
|
||||
let queryActions = null;
|
||||
|
||||
if (exportData && exportData.length > 0) {
|
||||
const options = exportData.map(item => item.id);
|
||||
const selectedValue = options[selectedIndex];
|
||||
actions = options.length > 1 ? [
|
||||
(
|
||||
<SelectField
|
||||
id="theme"
|
||||
placeholder="Theme"
|
||||
position={SelectField.Positions.BELOW}
|
||||
defaultValue={selectedValue}
|
||||
menuItems={options}
|
||||
onChange={(newValue, index) => SettingsActions.selectIndex(index)}
|
||||
tabIndex={-1}
|
||||
toolbar
|
||||
/>
|
||||
)
|
||||
] : <Button flat disabled label={selectedValue} style={{ textTransform: 'none', fontWeight: 'normal' }} />;
|
||||
|
||||
const selected: IExportData = exportData[selectedIndex];
|
||||
|
||||
// data
|
||||
const data = selected.data;
|
||||
switch (typeof data) {
|
||||
case 'object':
|
||||
json = data ? JSON.stringify(data, null, 2) : 'null';
|
||||
mode = 'json';
|
||||
break;
|
||||
case 'string':
|
||||
json = data;
|
||||
break;
|
||||
case 'boolean':
|
||||
json = (data === true) ? 'true' : 'false';
|
||||
break;
|
||||
case 'undefined':
|
||||
json = 'undefined';
|
||||
break;
|
||||
case 'number':
|
||||
default:
|
||||
json = data.toString();
|
||||
}
|
||||
|
||||
// query
|
||||
if (selected.query) {
|
||||
query = selected.query;
|
||||
}
|
||||
|
||||
// actions
|
||||
dataActions = [
|
||||
(
|
||||
<Button icon tooltipLabel="Copy" onClick={this.copyData} tabIndex={-1}>
|
||||
content_copy
|
||||
</Button>
|
||||
),
|
||||
(
|
||||
<Button icon tooltipLabel="Download" onClick={SettingsActions.downloadData} tabIndex={-1}>
|
||||
file_download
|
||||
</Button>
|
||||
)
|
||||
];
|
||||
|
||||
queryActions = [
|
||||
(
|
||||
<Button icon tooltipLabel="Copy" onClick={this.copyQuery} tabIndex={-1}>
|
||||
content_copy
|
||||
</Button>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
let id = 'Element id error';
|
||||
if ( elementId) {
|
||||
id = elementId.split('@')[0] || 'Element index error';
|
||||
}
|
||||
|
||||
const content = !query ? (
|
||||
<div className="md-toolbar-relative md-grid">
|
||||
<div className="md-cell--12">
|
||||
<h3>{id}</h3>
|
||||
</div>
|
||||
<div className="md-cell--12">
|
||||
<p>Use the same id for the element and data source to unwind the query and data.</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="md-toolbar-relative md-grid md-grid--no-spacing">
|
||||
<div className="md-cell--6">
|
||||
<Toolbar title="Data" actions={dataActions} themed style={{ width: '100%' }} />
|
||||
<AceEditor className="md-cell--12"
|
||||
name="ace"
|
||||
mode={mode}
|
||||
theme="github"
|
||||
value={json}
|
||||
readOnly={true}
|
||||
showGutter={true}
|
||||
showPrintMargin={false}
|
||||
highlightActiveLine={true}
|
||||
tabSize={2}
|
||||
width="100%"
|
||||
height={aceHeight}
|
||||
editorProps={editorProps}
|
||||
/>
|
||||
</div>
|
||||
<div className="md-cell--6">
|
||||
<Toolbar title="Query" actions={queryActions} themed style={{ width: '100%' }} />
|
||||
<AceEditor className="md-cell--12"
|
||||
name="ace"
|
||||
mode="text"
|
||||
theme="github"
|
||||
value={query}
|
||||
readOnly={true}
|
||||
showGutter={true}
|
||||
showPrintMargin={false}
|
||||
highlightActiveLine={true}
|
||||
tabSize={2}
|
||||
width="100%"
|
||||
height={aceHeight}
|
||||
editorProps={editorProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
id="editElementDialog"
|
||||
visible={visible}
|
||||
aria-label="Element settings"
|
||||
focusOnMount={false}
|
||||
onHide={this.closeDialog}
|
||||
dialogStyle={{ width: '80%' }}
|
||||
contentStyle={{ margin: '0px', padding: '0px' }}
|
||||
lastChild={true}
|
||||
>
|
||||
<Toolbar
|
||||
colored
|
||||
nav={<Button icon onClick={this.closeDialog} tabIndex={-1}>close</Button>}
|
||||
actions={actions}
|
||||
title={title}
|
||||
fixed
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
{content}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
private toast(text: string) {
|
||||
ToastActions.showText(text);
|
||||
}
|
||||
|
||||
private copyToClipboard(text: string) {
|
||||
if (!document.queryCommandSupported('copy')) {
|
||||
this.toast('Browser not supported');
|
||||
return;
|
||||
}
|
||||
const input = document.createElement('textarea');
|
||||
input.style.position = 'fixed';
|
||||
input.style.opacity = '0';
|
||||
input.value = text;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import alt, { AbstractActions } from '../../../alt';
|
||||
|
||||
interface ISettingsActions {
|
||||
openDialog(title: string, elementId: string): IDict<string>;
|
||||
closeDialog(): any;
|
||||
selectIndex(index: number): number;
|
||||
getExportData(dashboard: IDashboardConfig): IDashboardConfig;
|
||||
downloadData(): void;
|
||||
}
|
||||
|
||||
class SettingsActions extends AbstractActions implements ISettingsActions {
|
||||
constructor(alt: AltJS.Alt) {
|
||||
super(alt);
|
||||
}
|
||||
|
||||
openDialog(title: string, elementId: string) {
|
||||
return {title, elementId};
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
return {};
|
||||
}
|
||||
|
||||
selectIndex(index: number) {
|
||||
return index;
|
||||
}
|
||||
|
||||
getExportData(dashboard: IDashboardConfig) {
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
downloadData() {
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const settingsActions = alt.createActions<ISettingsActions>(SettingsActions);
|
||||
|
||||
export default settingsActions;
|
|
@ -0,0 +1,312 @@
|
|||
import * as React from 'react';
|
||||
import alt, { AbstractStoreModel } from '../../../alt';
|
||||
|
||||
import { DataSourceConnector, IDataSourceDictionary, IDataSource } from '../../../data-sources/DataSourceConnector';
|
||||
|
||||
import settingsActions from './SettingsActions';
|
||||
|
||||
import { downloadBlob } from '../../Dashboard/DownloadFile';
|
||||
|
||||
export interface IExportData {
|
||||
id: string;
|
||||
data: any;
|
||||
isJSON: boolean;
|
||||
query: string;
|
||||
group: string;
|
||||
isGroupedJSON: boolean;
|
||||
}
|
||||
|
||||
interface ISettingsStoreState {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
elementId: string;
|
||||
dashboard?: IDashboardConfig;
|
||||
selectedIndex: number;
|
||||
exportData?: IExportData[];
|
||||
result?: string;
|
||||
}
|
||||
|
||||
class SettingsStore extends AbstractStoreModel<ISettingsStoreState> implements ISettingsStoreState {
|
||||
|
||||
visible: boolean;
|
||||
title: string;
|
||||
elementId: string;
|
||||
dashboard?: IDashboardConfig;
|
||||
selectedIndex: number;
|
||||
exportData?: IExportData[];
|
||||
result?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.visible = false;
|
||||
this.selectedIndex = 0;
|
||||
|
||||
this.bindListeners({
|
||||
openDialog: settingsActions.openDialog,
|
||||
closeDialog: settingsActions.closeDialog,
|
||||
selectIndex: settingsActions.selectIndex,
|
||||
getExportData: settingsActions.getExportData,
|
||||
downloadData: settingsActions.downloadData,
|
||||
});
|
||||
}
|
||||
|
||||
openDialog(state: IDict<string>) {
|
||||
this.title = state.title;
|
||||
this.elementId = state.elementId;
|
||||
this.visible = true;
|
||||
}
|
||||
|
||||
closeDialog() {
|
||||
this.visible = false;
|
||||
this.title = '';
|
||||
this.exportData = null;
|
||||
}
|
||||
|
||||
selectIndex(index: number) {
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
downloadData() {
|
||||
const selected = this.exportData[this.selectedIndex];
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
const text = selected.isJSON ? JSON.stringify(selected.data) : selected.data.toString();
|
||||
const filename = selected.id + '.json';
|
||||
downloadBlob(text, 'application/json', filename);
|
||||
}
|
||||
|
||||
getExportData(dashboard: IDashboardConfig) {
|
||||
if (!this.elementId) {
|
||||
console.warn('Requires element "id" prop:', this.elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = this.elementId.split('@');
|
||||
if (matches.length !== 2) {
|
||||
console.warn('Element index not found:', this.elementId);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = matches[0];
|
||||
const index = parseInt(matches[1], 10);
|
||||
let elements = dashboard.elements;
|
||||
|
||||
if (isNaN(index) || index >= elements.length || index < 0) {
|
||||
console.warn('Element index invalid value:', index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (elements[index].id === id) {
|
||||
this.getElement(elements, index);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle dialog element
|
||||
dashboard.dialogs.every(dialog => {
|
||||
if (dialog.elements.length > index && dialog.elements[index].id === id) {
|
||||
elements = dialog.elements;
|
||||
this.getElement(elements, index);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getElement(elements: IElement[], index: number) {
|
||||
const element: IElement = elements[index];
|
||||
this.exportData = this.extrapolateElementExportData(element.dependencies, element.source, element.id);
|
||||
this.selectedIndex = 0; // resets dialog menu selection
|
||||
}
|
||||
|
||||
private extrapolateElementExportData(elementDependencies: IStringDictionary,
|
||||
sources: string | IStringDictionary,
|
||||
elementId: string): IExportData[] {
|
||||
let result: IExportData[] = [];
|
||||
let dependencies = {};
|
||||
Object.assign(dependencies, elementDependencies);
|
||||
if (typeof sources === 'string') {
|
||||
let source = {};
|
||||
source[elementId] = sources;
|
||||
Object.assign(dependencies, source);
|
||||
} else if (sources && typeof sources === 'object' && Object.keys(sources).length > 0 ) {
|
||||
Object.assign(dependencies, sources);
|
||||
}
|
||||
if (!dependencies || Object.keys(dependencies).length === 0 ) {
|
||||
console.warn('Missing element dependencies');
|
||||
return result;
|
||||
}
|
||||
const datasources = DataSourceConnector.getDataSources();
|
||||
Object.keys(dependencies).forEach((id) => {
|
||||
const dependency = dependencies[id];
|
||||
|
||||
let dependencySource = dependency;
|
||||
let dependencyProperty = 'values';
|
||||
const dependencyPath = dependency.split(':');
|
||||
|
||||
if (dependencyPath.length === 2) {
|
||||
dependencySource = dependencyPath[0];
|
||||
dependencyProperty = dependencyPath[1];
|
||||
}
|
||||
|
||||
const datasource = Object.keys(datasources).find(key => dependencySource === key);
|
||||
if (!datasource) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (datasource === 'mode' || datasource === 'modes') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Data (JSON)
|
||||
let data: any;
|
||||
let isJSON = false;
|
||||
let isGroupedJSON = false;
|
||||
let group = datasource;
|
||||
const values = datasources[datasource].store.state[dependencyProperty];
|
||||
if (values === null || typeof values === undefined) {
|
||||
console.warn('Missing data:', datasource, dependency);
|
||||
return;
|
||||
}
|
||||
if (typeof values === 'object' || Array.isArray(values)) {
|
||||
isJSON = true;
|
||||
}
|
||||
data = values;
|
||||
|
||||
// Query
|
||||
let queryFn, queryFilters;
|
||||
let queryId = dependencyProperty;
|
||||
const forkedQueryComponents = dependencyProperty.split('-');
|
||||
const params = datasources[datasource].config.params;
|
||||
|
||||
const isForked = !params.query && !!params.table;
|
||||
|
||||
if (!isForked) {
|
||||
// unforked
|
||||
queryFn = params.query;
|
||||
} else {
|
||||
// forked
|
||||
if (!params.queries[queryId] && forkedQueryComponents.length === 2) {
|
||||
queryId = forkedQueryComponents[0];
|
||||
group = queryId;
|
||||
}
|
||||
if (params.queries[dependencySource]) {
|
||||
queryId = dependencySource; // dialog case
|
||||
}
|
||||
if (!params.queries[queryId]) {
|
||||
console.warn(`Unable to locate query id '${queryId}' in datasource '${dependencySource}'.`);
|
||||
return;
|
||||
}
|
||||
queryFn = params.queries[queryId].query;
|
||||
queryFilters = params.queries[queryId].filters;
|
||||
}
|
||||
|
||||
// Query dependencies
|
||||
let query, filter = '';
|
||||
let queryDependencies: IDict<any> = {};
|
||||
if (typeof queryFn === 'function') {
|
||||
// Get query function dependencies
|
||||
const queryDependenciesDict = datasources[datasource].config.dependencies || {};
|
||||
|
||||
Object.keys(queryDependenciesDict).forEach((dependenciesKey) => {
|
||||
const value = queryDependenciesDict[dependenciesKey];
|
||||
const path = value.split(':');
|
||||
|
||||
let source = value;
|
||||
let property;
|
||||
|
||||
if (path.length === 2) {
|
||||
source = path[0];
|
||||
property = path[1];
|
||||
}
|
||||
|
||||
if (source.startsWith('::') || source === 'connection') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === 'args') {
|
||||
const args = datasources[datasource].plugin['lastArgs'];
|
||||
const arg = Object.keys(args).find(key => property === key);
|
||||
if (!arg) {
|
||||
console.warn('Unable to find arg property:', property);
|
||||
return;
|
||||
}
|
||||
const argValue = args[arg] || '';
|
||||
let append = {};
|
||||
append[property] = argValue;
|
||||
Object.assign(queryDependencies, append);
|
||||
} else {
|
||||
const datasourceId = Object.keys(datasources).find(key => source === key);
|
||||
if (!datasourceId) {
|
||||
console.warn('Unable to find data source id:', source);
|
||||
return;
|
||||
}
|
||||
const resolvedValues = !property ? JSON.parse(JSON.stringify(datasources[datasourceId].store.state))
|
||||
: datasources[datasourceId].store.state[property];
|
||||
let append = {};
|
||||
append[dependenciesKey] = resolvedValues;
|
||||
Object.assign(queryDependencies, append);
|
||||
}
|
||||
});
|
||||
query = queryFn(queryDependencies);
|
||||
} else {
|
||||
query = queryFn ? queryFn.toString() : 'n/a';
|
||||
}
|
||||
|
||||
query = this.formatQueryString(query);
|
||||
|
||||
const dataSource: IDataSource = datasources[datasource];
|
||||
const elementQuery = dataSource.plugin.getElementQuery(dataSource, queryDependencies, query, queryFilters);
|
||||
if (elementQuery !== null) {
|
||||
query = elementQuery;
|
||||
}
|
||||
|
||||
const exportData: IExportData = { id, data, isJSON, query, group, isGroupedJSON };
|
||||
result.push(exportData);
|
||||
});
|
||||
|
||||
// Group primative (scorecard) results
|
||||
result = result.reduce((a: IExportData[], c: IExportData) => {
|
||||
if (c.isJSON) {
|
||||
a.push(c);
|
||||
return a;
|
||||
}
|
||||
const target = a.find((i) => i.group === c.group);
|
||||
let data = {};
|
||||
data[c.id] = c.data;
|
||||
// new
|
||||
if (!target) {
|
||||
c.data = data;
|
||||
c.isGroupedJSON = true;
|
||||
c.isJSON = true;
|
||||
c.id = c.group;
|
||||
a.push(c);
|
||||
return a;
|
||||
}
|
||||
// skip
|
||||
if (target.isGroupedJSON !== true) {
|
||||
a.push(c);
|
||||
return a;
|
||||
}
|
||||
// merge
|
||||
target.data = Object.assign(target.data, data);
|
||||
return a;
|
||||
}, []);
|
||||
|
||||
// Order by largest data set
|
||||
result.sort((a, b) => b.data.toString().length - a.data.toString().length);
|
||||
return result;
|
||||
}
|
||||
|
||||
private formatQueryString(query: string) {
|
||||
// Strip indent whitespaces
|
||||
return query.replace(/(\s{2,})?(.+)?/gm, '$2\n').trim();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const settingsStore = alt.createStore<ISettingsStoreState>(SettingsStore, 'SettingsStore');
|
||||
|
||||
export default settingsStore;
|
|
@ -0,0 +1,9 @@
|
|||
import Settings from './Settings';
|
||||
import SettingsActions from './SettingsActions';
|
||||
import SettingsStore from './SettingsStore';
|
||||
|
||||
export {
|
||||
Settings,
|
||||
SettingsActions,
|
||||
SettingsStore
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import Card from './Card';
|
||||
|
||||
export default Card;
|
|
@ -1,26 +1,3 @@
|
|||
import * as _ from 'lodash';
|
||||
import { DataSourceConnector } from '../../data-sources/DataSourceConnector';
|
||||
|
||||
interface IDownloadFile {
|
||||
filename: string;
|
||||
csv: string;
|
||||
json: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export default class DownloadFile implements IDownloadFile {
|
||||
filename: string;
|
||||
json: string;
|
||||
csv: string;
|
||||
source: string;
|
||||
|
||||
constructor(filename: string, json: string, csv: string, source: string = '') {
|
||||
this.filename = filename;
|
||||
this.json = json;
|
||||
this.csv = csv;
|
||||
this.source = source;
|
||||
}
|
||||
}
|
||||
|
||||
function downloadBlob(data: string, mimeType: string, filename: string) {
|
||||
const blob = new Blob([data], {
|
||||
|
@ -35,101 +12,4 @@ function downloadBlob(data: string, mimeType: string, filename: string) {
|
|||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
function exportDataSources() {
|
||||
const sources = DataSourceConnector.getDataSources();
|
||||
let states = {};
|
||||
Object.keys(sources).forEach(key => {
|
||||
let state = sources[key].store.state;
|
||||
if (_.isEmpty(state)) {
|
||||
return;
|
||||
}
|
||||
let isEmpty = Object.keys(state).every(prop => {
|
||||
if (state[prop] === null || state[prop] === undefined) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (isEmpty) {
|
||||
return;
|
||||
}
|
||||
states[key] = state;
|
||||
});
|
||||
return states;
|
||||
}
|
||||
|
||||
function createDownloadFiles(json: any) {
|
||||
let files: IDownloadFile[] = [];
|
||||
if (Array.isArray(json)) {
|
||||
let csv = arrayToFileData(json, 'data', true);
|
||||
return [csv];
|
||||
}
|
||||
if (typeof json === 'object') {
|
||||
return objectArraysToFiles(json, files);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function objectArraysToFiles(obj: any, container: IDownloadFile[], sourceName: string = '') {
|
||||
if (obj === null || obj === undefined) {
|
||||
return container;
|
||||
}
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
const csv = arrayToFileData(value, key, true, sourceName);
|
||||
container.push(csv);
|
||||
} else if (typeof value === 'object') {
|
||||
objectArraysToFiles(value, container, key);
|
||||
}
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
function arrayToFileData(arr: any[], title: string, useColumnNames: boolean = true, source: string = '') {
|
||||
let rows = [];
|
||||
let keys = Object.keys(arr[0]);
|
||||
let columnNames = (keys).join(',');
|
||||
|
||||
if (typeof arr[0] === 'object') {
|
||||
|
||||
const collection = arr.reduce((a, c) => {
|
||||
let values = [];
|
||||
Object.keys(c).forEach(key => {
|
||||
const i = keys.findIndex(k => k === key);
|
||||
const value = typeof c[key] === 'object' ? JSON.stringify(c[key]) : c[key];
|
||||
if (i > -1) {
|
||||
values[i] = escapeValue(value);
|
||||
} else {
|
||||
keys.push(key);
|
||||
values[keys.length - 1] = escapeValue(value);
|
||||
}
|
||||
});
|
||||
a.push(values.join(','));
|
||||
return a;
|
||||
}, []);
|
||||
rows.push(collection.join('\n'));
|
||||
columnNames = (keys).join(',');
|
||||
} else {
|
||||
rows.push(arr.join('\n')); // single value string array
|
||||
columnNames = title;
|
||||
}
|
||||
|
||||
if (useColumnNames) {
|
||||
rows.unshift(columnNames);
|
||||
}
|
||||
|
||||
return new DownloadFile(title, stripSlashes(JSON.stringify(arr)), rows.join('\n'), source);
|
||||
}
|
||||
|
||||
function escapeValue(value: string) {
|
||||
if (typeof value === 'string' && value.indexOf(',') > -1) {
|
||||
return (value.indexOf('"') === -1) ? `"${value}"` : '"' + stripSlashes(value.replace(/"/g, '""')) + '"';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function stripSlashes(value: string) {
|
||||
return value.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
export { exportDataSources, createDownloadFiles, downloadBlob };
|
||||
export { downloadBlob };
|
|
@ -14,7 +14,7 @@ ResponsiveReactGridLayout = WidthProvider(ResponsiveReactGridLayout);
|
|||
|
||||
import ElementConnector from '../ElementConnector';
|
||||
import { loadDialogsFromDashboard } from '../generic/Dialogs';
|
||||
import IDownloadFile, { exportDataSources, createDownloadFiles, downloadBlob } from './DownloadFile';
|
||||
import { downloadBlob } from './DownloadFile';
|
||||
|
||||
import { SettingsButton } from '../Settings';
|
||||
import ConfigurationsActions from '../../actions/ConfigurationsActions';
|
||||
|
@ -22,6 +22,7 @@ import ConfigurationsStore from '../../stores/ConfigurationsStore';
|
|||
import VisibilityStore from '../../stores/VisibilityStore';
|
||||
|
||||
import {Editor, EditorActions} from './Editor';
|
||||
import { Settings } from '../Card/Settings';
|
||||
|
||||
const renderHTML = require('react-render-html');
|
||||
|
||||
|
@ -41,10 +42,7 @@ interface IDashboardProps {
|
|||
interface IDashboardState {
|
||||
editMode?: boolean;
|
||||
askDelete?: boolean;
|
||||
askDownload?: boolean;
|
||||
askSaveAsTemplate?: boolean;
|
||||
downloadFiles?: IDownloadFile[];
|
||||
downloadFormat?: string;
|
||||
mounted?: boolean;
|
||||
currentBreakpoint?: string;
|
||||
layouts?: ILayouts;
|
||||
|
@ -65,9 +63,6 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
editMode: false,
|
||||
askDelete: false,
|
||||
askSaveAsTemplate: false,
|
||||
askDownload: false,
|
||||
downloadFiles: [],
|
||||
downloadFormat: 'json',
|
||||
currentBreakpoint: 'lg',
|
||||
mounted: false,
|
||||
layouts: {},
|
||||
|
@ -84,7 +79,8 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
super(props);
|
||||
|
||||
this.onBreakpointChange = this.onBreakpointChange.bind(this);
|
||||
this.onLayoutChange = this.onLayoutChange.bind(this);
|
||||
this.onLayoutChangeActive = this.onLayoutChangeActive.bind(this);
|
||||
this.onLayoutChangeInactive = this.onLayoutChangeInactive.bind(this);
|
||||
this.onConfigDashboard = this.onConfigDashboard.bind(this);
|
||||
this.toggleEditMode = this.toggleEditMode.bind(this);
|
||||
this.onDeleteDashboard = this.onDeleteDashboard.bind(this);
|
||||
|
@ -93,20 +89,16 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.onUpdateLayout = this.onUpdateLayout.bind(this);
|
||||
this.onOpenInfo = this.onOpenInfo.bind(this);
|
||||
this.onCloseInfo = this.onCloseInfo.bind(this);
|
||||
this.onExport = this.onExport.bind(this);
|
||||
this.onCloseExport = this.onCloseExport.bind(this);
|
||||
this.onClickDownloadFile = this.onClickDownloadFile.bind(this);
|
||||
this.onChangeDownloadFormat = this.onChangeDownloadFormat.bind(this);
|
||||
this.onDownloadDashboard = this.onDownloadDashboard.bind(this);
|
||||
this.onSaveAsTemplate = this.onSaveAsTemplate.bind(this);
|
||||
this.newTemplateNameChange = this.newTemplateNameChange.bind(this);
|
||||
this.onSaveAsTemplateApprove = this.onSaveAsTemplateApprove.bind(this);
|
||||
this.onSaveAsTemplateCancel = this.onSaveAsTemplateCancel.bind(this);
|
||||
this.newTemplateDescriptionChange = this.newTemplateDescriptionChange.bind(this);
|
||||
this.onVisibilityStoreChange = this.onVisibilityStoreChange.bind(this);
|
||||
|
||||
VisibilityStore.listen(this.onVisibilityStoreChange);
|
||||
|
||||
VisibilityStore.listen(state => {
|
||||
this.setState({ visibilityFlags: state.flags });
|
||||
});
|
||||
this.state.newTemplateName = this.props.dashboard.name;
|
||||
this.state.newTemplateDescription = this.props.dashboard.description;
|
||||
}
|
||||
|
@ -114,6 +106,7 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
componentDidMount() {
|
||||
let { dashboard } = this.props;
|
||||
let { mounted } = this.state;
|
||||
this.onLayoutChange = this.onLayoutChangeActive;
|
||||
|
||||
if (dashboard && !mounted) {
|
||||
|
||||
|
@ -142,6 +135,15 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.componentDidMount();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.onLayoutChange = this.onLayoutChangeInactive;
|
||||
VisibilityStore.unlisten(this.onVisibilityStoreChange);
|
||||
}
|
||||
|
||||
onVisibilityStoreChange(state: any) {
|
||||
this.setState({ visibilityFlags: state.flags });
|
||||
}
|
||||
|
||||
onBreakpointChange(breakpoint: any) {
|
||||
var layouts = this.state.layouts;
|
||||
layouts[breakpoint] = layouts[breakpoint] || this.layouts[breakpoint];
|
||||
|
@ -151,31 +153,29 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
});
|
||||
}
|
||||
|
||||
onLayoutChange(layout: any, layouts: any) {
|
||||
onLayoutChange(layout: any, layouts: any) { }
|
||||
|
||||
onLayoutChangeActive(layout: any, layouts: any) {
|
||||
|
||||
// Waiting for breakpoint to change
|
||||
let currentBreakpoint = this.state.currentBreakpoint;
|
||||
setTimeout(
|
||||
() => {
|
||||
if (currentBreakpoint !== this.state.currentBreakpoint) { return; }
|
||||
let breakpoint = this.state.currentBreakpoint;
|
||||
let newLayouts = this.state.layouts;
|
||||
newLayouts[breakpoint] = layout;
|
||||
this.setState({
|
||||
layouts: newLayouts
|
||||
});
|
||||
|
||||
var breakpoint = this.state.currentBreakpoint;
|
||||
var newLayouts = this.state.layouts;
|
||||
newLayouts[breakpoint] = layout;
|
||||
this.setState({
|
||||
layouts: newLayouts
|
||||
});
|
||||
// Saving layout to API
|
||||
let { dashboard } = this.props;
|
||||
dashboard.layouts = dashboard.layouts || {};
|
||||
dashboard.layouts[breakpoint] = layout;
|
||||
|
||||
// Saving layout to API
|
||||
let { dashboard } = this.props;
|
||||
dashboard.layouts = dashboard.layouts || {};
|
||||
dashboard.layouts[breakpoint] = layout;
|
||||
if (this.state.editMode) {
|
||||
ConfigurationsActions.saveConfiguration(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.editMode) {
|
||||
ConfigurationsActions.saveConfiguration(dashboard);
|
||||
}
|
||||
},
|
||||
500);
|
||||
onLayoutChangeInactive(layout: any, layouts: any) {
|
||||
}
|
||||
|
||||
onConfigDashboard() {
|
||||
|
@ -244,6 +244,7 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.setState({ editMode: !this.state.editMode });
|
||||
this.setState({ editMode: !this.state.editMode });
|
||||
}
|
||||
|
||||
onOpenInfo(html: string) {
|
||||
this.setState({ infoVisible: true, infoHtml: html });
|
||||
}
|
||||
|
@ -252,15 +253,6 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
this.setState({ infoVisible: false });
|
||||
}
|
||||
|
||||
onExport() {
|
||||
const data = exportDataSources();
|
||||
let downloadFiles: IDownloadFile[] = createDownloadFiles(data);
|
||||
downloadFiles.sort((a, b) => {
|
||||
return a.source === b.source ? a.filename > b.filename ? 1 : -1 : a.source > b.source ? 1 : -1 ;
|
||||
});
|
||||
this.setState({ askDownload: true, downloadFiles: downloadFiles });
|
||||
}
|
||||
|
||||
onDownloadDashboard() {
|
||||
let { dashboard } = this.props;
|
||||
dashboard.layouts = dashboard.layouts || {};
|
||||
|
@ -270,34 +262,13 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
downloadBlob('return ' + stringDashboard, 'application/json', dashboardName + '.private.js');
|
||||
}
|
||||
|
||||
onCloseExport(event: any) {
|
||||
this.setState({ askDownload: false });
|
||||
}
|
||||
|
||||
onClickDownloadFile(file: IDownloadFile, event: any) {
|
||||
const { downloadFormat } = this.state;
|
||||
if (downloadFormat === 'json') {
|
||||
downloadBlob(file.json, 'application/json', file.filename + '.json');
|
||||
} else {
|
||||
downloadBlob(file.csv, 'text/csv', file.filename + '.csv');
|
||||
}
|
||||
}
|
||||
|
||||
onChangeDownloadFormat(value: string, event: any) {
|
||||
this.setState({ downloadFormat: value });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const { dashboard } = this.props;
|
||||
const {
|
||||
currentBreakpoint,
|
||||
grid,
|
||||
editMode,
|
||||
askDelete,
|
||||
askDownload,
|
||||
downloadFiles,
|
||||
downloadFormat,
|
||||
askConfig ,
|
||||
askSaveAsTemplate,
|
||||
newTemplateName,
|
||||
|
@ -324,26 +295,19 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
|
||||
if (!editMode) {
|
||||
toolbarActions.push(
|
||||
(
|
||||
<span>
|
||||
<Button key="downloadDashboard" icon tooltipLabel="Download Dashboard" onClick={this.onDownloadDashboard}>
|
||||
file_download
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
(
|
||||
<span>
|
||||
<Button key="export" icon tooltipLabel="Export data" onClick={this.onExport}>
|
||||
play_for_work
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
(
|
||||
<span>
|
||||
<Button key="info" icon tooltipLabel="Info" onClick={this.onOpenInfo.bind(this, dashboard.html)}>
|
||||
info
|
||||
</Button>
|
||||
</span>
|
||||
),
|
||||
(
|
||||
<span>
|
||||
<Button key="downloadDashboard" icon tooltipLabel="Download Dashboard" onClick={this.onDownloadDashboard}>
|
||||
file_download
|
||||
</Button>
|
||||
</span>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
|
@ -386,37 +350,6 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
</Button></span>
|
||||
)
|
||||
);
|
||||
|
||||
const fileAvatar = (downloadFormat === 'json') ?
|
||||
<Avatar suffix="red" icon={<FontIcon>insert_drive_file</FontIcon>} />
|
||||
: <Avatar suffix="green" icon={<FontIcon>description</FontIcon>} /> ;
|
||||
|
||||
let downloadItems = [];
|
||||
let prevSection = '';
|
||||
if (!_.isEmpty(downloadFiles)) {
|
||||
Object.keys(downloadFiles).forEach((key, index) => {
|
||||
const item: IDownloadFile = downloadFiles[key];
|
||||
if ( prevSection !== item.source ) {
|
||||
if (prevSection !== '') {
|
||||
downloadItems.push(<Divider key={item.source + '_' + index} className="md-cell md-cell--12" />);
|
||||
}
|
||||
downloadItems.push(
|
||||
<Subheader primaryText={item.source} key={item.source + index} className="md-cell md-cell--12" />);
|
||||
}
|
||||
downloadItems.push(
|
||||
<ListItem
|
||||
key={item.filename + index}
|
||||
leftAvatar={fileAvatar}
|
||||
rightIcon={<FontIcon>file_download</FontIcon>}
|
||||
primaryText={item.filename}
|
||||
secondaryText={'.' + downloadFormat}
|
||||
onClick={this.onClickDownloadFile.bind(this, item)}
|
||||
className="md-cell md-cell--3"
|
||||
/>
|
||||
);
|
||||
prevSection = item.source;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: '100%'}}>
|
||||
|
@ -457,51 +390,23 @@ export default class Dashboard extends React.Component<IDashboardProps, IDashboa
|
|||
{renderHTML(infoHtml)}
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
id="downloadData"
|
||||
title={(
|
||||
<Toolbar
|
||||
title="Export Data"
|
||||
fixed
|
||||
style={{ width: '100%' }}
|
||||
actions={(
|
||||
<SelectField
|
||||
id="selectExportFormat"
|
||||
placeholder="File format"
|
||||
position={SelectField.Positions.BELOW}
|
||||
menuItems={['json', 'csv']}
|
||||
defaultValue={downloadFormat}
|
||||
onChange={this.onChangeDownloadFormat.bind(this)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
visible={askDownload}
|
||||
focusOnMount={false}
|
||||
onHide={this.onCloseExport}
|
||||
dialogStyle={{ width: '80%' }}
|
||||
contentStyle={{ marginTop: '20px' }}
|
||||
>
|
||||
<List className="md-grid" style={{ maxHeight: 400 }}>
|
||||
{downloadItems}
|
||||
</List>
|
||||
</Dialog>
|
||||
|
||||
<Editor dashboard={dashboard} />
|
||||
|
||||
<Settings dashboard={dashboard} />
|
||||
|
||||
<Dialog
|
||||
id="speedBoost"
|
||||
id="deleteDashboard"
|
||||
visible={askDelete}
|
||||
title="Are you sure?"
|
||||
aria-labelledby="speedBoostDescription"
|
||||
aria-labelledby="deleteDashboardDescription"
|
||||
modal
|
||||
actions={[
|
||||
{ onClick: this.onDeleteDashboardApprove, primary: false, label: 'Permanently Delete', },
|
||||
{ onClick: this.onDeleteDashboardCancel, primary: true, label: 'Cancel' }
|
||||
]}
|
||||
>
|
||||
<p id="speedBoostDescription" className="md-color--secondary-text">
|
||||
<p id="deleteDashboardDescription" className="md-color--secondary-text">
|
||||
Deleting this dashboard will remove all Connections/Customization you have made to it.
|
||||
Are you sure you want to permanently delete this dashboard?
|
||||
</p>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import plugins from './generic/plugins';
|
||||
import * as formats from '../utils/data-formats';
|
||||
|
||||
import { DataSourceConnector } from '../data-sources/DataSourceConnector';
|
||||
import VisibilityActions from '../actions/VisibilityActions';
|
||||
|
@ -58,9 +59,14 @@ export default class ElementConnector {
|
|||
|
||||
dashboard.elements.forEach((element, idx) => {
|
||||
var ReactElement = plugins[element.type];
|
||||
var { id, dependencies, actions, props, title, subtitle, size, theme, location } = element;
|
||||
var { id, dependencies, source, actions, props, title, subtitle, size, theme, location } = element;
|
||||
var layoutProps = _.find(layout, { 'i': id });
|
||||
|
||||
if (source && typeof ReactElement.fromSource === 'function') {
|
||||
let fromSource = ReactElement.fromSource(source);
|
||||
dependencies = _.extend({}, dependencies, fromSource);
|
||||
}
|
||||
|
||||
if (dependencies && dependencies.visible && !visibilityFlags[dependencies.visible]) {
|
||||
if (typeof visibilityFlags[dependencies.visible] === 'undefined') {
|
||||
let flagDependencies = DataSourceConnector.extrapolateDependencies({ value: dependencies.visible });
|
||||
|
@ -79,7 +85,7 @@ export default class ElementConnector {
|
|||
elements.push(
|
||||
<div key={id}>
|
||||
<ReactElement
|
||||
id={id + idx}
|
||||
id={id + '@' + idx}
|
||||
dependencies={dependencies}
|
||||
actions={actions || {}}
|
||||
props={props || {}}
|
||||
|
@ -99,18 +105,25 @@ export default class ElementConnector {
|
|||
filters: React.Component<any, any>[],
|
||||
additionalFilters: React.Component<any, any>[]
|
||||
} {
|
||||
var filters = [];
|
||||
var additionalFilters = [];
|
||||
let filters = [];
|
||||
let additionalFilters = [];
|
||||
dashboard.filters.forEach((element, idx) => {
|
||||
var ReactElement = plugins[element.type];
|
||||
let ReactElement = plugins[element.type];
|
||||
let { dependencies, source, actions, title, subtitle, icon } = element;
|
||||
|
||||
if (source && typeof ReactElement.fromSource === 'function') {
|
||||
let fromSource = ReactElement.fromSource(source);
|
||||
dependencies = _.extend({}, dependencies, fromSource);
|
||||
}
|
||||
|
||||
(element.first ? filters : additionalFilters).push(
|
||||
<ReactElement
|
||||
key={idx}
|
||||
dependencies={element.dependencies}
|
||||
actions={element.actions}
|
||||
title={element.title}
|
||||
subtitle={element.subtitle}
|
||||
icon={element.icon}
|
||||
dependencies={dependencies}
|
||||
actions={actions}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
icon={icon}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import ConfigurationStore from '../../stores/ConfigurationsStore';
|
|||
import ConfigurationsActions from '../../actions/ConfigurationsActions';
|
||||
import utils from '../../utils';
|
||||
|
||||
import IDownloadFile, { exportDataSources, createDownloadFiles, downloadBlob } from '../Dashboard/DownloadFile';
|
||||
import { downloadBlob } from '../Dashboard/DownloadFile';
|
||||
|
||||
const renderHTML = require('react-render-html');
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
|
|||
var sendOriginal = XMLHttpRequest.prototype.send;
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method: string, url: string, async?: boolean, _?: string, __?: string) {
|
||||
SpinnerActions.startRequestLoading();
|
||||
SpinnerActions.startRequestLoading.defer(null);
|
||||
openOriginal.apply(this, arguments);
|
||||
};
|
||||
|
||||
|
@ -39,7 +39,7 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
|
|||
|
||||
// readyState === 4: means the response is complete
|
||||
if (_xhr.readyState === 4) {
|
||||
SpinnerActions.endRequestLoading();
|
||||
SpinnerActions.endRequestLoading.defer(null);
|
||||
|
||||
if (_xhr.status === 429) {
|
||||
self._429ApplicationInsights();
|
||||
|
@ -53,9 +53,14 @@ export default class Spinner extends React.Component<any, ISpinnerState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.onChange(SpinnerStore.getState());
|
||||
SpinnerStore.listen(this.onChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SpinnerStore.unlisten(this.onChange);
|
||||
}
|
||||
|
||||
_429ApplicationInsights() {
|
||||
let toast: IToast = { text: 'You have reached the maximum number of Application Insights requests.' };
|
||||
ToastActions.addToast(toast);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import alt, { AbstractActions } from '../../alt';
|
||||
|
||||
interface ISpinnerActions {
|
||||
startPageLoading(): void;
|
||||
endPageLoading(): void;
|
||||
startRequestLoading(): void;
|
||||
endRequestLoading(): void;
|
||||
startPageLoading: AltJS.Action<any>;
|
||||
endPageLoading: AltJS.Action<any>;
|
||||
startRequestLoading: AltJS.Action<any>;
|
||||
endRequestLoading: AltJS.Action<any>;
|
||||
}
|
||||
|
||||
class SpinnerActions extends AbstractActions /*implements ISpinnerActions*/ {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import Tooltip from './Tooltip';
|
||||
|
||||
export default Tooltip;
|
|
@ -29,11 +29,36 @@ interface IAreaState extends IGenericState {
|
|||
export default class Area extends GenericComponent<IAreaProps, IAreaState> {
|
||||
|
||||
static editor = AreaSettings;
|
||||
|
||||
static defaultProps = {
|
||||
isStacked: true
|
||||
isStacked: false
|
||||
};
|
||||
|
||||
state = {
|
||||
timeFormat: '',
|
||||
values: [],
|
||||
lines: [],
|
||||
isStacked: this.props.isStacked
|
||||
};
|
||||
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
values: GenericComponent.sourceFormat(source, 'graphData'),
|
||||
lines: GenericComponent.sourceFormat(source, 'lines'),
|
||||
timeFormat: GenericComponent.sourceFormat(source, 'timeFormat')
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: IAreaProps) {
|
||||
super(props);
|
||||
|
||||
// apply nested props
|
||||
if (props && props.props) {
|
||||
if (props.props.isStacked !== undefined) {
|
||||
this.state.isStacked = props.props.isStacked as boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dateFormat(time: string): string {
|
||||
return moment(time).format('MMM-DD');
|
||||
}
|
||||
|
@ -43,18 +68,16 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
|
|||
}
|
||||
|
||||
generateWidgets() {
|
||||
let checked = this.is('isStacked');
|
||||
const { isStacked } = this.state;
|
||||
return (
|
||||
<div className="widgets">
|
||||
<Switch
|
||||
id="stack"
|
||||
name="stack"
|
||||
label="Stack"
|
||||
checked={checked}
|
||||
defaultChecked
|
||||
onChange={this.handleStackChange}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
id="stack"
|
||||
name="stack"
|
||||
label="Stack"
|
||||
checked={isStacked}
|
||||
defaultChecked
|
||||
onChange={this.handleStackChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -64,23 +87,18 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
var { timeFormat, values, lines } = this.state;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { showLegend, areaProps } = props;
|
||||
const { timeFormat, values, lines, isStacked } = this.state;
|
||||
const { id, title, subtitle, theme, props } = this.props;
|
||||
const { showLegend, areaProps } = props;
|
||||
|
||||
var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
|
||||
var themeColors = theme || ThemeColors;
|
||||
const format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
|
||||
const themeColors = theme || ThemeColors;
|
||||
|
||||
// gets the 'isStacked' boolean option from state, passed props or default values (in that order).
|
||||
var isStacked = this.is('isStacked');
|
||||
let stackProps = {};
|
||||
if (isStacked) {
|
||||
stackProps['stackId'] = '1';
|
||||
}
|
||||
const stackProps = !isStacked ? {} : { stackId : 1 };
|
||||
|
||||
var widgets = this.generateWidgets();
|
||||
const widgets = this.generateWidgets();
|
||||
|
||||
var fillElements = [];
|
||||
let fillElements = [];
|
||||
if (values && values.length && lines) {
|
||||
fillElements = lines.map((line, idx) => {
|
||||
return (
|
||||
|
@ -97,8 +115,7 @@ export default class Area extends GenericComponent<IAreaProps, IAreaState> {
|
|||
}
|
||||
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
{widgets}
|
||||
<Card id={id} title={title} subtitle={subtitle} widgets={widgets}>
|
||||
<ResponsiveContainer>
|
||||
<AreaChart
|
||||
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
|
||||
|
|
|
@ -21,23 +21,28 @@ interface IBarProps extends IGenericProps {
|
|||
};
|
||||
|
||||
interface IBarState extends IGenericState {
|
||||
values: Object[];
|
||||
bars: Object[];
|
||||
values: any[];
|
||||
bars: any[];
|
||||
}
|
||||
|
||||
export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
||||
|
||||
static editor = settings;
|
||||
|
||||
state = {
|
||||
values: [],
|
||||
bars: []
|
||||
};
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
values: GenericComponent.sourceFormat(source, 'values'),
|
||||
bars: GenericComponent.sourceFormat(source, 'bars')
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.state = {
|
||||
values: [],
|
||||
bars: []
|
||||
};
|
||||
}
|
||||
|
||||
handleClick(data: any, index: number) {
|
||||
|
@ -45,9 +50,11 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
var { values, bars } = this.state;
|
||||
var { title, subtitle, props } = this.props;
|
||||
var { barProps, showLegend, nameKey } = props;
|
||||
let { values, bars } = this.state;
|
||||
let { id, title, subtitle, props } = this.props;
|
||||
let { barProps, showLegend, nameKey } = props;
|
||||
|
||||
nameKey = nameKey || 'value';
|
||||
|
||||
if (!values) {
|
||||
return null;
|
||||
|
@ -55,7 +62,7 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
|
||||
if (!values || !values.length) {
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<div style={{ padding: 20 }}>No data is available</div>
|
||||
</Card>
|
||||
);
|
||||
|
@ -78,7 +85,7 @@ export default class BarData extends GenericComponent<IBarProps, IBarState> {
|
|||
|
||||
// Todo: Receive the width of the SVG component from the container
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<BarChart
|
||||
data={values}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent';
|
||||
import * as _ from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import { Card, CardText } from 'react-md/lib/Cards';
|
||||
import FontIcon from 'react-md/lib/FontIcons';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
|
||||
import { GenericComponent, IGenericProps, IGenericState } from '../GenericComponent';
|
||||
import Card from '../../Card';
|
||||
|
||||
const styles = {
|
||||
autoscroll: {
|
||||
|
@ -40,7 +40,7 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
|
|||
}
|
||||
|
||||
render() {
|
||||
const { props } = this.props;
|
||||
const { props, id, title } = this.props;
|
||||
const { cols, hideBorders } = props;
|
||||
const { values } = this.state;
|
||||
|
||||
|
@ -74,7 +74,12 @@ export default class Detail extends GenericComponent<IDetailProps, IDetailState>
|
|||
});
|
||||
|
||||
return (
|
||||
<Card className={hideBorders ? 'hide-borders' : ''} style={styles.autoscroll}>
|
||||
<Card
|
||||
id={id}
|
||||
title={title}
|
||||
hideTitle={true}
|
||||
className={hideBorders ? 'hide-borders' : ''}
|
||||
contentStyle={styles.autoscroll}>
|
||||
{lists}
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -23,6 +23,15 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
|
|||
|
||||
private id: string = null;
|
||||
|
||||
static sourceFormat(source: string, variable: string) {
|
||||
return source + ((source || '').indexOf(':') >= 0 ? '-' : ':') + variable;
|
||||
}
|
||||
|
||||
static sourceAction(source: string, variable: string, action: string) {
|
||||
let sourceFormat = GenericComponent.sourceFormat(source, variable).split(':');
|
||||
return sourceFormat.join(`:${action}:`);
|
||||
}
|
||||
|
||||
constructor(props: T1) {
|
||||
super(props);
|
||||
|
||||
|
@ -87,24 +96,6 @@ export abstract class GenericComponent<T1 extends IGenericProps, T2 extends IGen
|
|||
|
||||
abstract render();
|
||||
|
||||
/**
|
||||
* returns boolean option from state, passed props or default values (in that order).
|
||||
* @param property name of property
|
||||
*/
|
||||
protected is(property: string): boolean {
|
||||
if (this.state[property] !== undefined && typeof(this.state[property]) === 'boolean') {
|
||||
return this.state[property];
|
||||
}
|
||||
let { props } = this.props;
|
||||
if (props && props[property] !== undefined && typeof(props[property]) === 'boolean') {
|
||||
return props[property] as boolean;
|
||||
}
|
||||
if (this.props[property] !== undefined && typeof(this.props[property]) === 'boolean') {
|
||||
return this.props[property];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private onStateChange(state: any) {
|
||||
var result = DataSourceConnector.extrapolateDependencies(this.props.dependencies);
|
||||
var updatedState: IGenericState = {};
|
||||
|
|
|
@ -44,7 +44,7 @@ interface IMapDataProps extends IGenericProps {
|
|||
};
|
||||
|
||||
interface IMapDataState extends IGenericState {
|
||||
markers: Object[];
|
||||
markers: any[];
|
||||
locations: any[];
|
||||
}
|
||||
|
||||
|
@ -56,13 +56,19 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
|
|||
maxZoom: 8,
|
||||
};
|
||||
|
||||
state = {
|
||||
markers: [],
|
||||
locations: [],
|
||||
};
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
locations: source
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: IMapDataProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
markers: [],
|
||||
locations: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -127,7 +133,7 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
|
|||
|
||||
render() {
|
||||
const { markers } = this.state;
|
||||
const { title, subtitle, props, mapProps } = this.props;
|
||||
const { id, title, subtitle, props, mapProps } = this.props;
|
||||
|
||||
if (!markers) {
|
||||
return null;
|
||||
|
@ -148,7 +154,7 @@ export default class MapData extends GenericComponent<IMapDataProps, IMapDataSta
|
|||
const mapProperties = { ...MapData.defaultProps, ...mapProps };
|
||||
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<Map
|
||||
className="markercluster-map"
|
||||
style={styles.map}
|
||||
|
|
|
@ -53,12 +53,12 @@ export default class MenuFilter extends GenericComponent<any, any> {
|
|||
selectNone: 'Clear filters'
|
||||
};
|
||||
|
||||
state = {
|
||||
overlay: false,
|
||||
values: [],
|
||||
selectedValues: [],
|
||||
originalSelectedValues: []
|
||||
};
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
selectedValue: GenericComponent.sourceFormat(source, 'values-selected'),
|
||||
values: GenericComponent.sourceFormat(source, 'values-all')
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
@ -68,6 +68,13 @@ export default class MenuFilter extends GenericComponent<any, any> {
|
|||
this.hideOverlay = this.hideOverlay.bind(this);
|
||||
this.selectAll = this.selectAll.bind(this);
|
||||
this.selectNone = this.selectNone.bind(this);
|
||||
|
||||
this.state = {
|
||||
overlay: false,
|
||||
values: [],
|
||||
selectedValues: [],
|
||||
originalSelectedValues: []
|
||||
};
|
||||
}
|
||||
|
||||
toggleOverlay() {
|
||||
|
|
|
@ -38,6 +38,12 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
values: null
|
||||
};
|
||||
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
values: GenericComponent.sourceFormat(source, 'pieData')
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
@ -128,7 +134,7 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
|
||||
render() {
|
||||
var { values } = this.state;
|
||||
var { props, title, subtitle, layout, theme } = this.props;
|
||||
var { id, props, title, subtitle, layout, theme } = this.props;
|
||||
var { pieProps, showLegend, legendVerticalAlign } = props;
|
||||
|
||||
if (!values) {
|
||||
|
@ -139,7 +145,7 @@ export default class PieData extends GenericComponent<IPieProps, IPieState> {
|
|||
|
||||
// Todo: Receive the width of the SVG component from the container
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<PieChart>
|
||||
<Pie
|
||||
|
|
|
@ -36,7 +36,7 @@ export default class Scatter extends GenericComponent<IScatterProps, IScatterSta
|
|||
|
||||
render() {
|
||||
var { groupedValues } = this.state;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { id, title, subtitle, theme, props } = this.props;
|
||||
var { scatterProps, groupTitles } = props;
|
||||
|
||||
var { xDataKey, yDataKey, zDataKey, zRange } = this.props.props;
|
||||
|
@ -70,7 +70,7 @@ export default class Scatter extends GenericComponent<IScatterProps, IScatterSta
|
|||
}
|
||||
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<ScatterChart margin={{ top: 5, right: 30, left: 20, bottom: 5 }} {...scatterProps}>
|
||||
<XAxis dataKey={xDataKey} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { Card } from 'react-md/lib/Cards';
|
||||
import Card from '../../Card';
|
||||
import FontIcon from 'react-md/lib/FontIcons';
|
||||
import Tooltip from '../../Tooltip';
|
||||
|
||||
|
@ -15,7 +15,16 @@ const styles = {
|
|||
float: 'none',
|
||||
padding: 0,
|
||||
verticalAlign: 'middle'
|
||||
}
|
||||
} as React.CSSProperties,
|
||||
title: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
} as React.CSSProperties,
|
||||
content: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
overflow: 'visible'
|
||||
} as React.CSSProperties
|
||||
};
|
||||
|
||||
interface IScorecardProps extends IGenericProps {
|
||||
|
@ -31,6 +40,20 @@ interface IScorecardProps extends IGenericProps {
|
|||
export default class Scorecard extends GenericComponent<IScorecardProps, any> {
|
||||
|
||||
static editor = settings;
|
||||
static fromSource(source: any) {
|
||||
if (!source || typeof source !== 'object') { return {}; }
|
||||
|
||||
let mappings = {};
|
||||
_.keys(source).forEach(key => {
|
||||
mappings['card_' + key + '_value'] = GenericComponent.sourceFormat(source[key], 'value');
|
||||
mappings['card_' + key + '_heading'] = GenericComponent.sourceFormat(source[key], 'heading');
|
||||
mappings['card_' + key + '_color'] = GenericComponent.sourceFormat(source[key], 'color');
|
||||
mappings['card_' + key + '_icon'] = GenericComponent.sourceFormat(source[key], 'icon');
|
||||
mappings['card_' + key + '_subvalue'] = GenericComponent.sourceFormat(source[key], 'subvalue');
|
||||
mappings['card_' + key + '_subheading'] = GenericComponent.sourceFormat(source[key], 'subheading');
|
||||
});
|
||||
return mappings;
|
||||
}
|
||||
|
||||
constructor(props: IScorecardProps) {
|
||||
super(props);
|
||||
|
@ -47,7 +70,7 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
|
|||
|
||||
render() {
|
||||
let { values, value, icon, subvalue, color, className } = this.state;
|
||||
let { title, props, actions } = this.props;
|
||||
let { id, title, props, actions } = this.props;
|
||||
let { subheading, colorPosition, scorecardWidth, onClick, tooltip } = props;
|
||||
|
||||
if (_.has(this.state, 'values')) {
|
||||
|
@ -90,8 +113,8 @@ export default class Scorecard extends GenericComponent<IScorecardProps, any> {
|
|||
this.valueToCard(val, idx, className, colorPosition, scorecardWidth));
|
||||
|
||||
return (
|
||||
<Card className="md-card-scorecard">
|
||||
<div className="md-grid--no-spacing">
|
||||
<Card id={id} title={title} hideTitle={true} titleStyle={styles.title} contentStyle={styles.content}>
|
||||
<div className="md-grid--no-spacing md-card-scorecard">
|
||||
{cards}
|
||||
</div>
|
||||
</Card>
|
||||
|
|
|
@ -93,7 +93,7 @@ export default class SplitPanel extends GenericComponent<ISplitViewProps, ISplit
|
|||
}
|
||||
|
||||
render() {
|
||||
const { props } = this.props;
|
||||
const { props, id } = this.props;
|
||||
const { cols, group, hideBorders, compact } = props;
|
||||
const { groups, values } = this.state;
|
||||
|
||||
|
@ -123,11 +123,12 @@ export default class SplitPanel extends GenericComponent<ISplitViewProps, ISplit
|
|||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
const table = (!values || values.length === 0) ?
|
||||
<CircularProgress key="loading" id="spinner" />
|
||||
: (
|
||||
<Table
|
||||
id={id}
|
||||
props={this.props.props}
|
||||
dependencies={this.props.dependencies}
|
||||
actions={this.props.actions || {}}
|
||||
|
|
|
@ -5,10 +5,10 @@ import * as moment from 'moment';
|
|||
import utils from '../../../utils';
|
||||
|
||||
import { DataTable, TableHeader, TableBody, TableRow, TableColumn, TablePagination } from 'react-md/lib/DataTables';
|
||||
import { Card, CardText, TableCardHeader } from 'react-md/lib/Cards';
|
||||
import FontIcon from 'react-md/lib/FontIcons';
|
||||
import Button from 'react-md/lib/Buttons/Button';
|
||||
import CircularProgress from 'react-md/lib/Progress/CircularProgress';
|
||||
import Card from '../../Card';
|
||||
|
||||
const styles = {
|
||||
autoscroll: {
|
||||
|
@ -100,7 +100,7 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { props } = this.props;
|
||||
const { props, id, title } = this.props;
|
||||
const { checkboxes, cols, rowClassNameField, hideBorders, compact } = props;
|
||||
const { values, rowIndex, rowsPerPage, currentPage, rowsPerPageItems } = this.state;
|
||||
|
||||
|
@ -109,7 +109,7 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
|
|||
}
|
||||
|
||||
let totalRows = values.length;
|
||||
let pageValues = values.slice(rowIndex, rowIndex + rowsPerPage) || [];
|
||||
let pageValues = Array.isArray(values) && values.slice(rowIndex, rowIndex + rowsPerPage) || [];
|
||||
|
||||
let renderColumn = (col: ITableColumnProps, value: any): JSX.Element => {
|
||||
let style = { color: col.color ? value[col.color] : null };
|
||||
|
@ -181,7 +181,11 @@ export default class Table extends GenericComponent<ITableProps, ITableState> {
|
|||
className += compact ? 'table-compact' : '';
|
||||
|
||||
return (
|
||||
<Card className={hideBorders ? 'hide-borders' : ''} style={styles.autoscroll}>
|
||||
<Card id={id}
|
||||
title={title}
|
||||
hideTitle={true}
|
||||
className={hideBorders ? 'hide-borders' : ''}
|
||||
contentStyle={styles.autoscroll}>
|
||||
<DataTable plain={!checkboxes} data={checkboxes} className={className} baseId="pagination" responsive={false}>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
|
|
@ -7,6 +7,13 @@ export default class TextFilter extends GenericComponent<any, any> {
|
|||
static defaultProps = {
|
||||
title: 'Select'
|
||||
};
|
||||
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
selectedValue: GenericComponent.sourceFormat(source, 'values-selected'),
|
||||
values: GenericComponent.sourceFormat(source, 'values-all')
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
|
|
@ -25,6 +25,13 @@ interface ITimelineState extends IGenericState {
|
|||
export default class Timeline extends GenericComponent<ITimelineProps, ITimelineState> {
|
||||
|
||||
static editor = settings;
|
||||
static fromSource(source: string) {
|
||||
return {
|
||||
values: GenericComponent.sourceFormat(source, 'graphData'),
|
||||
lines: GenericComponent.sourceFormat(source, 'lines'),
|
||||
timeFormat: GenericComponent.sourceFormat(source, 'timeFormat')
|
||||
};
|
||||
}
|
||||
|
||||
dateFormat(time: string) {
|
||||
return moment(time).format('MMM-DD');
|
||||
|
@ -36,7 +43,7 @@ export default class Timeline extends GenericComponent<ITimelineProps, ITimeline
|
|||
|
||||
render() {
|
||||
var { timeFormat, values, lines } = this.state;
|
||||
var { title, subtitle, theme, props } = this.props;
|
||||
var { id, title, subtitle, theme, props } = this.props;
|
||||
var { lineProps } = props;
|
||||
|
||||
var format = timeFormat === 'hour' ? this.hourFormat : this.dateFormat;
|
||||
|
@ -59,7 +66,7 @@ export default class Timeline extends GenericComponent<ITimelineProps, ITimeline
|
|||
}
|
||||
|
||||
return (
|
||||
<Card title={title} subtitle={subtitle}>
|
||||
<Card id={id} title={title} subtitle={subtitle}>
|
||||
<ResponsiveContainer>
|
||||
<LineChart data={values} margin={{ top: 5, right: 30, left: 20, bottom: 5 }} {...lineProps}>
|
||||
<XAxis dataKey="time" tickFormatter={format} minTickGap={20} />
|
||||
|
|
|
@ -81,6 +81,12 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* MD tooltip to appear on top of Ace editor */
|
||||
|
||||
.md-tooltip-container {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
|
||||
.table > .primary {
|
||||
|
|
|
@ -5,6 +5,9 @@ import DialogsActions from '../components/generic/Dialogs/DialogsActions';
|
|||
import datasourcePluginsMappings from './plugins/PluginsMapping';
|
||||
import VisibilityActions from '../actions/VisibilityActions';
|
||||
import VisibilityStore from '../stores/VisibilityStore';
|
||||
import * as formats from '../utils/data-formats';
|
||||
|
||||
const DataFormatTypes = formats.DataFormatTypes;
|
||||
|
||||
export interface IDataSource {
|
||||
id: string;
|
||||
|
@ -204,6 +207,28 @@ export class DataSourceConnector {
|
|||
return this.dataSources[name];
|
||||
}
|
||||
|
||||
static handleDataFormat(
|
||||
format: string | formats.IDataFormat,
|
||||
plugin: IDataSourcePlugin,
|
||||
state: any,
|
||||
dependencies: IDictionary) {
|
||||
|
||||
if (!format) { return null; }
|
||||
|
||||
const prevState = DataSourceConnector.dataSources[plugin._props.id].store.getState();
|
||||
|
||||
let result = {};
|
||||
|
||||
let formatName = (typeof format === 'string' ? format : format.type) || DataFormatTypes.none.toString();
|
||||
|
||||
if (formatName && typeof formats[formatName] === 'function') {
|
||||
let additionalValues = formats[formatName](format, state, dependencies, plugin, prevState) || {};
|
||||
Object.assign(result, additionalValues);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static connectDataSource(sourceDS: IDataSource) {
|
||||
// Connect sources and dependencies
|
||||
sourceDS.store.listen((state) => {
|
||||
|
@ -307,7 +332,7 @@ export class DataSourceConnector {
|
|||
(<any> this).setState(newData);
|
||||
}
|
||||
}
|
||||
var StoreClass = alt.createStore(NewStoreClass, config.id + '-Store');
|
||||
var StoreClass = alt.createStore(NewStoreClass as any, config.id + '-Store');
|
||||
return StoreClass;
|
||||
}
|
||||
|
||||
|
@ -323,24 +348,26 @@ export class DataSourceConnector {
|
|||
}
|
||||
|
||||
// Callibrate calculated values
|
||||
var calculated = plugin._props.calculated;
|
||||
var state = DataSourceConnector.dataSources[plugin._props.id].store.getState();
|
||||
const calculated = plugin._props.calculated;
|
||||
|
||||
let state = DataSourceConnector.dataSources[plugin._props.id].store.getState();
|
||||
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') {
|
||||
var additionalValues = calculated(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => {
|
||||
result[key] = additionalValues[key];
|
||||
});
|
||||
let additionalValues = calculated(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => { result[key] = additionalValues[key]; });
|
||||
}
|
||||
|
||||
if (Array.isArray(calculated)) {
|
||||
calculated.forEach(calc => {
|
||||
var additionalValues = calc(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => {
|
||||
result[key] = additionalValues[key];
|
||||
});
|
||||
let additionalValues = calc(state, dependencies) || {};
|
||||
Object.keys(additionalValues).forEach(key => { result[key] = additionalValues[key]; });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import * as _ from 'lodash';
|
||||
import * as request from 'xhr-request';
|
||||
import { DataSourcePlugin, IOptions } from '../DataSourcePlugin';
|
||||
import { appInsightsUri } from './common';
|
||||
import ApplicationInsightsConnection from '../../connections/application-insights';
|
||||
import { DataSourceConnector } from '../../DataSourceConnector';
|
||||
import { DataSourceConnector, IDataSource } from '../../DataSourceConnector';
|
||||
import * as formats from '../../../utils/data-formats';
|
||||
|
||||
let connectionType = new ApplicationInsightsConnection();
|
||||
|
||||
|
@ -71,7 +73,7 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
let mappings: Array<any> = [];
|
||||
let queries: IDictionary = {};
|
||||
let table: string = null;
|
||||
let filters: Array<IFilterParams> = params.filters;
|
||||
let filters: Array<IFilterParams> = params.filters || [];
|
||||
|
||||
// Checking if this is a single query or a fork query
|
||||
let query: string;
|
||||
|
@ -142,10 +144,24 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
returnedResults[aTable] = resultTables.length > idx ? resultTables[idx] : null;
|
||||
// Get state for filter selection
|
||||
const prevState = DataSourceConnector.getDataSource(this._props.id).store.getState();
|
||||
|
||||
// Extract data formats
|
||||
let format = queries[aTable].format;
|
||||
if (format) {
|
||||
format = typeof format === 'string' ? { type: format } : format;
|
||||
format = _.extend({ args: {} }, format);
|
||||
format.args = _.extend({ prefix: aTable + '-' }, format.args);
|
||||
let result = { values: returnedResults[aTable] };
|
||||
let formatExtract = DataSourceConnector.handleDataFormat(format, this, result, dependencies);
|
||||
if (formatExtract) {
|
||||
Object.assign(returnedResults, formatExtract);
|
||||
}
|
||||
}
|
||||
|
||||
// Extracting calculated values
|
||||
let calc = queries[aTable].calculated;
|
||||
if (typeof calc === 'function') {
|
||||
var additionalValues = calc(returnedResults[aTable], dependencies, prevState) || {};
|
||||
let additionalValues = calc(returnedResults[aTable], dependencies, prevState) || {};
|
||||
Object.assign(returnedResults, additionalValues);
|
||||
}
|
||||
});
|
||||
|
@ -164,6 +180,23 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
}
|
||||
}
|
||||
|
||||
getElementQuery(dataSource: IDataSource, dependencies: IDict<any>, partialQuery: string, queryFilters: any): string {
|
||||
let timespan = '30d';
|
||||
const table = dataSource && dataSource['config'] && dataSource['config'].params &&
|
||||
dataSource['config'].params.table || null;
|
||||
if (dependencies && dependencies['timespan'] && dependencies['timespan']['queryTimespan']) {
|
||||
timespan = dependencies['timespan']['queryTimespan'];
|
||||
timespan = this.convertApplicationInsightsTimespan(timespan);
|
||||
} else if (dependencies && dependencies['queryTimespan']) {
|
||||
// handle dialog params
|
||||
timespan = dependencies['queryTimespan'];
|
||||
timespan = this.convertApplicationInsightsTimespan(timespan);
|
||||
}
|
||||
const filter = this.formatApplicationInsightsFilterString(queryFilters, dependencies);
|
||||
const query = this.formatApplicationInsightsQueryString(partialQuery, timespan, filter, table);
|
||||
return query;
|
||||
}
|
||||
|
||||
private mapAllTables(results: IQueryResults, mappings: Array<IDictionary>): any[][] {
|
||||
|
||||
if (!results || !results.Tables || !results.Tables.length) {
|
||||
|
@ -215,8 +248,8 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
const { dependency, queryProperty } = filter;
|
||||
const selectedFilters = dependencies[dependency] || [];
|
||||
if (selectedFilters.length > 0) {
|
||||
const f = 'where ' + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ') + ' | ';
|
||||
q = ` ${f} \n ${q} `;
|
||||
const f = 'where ' + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ');
|
||||
q = isForked ? ` ${f} |\n ${q} ` : q.replace(/^(\s?\w+\s*?){1}(.)*/gim, '$1 | ' + f + ' $2');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -259,4 +292,51 @@ export default class ApplicationInsightsQuery extends DataSourcePlugin<IQueryPar
|
|||
throw new Error('{ table, queries } should be of types { "string", { query1: {...}, query2: {...} } }.');
|
||||
}
|
||||
}
|
||||
|
||||
// element export formatting functions
|
||||
private formatApplicationInsightsQueryString(query: string, timespan: string, filter: string, table?: string) {
|
||||
let str = query.replace(/(\|)((\s??\n\s??)(.+?))/gm, '\n| $4'); // move '|' to start of each line
|
||||
str = str.replace(/(\s?\|\s?)([^\|])/gim, '\n| $2'); // newline on '|'
|
||||
str = str.replace(/(\sand\s)/gm, '\n $1'); // newline on 'and'
|
||||
str = str.replace(/([^\(\'\"])(,\s*)(?=(\w+[^\)\'\"]\w+))/gim, '$1,\n '); // newline on ','
|
||||
const timespanQuery = '| where timestamp > ago(' + timespan + ') \n';
|
||||
// Checks for '|' at start
|
||||
const matches = str.match(/^(\s*\||\s*\w+\s*\|)/ig);
|
||||
const start = (!matches || matches.length !== 1) ? '| ' : '';
|
||||
if (table) {
|
||||
str = table + ' \n' + timespanQuery + filter + start + str;
|
||||
} else {
|
||||
// Insert timespan and filter after table name
|
||||
str = str.replace(/^\s*(\w+)\s*/gi, '$1 \n' + timespanQuery + filter + start);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
private formatApplicationInsightsFilterString(filters: IStringDictionary[], dependencies: IDict<any>) {
|
||||
let str = '';
|
||||
if (!filters) {
|
||||
return str;
|
||||
}
|
||||
// Apply selected filters to connected query
|
||||
filters.forEach((filter) => {
|
||||
const { dependency, queryProperty } = filter;
|
||||
const selectedFilters: string[] = dependencies[dependency] || [];
|
||||
if (selectedFilters.length > 0) {
|
||||
const f = '| where ' + selectedFilters.map((value) => `${queryProperty}=="${value}"`).join(' or ');
|
||||
str = `${str}${f} \n`;
|
||||
}
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
private convertApplicationInsightsTimespan(timespan: string) {
|
||||
let str = timespan;
|
||||
if (timespan.substr(0, 2) === 'PT') {
|
||||
str = str.substring(2).toLowerCase();
|
||||
} else if (timespan.substr(0, 1) === 'P') {
|
||||
str = str.substring(1).toLowerCase();
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import { IDataSource } from '../DataSourceConnector';
|
||||
import { ToastActions } from '../../components/Toast';
|
||||
import { DataFormatTypes, IDataFormat } from '../../utils/data-formats';
|
||||
|
||||
export interface IDataSourceOptions {
|
||||
dependencies: (string | Object)[];
|
||||
|
@ -27,6 +28,7 @@ export interface IDataSourcePlugin {
|
|||
dependables: string[],
|
||||
actions: string[],
|
||||
params: IDictionary,
|
||||
format: string | IDataFormat,
|
||||
calculated: ICalculated
|
||||
};
|
||||
|
||||
|
@ -37,8 +39,10 @@ export interface IDataSourcePlugin {
|
|||
getActions(): string[];
|
||||
getParamKeys(): string[];
|
||||
getParams(): IDictionary;
|
||||
getFormat(): string | IDataFormat;
|
||||
getCalculated(): ICalculated;
|
||||
getConnection(): IStringDictionary;
|
||||
getElementQuery(dataSource: IDataSource, dependencies: IDict<any>, aQuery: string, queryFilters: any): string;
|
||||
}
|
||||
|
||||
export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
||||
|
@ -53,6 +57,7 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
dependables: [],
|
||||
actions: [ 'updateDependencies', 'failure', 'updateSelectedValues' ],
|
||||
params: <T> {},
|
||||
format: DataFormatTypes.none.toString(),
|
||||
calculated: {},
|
||||
autoUpdateIntervalMs: -1,
|
||||
};
|
||||
|
@ -73,6 +78,7 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
props.dependables = options.dependables || [];
|
||||
props.actions.push.apply(props.actions, options.actions || []);
|
||||
props.params = <T> (options.params || {});
|
||||
props.format = options.format || DataFormatTypes.none.toString();
|
||||
props.calculated = options.calculated || {};
|
||||
props.autoUpdateIntervalMs = options.autoUpdateIntervalMs || -1;
|
||||
|
||||
|
@ -139,10 +145,20 @@ export abstract class DataSourcePlugin<T> implements IDataSourcePlugin {
|
|||
return this._props.params;
|
||||
}
|
||||
|
||||
getFormat(): string | IDataFormat {
|
||||
return this._props.format || DataFormatTypes.none.toString();
|
||||
}
|
||||
|
||||
getCalculated() {
|
||||
return this._props.calculated;
|
||||
}
|
||||
|
||||
getElementQuery(dataSource: IDataSource, dependencies: IDict<any>, aQuery: string, queryFilters: any): string {
|
||||
const plugin = dataSource.plugin.type;
|
||||
console.warn(`'getElementQuery' function may not be fully implemented for the ${plugin} plugin.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
failure(error: any): void {
|
||||
ToastActions.addToast({ text: this.errorToMessage(error) });
|
||||
return error;
|
||||
|
|
|
@ -103,6 +103,25 @@ h2, .md-subheading-2 {
|
|||
float: right;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
.card-settings {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-settings > span {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.card-settings-btn {
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
.card-settings-btn:hover {
|
||||
color: $md-primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.react-draggable {
|
||||
|
|
|
@ -20,28 +20,28 @@ describe('Spinner', () => {
|
|||
});
|
||||
|
||||
it ('Start page loading', () => {
|
||||
SpinnerActions.startPageLoading();
|
||||
SpinnerActions.startPageLoading(null);
|
||||
|
||||
let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress);
|
||||
expect(progress.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Stop page loading', () => {
|
||||
SpinnerActions.endPageLoading();
|
||||
SpinnerActions.endPageLoading(null);
|
||||
|
||||
let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress);
|
||||
expect(progress.length).toBe(0);
|
||||
});
|
||||
|
||||
it ('Start request loading', () => {
|
||||
SpinnerActions.startRequestLoading();
|
||||
SpinnerActions.startRequestLoading(null);
|
||||
|
||||
let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress);
|
||||
expect(progress.length).toBe(1);
|
||||
});
|
||||
|
||||
it ('Start request loading', () => {
|
||||
SpinnerActions.endRequestLoading();
|
||||
SpinnerActions.endRequestLoading(null);
|
||||
|
||||
let progress = TestUtils.scryRenderedComponentsWithType(spinner, CircularProgress);
|
||||
expect(progress.length).toBe(0);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { ToastActions } from '../../components/Toast';
|
||||
import { IDataSourcePlugin } from '../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
export enum DataFormatTypes {
|
||||
none,
|
||||
timespan,
|
||||
flags,
|
||||
retention,
|
||||
timeline
|
||||
}
|
||||
|
||||
export interface IDataFormat {
|
||||
type: string;
|
||||
args: any;
|
||||
}
|
||||
|
||||
export function formatWarn(text: string, format: string, plugin: IDataSourcePlugin) {
|
||||
ToastActions.addToast({ text: `[format:${format}] text [data source:${plugin._props.id}]` });
|
||||
}
|
||||
|
||||
export function getPrefix(format: string | IDataFormat) {
|
||||
return (format && typeof format !== 'string' && format.args && format.args.prefix) || '';
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Formats a result to suite a bar chart
|
||||
*
|
||||
* Receives a list of filtering values:
|
||||
* 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' },
|
||||
* ]
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-bars": [ 'bar 1', 'bar 2', 'bar 3' ],
|
||||
* "prefix-values": [
|
||||
* { value: 'bar 1', series1Value: 10, series2Value: 20 },
|
||||
* { value: 'bar 2', series1Value: 15, series2Value: 0 },
|
||||
* { value: 'bar 3', series1Value: 0, series2Value: 44 },
|
||||
* ],
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format {
|
||||
* type: 'bars',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* valueField: string - The field name holding the value/y value of the bar
|
||||
* barsField: string - The field name holding the names for the bars
|
||||
* seriesField: string - The field name holding the series name (aggregation in a specific field)
|
||||
* valueMaxLength: number - At what length to cut string values (default: 13),
|
||||
* threshold: number - Under this threshold, the values will be aggregated to others (default: 0 - none)
|
||||
* othersValue: string - Name for the 'Others' field (default: 'Others')
|
||||
* }
|
||||
* }
|
||||
* @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 bars(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
if (typeof format === 'string') {
|
||||
return formatWarn('format should be an object with args', 'bars', plugin);
|
||||
}
|
||||
|
||||
const args = format.args || {};
|
||||
const prefix = getPrefix(format);
|
||||
const valueField = args.valueField || 'count';
|
||||
const barsField = args.barsField || null;
|
||||
const seriesField = args.seriesField || null;
|
||||
const valueMaxLength = args.valueMaxLength && parseInt(args.valueMaxLength, 10) || 13;
|
||||
const threshold = args.threshold || 0;
|
||||
const othersValue = args.othersValue || 'Others';
|
||||
|
||||
let values: any[] = state.values;
|
||||
|
||||
// Concating values with '...'
|
||||
if (values && values.length && valueMaxLength && (seriesField || barsField)) {
|
||||
const cutLength = Math.max(valueMaxLength - 3, 0);
|
||||
values.forEach(val => {
|
||||
if (seriesField && val[seriesField] && val[seriesField].length > valueMaxLength) {
|
||||
val[seriesField] = val[seriesField].substring(0, cutLength) + '...';
|
||||
}
|
||||
if (barsField && val[barsField] && val[barsField].length > valueMaxLength) {
|
||||
val[barsField] = val[barsField].substring(0, cutLength) + '...';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let result = {};
|
||||
let barValues = {};
|
||||
|
||||
// Setting the field describing the bars
|
||||
if (barsField) {
|
||||
|
||||
let series = {};
|
||||
values.forEach(val => {
|
||||
barValues[val[barsField]] = barValues[val[barsField]] || { value: val[barsField] };
|
||||
|
||||
if (threshold && val[valueField] < threshold) {
|
||||
barValues[val[barsField]][othersValue] = (barValues[val[barsField]][othersValue] || 0) + val[valueField];
|
||||
series[othersValue] = true;
|
||||
} else {
|
||||
barValues[val[barsField]][val[seriesField]] = val[valueField];
|
||||
series[val[seriesField]] = true;
|
||||
}
|
||||
});
|
||||
|
||||
result[prefix + 'bars'] = _.keys(series);
|
||||
result[prefix + 'values'] = _.values(barValues);
|
||||
|
||||
} else {
|
||||
result[prefix + 'bars'] = [ valueField ];
|
||||
result[prefix + 'values'] = values;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Formats a result to fit a filter.
|
||||
*
|
||||
* Receives a list of filtering values:
|
||||
* values: [
|
||||
* { field: 'value 1' },
|
||||
* { field: 'value 2' },
|
||||
* { field: 'value 3' },
|
||||
* ]
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-filters": [ 'value 1', 'value 2', 'value 3' ],
|
||||
* "prefix-selected": [ ],
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format {
|
||||
* type: 'filter',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* field: string - the field holding the filter values in the results (default = "value")
|
||||
* }
|
||||
* }
|
||||
* @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 filter (
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
const { values } = state;
|
||||
if (!values) { return null; }
|
||||
|
||||
const args = typeof format !== 'string' ? format.args : {};
|
||||
const prefix = getPrefix(format);
|
||||
const field = args.field || 'value';
|
||||
const unknown = args.unknown || 'unknown';
|
||||
|
||||
// This code is meant to fix the following scenario:
|
||||
// When "Timespan" filter changes, to "channels-selected" variable
|
||||
// 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;
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Turns a list of values into a list of flags
|
||||
*
|
||||
* Receives a list of filtering values (on the data source params variable):
|
||||
* params: {
|
||||
* values: [ 'value1', 'value2', 'value3' ]
|
||||
* }
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "value1": false, (will be true if prefix-selected contains "value1")
|
||||
* "value2": false,
|
||||
* "value3": false,
|
||||
* "prefix-filters": [ 'value 1', 'value 2', 'value 3' ],
|
||||
* "prefix-selected": [ ],
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format 'filter' | {
|
||||
* type: 'filter',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* }
|
||||
* }
|
||||
* @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 flags(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
const prefix = getPrefix(format);
|
||||
const params = plugin.getParams();
|
||||
if (!params || !Array.isArray(params.values)) {
|
||||
return formatWarn('A paramerter "values" is expected as an array on "params" in the data source', 'filter', plugin);
|
||||
}
|
||||
|
||||
if (!state) { return null; }
|
||||
|
||||
let flags = {};
|
||||
params.values.forEach(key => { flags[key] = state.selectedValue === key; });
|
||||
|
||||
flags[prefix + 'values-all'] = params.values;
|
||||
flags[prefix + 'values-selected'] = state.selectedValue;
|
||||
|
||||
return flags;
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Received a result in the form of:
|
||||
* values: [
|
||||
* {
|
||||
* totalUnique: number
|
||||
* totalUniqueUsersIn24hr: number
|
||||
* totalUniqueUsersIn7d: number
|
||||
* totalUniqueUsersIn30d: number
|
||||
* returning24hr: number
|
||||
* returning7d: number
|
||||
* returning30d: number
|
||||
* }
|
||||
* ]
|
||||
*
|
||||
* And returns the following format:
|
||||
* {
|
||||
* total: number
|
||||
* returning: number
|
||||
* values: [
|
||||
* {
|
||||
* timespan: '24 hours',
|
||||
* retention: '%',
|
||||
* returning: number,
|
||||
* unique:number
|
||||
* },
|
||||
* {
|
||||
* timespan: '7 days',
|
||||
* retention: '%',
|
||||
* returning: number,
|
||||
* unique:number
|
||||
* }
|
||||
* {
|
||||
* timespan: '30 days',
|
||||
* retention: '%',
|
||||
* returning: number,
|
||||
* unique:number
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @param format Plugin format parameter
|
||||
* @param state Current received state from data source
|
||||
* @param dependencies Dependencies for the plugin
|
||||
* should contain "selectedTimespan" equals to 'PT24H', 'P7D' etc...
|
||||
* @param plugin The entire plugin (for id generation, params etc...)
|
||||
*/
|
||||
export function retention (
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
const { values } = state;
|
||||
const { selectedTimespan } = dependencies;
|
||||
|
||||
if (!values || !values.length) { return null; }
|
||||
|
||||
const prefix = getPrefix(format);
|
||||
let result = {
|
||||
totalUnique: 0,
|
||||
totalUniqueUsersIn24hr: 0,
|
||||
totalUniqueUsersIn7d: 0,
|
||||
totalUniqueUsersIn30d: 0,
|
||||
returning24hr: 0,
|
||||
returning7d: 0,
|
||||
returning30d: 0,
|
||||
|
||||
total: 0,
|
||||
returning: 0,
|
||||
values: []
|
||||
};
|
||||
|
||||
_.extend(result, values[0]);
|
||||
|
||||
switch (selectedTimespan) {
|
||||
case 'PT24H':
|
||||
result.total = result.totalUniqueUsersIn24hr;
|
||||
result.returning = result.returning24hr;
|
||||
break;
|
||||
|
||||
case 'P7D':
|
||||
result.total = result.totalUniqueUsersIn7d;
|
||||
result.returning = result.returning7d;
|
||||
break;
|
||||
|
||||
case 'P30D':
|
||||
result.total = result.totalUniqueUsersIn30d;
|
||||
result.returning = result.returning30d;
|
||||
break;
|
||||
|
||||
default:
|
||||
result.total = 0;
|
||||
result.returning = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
result[prefix + 'values'] = [
|
||||
{
|
||||
timespan: '24 hours',
|
||||
retention: Math.round(100 * result.returning24hr / result.totalUniqueUsersIn24hr || 0) + '%',
|
||||
returning: result.returning24hr,
|
||||
unique: result.totalUniqueUsersIn24hr
|
||||
},
|
||||
{
|
||||
timespan: '7 days',
|
||||
retention: Math.round(100 * result.returning7d / result.totalUniqueUsersIn7d || 0) + '%',
|
||||
returning: result.returning7d,
|
||||
unique: result.totalUniqueUsersIn7d
|
||||
},
|
||||
{
|
||||
timespan: '30 days',
|
||||
retention: Math.round(100 * result.returning30d / result.totalUniqueUsersIn30d || 0) + '%',
|
||||
returning: result.returning30d,
|
||||
unique: result.totalUniqueUsersIn30d
|
||||
},
|
||||
];
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Formats a result to suite a scorecard
|
||||
*
|
||||
* Receives a list of one value:
|
||||
* values: [{ count: 99 }]
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-value": 99,
|
||||
* "prefix-heading": "Heading",
|
||||
* "prefix-color": "#fff",
|
||||
* "prefix-icon": "chat",
|
||||
* "prefix-subvalue": 44,
|
||||
* "prefix-subheading": "Subheading"
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format 'scorecard' | {
|
||||
* type: 'scorecard',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* countField: 'count' - Field name with count value (default: 'count')
|
||||
* postfix: '%' - String to add after the value (default: null)
|
||||
* thresholds: [{ value: 0, heading: '', color: '#000', icon: 'done' }]
|
||||
* subvalueField: 'other_count' - Other number field to check
|
||||
* subvalueThresholds: [{ subvalue: 0, subheading: '' }]
|
||||
* }
|
||||
* }
|
||||
* @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 scorecard (
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
let { values } = state;
|
||||
|
||||
const args = typeof format !== 'string' && format.args || { thresholds: null };
|
||||
const countField = args.countField || 'count';
|
||||
const postfix = args.postfix || null;
|
||||
let checkValue = (values && values[0] && values[0][countField]) || 0;
|
||||
|
||||
let createValue = (value: any, heading: string, color: string, icon: string, subvalue?: any, subheading?: string) => {
|
||||
let item = {};
|
||||
const prefix = getPrefix(format);
|
||||
item[prefix + 'value'] = utils.kmNumber(value, postfix);
|
||||
item[prefix + 'heading'] = heading;
|
||||
item[prefix + 'color'] = color;
|
||||
item[prefix + 'icon'] = icon;
|
||||
item[prefix + 'subvalue'] = subvalue || '';
|
||||
item[prefix + 'subheading'] = subheading || '';
|
||||
return item;
|
||||
};
|
||||
|
||||
let thresholds = args.thresholds || [ ];
|
||||
if (!thresholds.length) {
|
||||
thresholds.push({ value: checkValue, heading: '', color: '#000', icon: 'done' });
|
||||
}
|
||||
let firstThreshold = thresholds[0];
|
||||
|
||||
if (!values || !values.length) {
|
||||
return createValue(
|
||||
firstThreshold.value,
|
||||
firstThreshold.heading,
|
||||
firstThreshold.color,
|
||||
firstThreshold.icon
|
||||
);
|
||||
}
|
||||
|
||||
// Todo: check validity of thresholds and each value
|
||||
|
||||
let thresholdIdx = 0;
|
||||
let threshold = thresholds[thresholdIdx];
|
||||
|
||||
while (thresholds.length > (thresholdIdx + 1) &&
|
||||
checkValue > threshold.value &&
|
||||
checkValue >= thresholds[++thresholdIdx].value) {
|
||||
threshold = thresholds[thresholdIdx];
|
||||
}
|
||||
|
||||
let subvalue = null;
|
||||
let subheading = null;
|
||||
if (args.subvalueField || args.subvalueThresholds) {
|
||||
let subvalueField = args.subvalueField || null;
|
||||
let subvalueThresholds = args.subvalueThresholds || [];
|
||||
|
||||
if (!subvalueThresholds.length) { subvalueThresholds.push({ subvalue: 0, subheading: '' }); }
|
||||
|
||||
checkValue = values[0][subvalueField || countField] || 0;
|
||||
thresholdIdx = 0;
|
||||
let subvalueThreshold = subvalueThresholds[thresholdIdx];
|
||||
|
||||
while (subvalueThresholds.length > (thresholdIdx + 1) &&
|
||||
checkValue > subvalueThreshold.value &&
|
||||
checkValue >= subvalueThresholds[++thresholdIdx].value) {
|
||||
subvalueThreshold = subvalueThresholds[thresholdIdx];
|
||||
}
|
||||
|
||||
subvalue = checkValue;
|
||||
subheading = subvalueThreshold.subheading;
|
||||
}
|
||||
|
||||
return createValue(checkValue, threshold.heading, threshold.color, threshold.icon, subvalue, subheading);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Formats a result to suite a timeline (time series) chart
|
||||
*
|
||||
* Receives a list of filtering values:
|
||||
* values: [
|
||||
* { field: 'value 1' },
|
||||
* { field: 'value 2' },
|
||||
* { field: 'value 3' },
|
||||
* ]
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-filters": [ 'value 1', 'value 2', 'value 3' ],
|
||||
* "prefix-selected": [ ],
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format {
|
||||
* type: 'filter',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* timeField: 'timestamp' - The field containing timestamp
|
||||
* lineField: 'channel' - A field to hold/group by different lines in the graph
|
||||
* valueField: 'count' - holds the value/y value of the current point
|
||||
* }
|
||||
* }
|
||||
* @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 timeline(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
if (typeof format === 'string') {
|
||||
return formatWarn('format should be an object with args', 'timeline', plugin);
|
||||
}
|
||||
|
||||
const timeline = state.values;
|
||||
const { timespan } = dependencies;
|
||||
const args = format.args || {};
|
||||
const { timeField, lineField, valueField } = args;
|
||||
const prefix = getPrefix(format);
|
||||
|
||||
let _timeline = {};
|
||||
let _lines = {};
|
||||
|
||||
timeline.forEach(row => {
|
||||
let timestamp = row[timeField];
|
||||
let lineFieldValue = row[lineField];
|
||||
let valueFieldValue = row[valueField];
|
||||
|
||||
let timeValue = (new Date(timestamp)).getTime();
|
||||
|
||||
if (!_timeline[timeValue]) {
|
||||
_timeline[timeValue] = { time: (new Date(timestamp)).toUTCString() };
|
||||
}
|
||||
|
||||
if (!_lines[lineFieldValue]) {
|
||||
_lines[lineFieldValue] = { name: lineFieldValue, value: 0 };
|
||||
}
|
||||
|
||||
_timeline[timeValue][lineFieldValue] = valueFieldValue;
|
||||
_lines[lineFieldValue].value += valueFieldValue;
|
||||
});
|
||||
|
||||
let lines = Object.keys(_lines);
|
||||
let usage = _.values(_lines);
|
||||
let timelineValues = _.map(_timeline, value => {
|
||||
lines.forEach(line => {
|
||||
if (!value[line]) { value[line] = 0; }
|
||||
});
|
||||
return value;
|
||||
});
|
||||
|
||||
let result = {};
|
||||
result[prefix + 'graphData'] = timelineValues;
|
||||
result[prefix + 'timeFormat'] = (timespan === '24 hours' ? 'hour' : 'date');
|
||||
result[prefix + 'lines'] = lines;
|
||||
result[prefix + 'pieData'] = usage;
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import * as _ from 'lodash';
|
||||
import utils from '../../index';
|
||||
import { DataFormatTypes, IDataFormat, formatWarn, getPrefix } from '../common';
|
||||
import { IDataSourcePlugin } from '../../../data-sources/plugins/DataSourcePlugin';
|
||||
|
||||
/**
|
||||
* Receives a timespan data source and formats is accordingly
|
||||
*
|
||||
* Receives a list of filtering values (on the data source params variable):
|
||||
* params: {
|
||||
* values: ["24 hours","1 week","1 month","3 months"]
|
||||
* selectedValue: "1 month",
|
||||
* queryTimespan: "PT24H",
|
||||
* granularity: "5m"
|
||||
* }
|
||||
*
|
||||
* And outputs the result in a consumable filter way:
|
||||
* result: {
|
||||
* "prefix-values": ["24 hours","1 week","1 month","3 months"]
|
||||
* "prefix-selected": "1 month",
|
||||
* }
|
||||
*
|
||||
* "prefix-selected" will be able to hold the selected values from the filter component
|
||||
*
|
||||
* @param format 'timespan' | {
|
||||
* type: 'timespan',
|
||||
* args: {
|
||||
* prefix: string - a prefix string for the exported variables (default to id).
|
||||
* }
|
||||
* }
|
||||
* @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 timespan(
|
||||
format: string | IDataFormat,
|
||||
state: any,
|
||||
dependencies: IDictionary,
|
||||
plugin: IDataSourcePlugin,
|
||||
prevState: any) {
|
||||
|
||||
if (!state) { return null; }
|
||||
|
||||
const params = plugin.getParams();
|
||||
const prefix = getPrefix(format);
|
||||
let queryTimespan =
|
||||
state.selectedValue === '24 hours' ? 'PT24H' :
|
||||
state.selectedValue === '1 week' ? 'P7D' :
|
||||
state.selectedValue === '1 month' ? 'P30D' :
|
||||
'P90D';
|
||||
|
||||
let granularity =
|
||||
state.selectedValue === '24 hours' ? '5m' :
|
||||
state.selectedValue === '1 week' ? '1d' : '1d';
|
||||
|
||||
let result = {
|
||||
queryTimespan,
|
||||
granularity
|
||||
};
|
||||
result[prefix + 'values-all'] = params.values;
|
||||
result[prefix + 'values-selected'] = state.selectedValue;
|
||||
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export * from './common';
|
||||
export * from './formats/bars';
|
||||
export * from './formats/filter';
|
||||
export * from './formats/flags';
|
||||
export * from './formats/retention';
|
||||
export * from './formats/scorecard';
|
||||
export * from './formats/timeline';
|
||||
export * from './formats/timespan';
|
|
@ -1,16 +1,18 @@
|
|||
import * as moment from 'moment';
|
||||
|
||||
export default class Utils {
|
||||
static kmNumber(num: number): string {
|
||||
if (isNaN(num)) { return ''; }
|
||||
static kmNumber(num: any, postfix?: string): string {
|
||||
if (isNaN(num)) { return num + (postfix || ''); }
|
||||
|
||||
let value = parseFloat(num);
|
||||
|
||||
return (
|
||||
num > 999999 ?
|
||||
(num / 1000000).toFixed(1) + 'M' :
|
||||
num > 999 ?
|
||||
(num / 1000).toFixed(1) + 'K' :
|
||||
(num % 1 * 10) !== 0 ?
|
||||
num.toFixed(1).toString() : num.toString());
|
||||
value > 999999 ?
|
||||
(value / 1000000).toFixed(1) + 'M' :
|
||||
value > 999 ?
|
||||
(value / 1000).toFixed(1) + 'K' :
|
||||
(value % 1 * 10) !== 0 ?
|
||||
value.toFixed(1).toString() : value.toString()) + (postfix || '');
|
||||
}
|
||||
|
||||
static ago(date: Date): string {
|
||||
|
@ -52,7 +54,7 @@ export default class Utils {
|
|||
objectValues.push(mapping);
|
||||
});
|
||||
|
||||
if (valuesStringLength <= 120) {
|
||||
if (valuesStringLength + sind.length <= 100) {
|
||||
result += `{ ${objectValues.join()} }`;
|
||||
} else {
|
||||
result += `{\n${sind}\t${objectValues.join(',\n' + sind + '\t')}\n${sind}}`;
|
||||
|
@ -91,7 +93,7 @@ export default class Utils {
|
|||
return res;
|
||||
});
|
||||
|
||||
if (arrayStringLength <= 120) {
|
||||
if (arrayStringLength + sind.length <= 100) {
|
||||
result += `[${mappedValues.join()}]`;
|
||||
} else {
|
||||
result += `[\n${sind}\t${mappedValues.join(',\n' + sind + '\t')}\n${sind}]`;
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -201,7 +201,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
};
|
||||
}
|
||||
},
|
||||
users_timeline: {
|
||||
timeline_users: {
|
||||
query: (dependencies) => {
|
||||
var { granularity } = dependencies;
|
||||
return `
|
||||
|
@ -246,10 +246,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
});
|
||||
|
||||
return {
|
||||
"timeline-users-graphData": timelineValues,
|
||||
"timeline-users-channelUsage": channelUsage,
|
||||
"timeline-users-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline-users-channels": channels
|
||||
"timeline_users-graphData": timelineValues,
|
||||
"timeline_users-channelUsage": channelUsage,
|
||||
"timeline_users-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline_users-channels": channels
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -323,7 +323,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
};
|
||||
}
|
||||
},
|
||||
sentiments: {
|
||||
sentiment: {
|
||||
query: () => `
|
||||
where name startswith 'MBFEvent.Sentiment' |
|
||||
extend score=customDimensions.score|
|
||||
|
@ -596,9 +596,9 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
size: { w: 5,h: 8 },
|
||||
dependencies: {
|
||||
visible: "modes:users",
|
||||
values: "ai:timeline-users-graphData",
|
||||
lines: "ai:timeline-users-channels",
|
||||
timeFormat: "ai:timeline-users-timeFormat"
|
||||
values: "ai:timeline_users-graphData",
|
||||
lines: "ai:timeline_users-channels",
|
||||
timeFormat: "ai:timeline_users-timeFormat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -616,7 +616,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
title: "Channel Usage (Users)",
|
||||
subtitle: "Total users sent per channel",
|
||||
size: { w: 3,h: 8 },
|
||||
dependencies: { visible: "modes:users",values: "ai:timeline-users-channelUsage" },
|
||||
dependencies: { visible: "modes:users",values: "ai:timeline_users-channelUsage" },
|
||||
props: { showLegend: true,compact: true,entityType: "Users" }
|
||||
},
|
||||
{
|
||||
|
@ -701,7 +701,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
params: ["title","intent","queryspan"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "intentsDialog-data",
|
||||
id: "intentsDialog",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: {
|
||||
intent: "dialog_intentsDialog:intent",
|
||||
|
@ -712,7 +712,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
params: {
|
||||
table: "customEvents",
|
||||
queries: {
|
||||
"intent-usage": {
|
||||
"intentTimeline": {
|
||||
query: ({ intent, granularity }) => `
|
||||
extend intent=(customDimensions.intent)
|
||||
| where timestamp > ago(30d) and intent =~ "${intent}"
|
||||
|
@ -737,13 +737,13 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
});
|
||||
|
||||
return {
|
||||
"timeline-graphData": _timeline,
|
||||
"timeline-values": ["value"],
|
||||
"timeline-timeFormat": (timespan === "24 hours" ? 'hour' : 'date')
|
||||
"intentTimeline-graphData": _timeline,
|
||||
"intentTimeline-values": ["value"],
|
||||
"intentTimeline-timeFormat": (timespan === "24 hours" ? 'hour' : 'date')
|
||||
};
|
||||
}
|
||||
},
|
||||
"entities-usage": {
|
||||
"entitiesUsage": {
|
||||
query: ({ intent }) => `
|
||||
extend conversation=tostring(customDimensions.conversationId),
|
||||
entityType=tostring(customDimensions.entityType),
|
||||
|
@ -766,12 +766,12 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
});
|
||||
|
||||
return {
|
||||
"entities-usage": _.values(barResults),
|
||||
"entities-usage-bars": entity_values
|
||||
"entitiesUsage": _.values(barResults),
|
||||
"entitiesUsage-bars": entity_values
|
||||
};
|
||||
}
|
||||
},
|
||||
"total-conversations": {
|
||||
"intentConversions": {
|
||||
query: ({ intent }) => `
|
||||
extend conversation=tostring(customDimensions.conversationId), intent=customDimensions.intent |
|
||||
where name=='MBFEvent.Intent' and intent =~ '${intent}' |
|
||||
|
@ -780,7 +780,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
`,
|
||||
calculated: (results) => {
|
||||
return {
|
||||
"total-conversations": (results && results.length && results[0].Count) || 0
|
||||
"intentConversions-totalConversations": (results && results.length && results[0].Count) || 0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -946,29 +946,29 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
title: "Entity count appearances in intent",
|
||||
subtitle: "Entity usage and count for the selected intent",
|
||||
size: { w: 6,h: 8 },
|
||||
dependencies: { values: "intentsDialog-data:entities-usage",bars: "intentsDialog-data:entities-usage-bars" },
|
||||
dependencies: { values: "intentsDialog:entitiesUsage",bars: "intentsDialog:entitiesUsage-bars" },
|
||||
props: { nameKey: "entityType" }
|
||||
},
|
||||
{
|
||||
id: "utterances",
|
||||
type: "Table",
|
||||
size: { w: 4,h: 8 },
|
||||
dependencies: { values: "intentsDialog-data:intent_utterances" },
|
||||
dependencies: { values: "intentsDialog:intent_utterances" },
|
||||
props: {
|
||||
cols: [{ header: "Top Utterances",width: "200px",field: "text" },{ header: "Count",field: "count_utterances",type: "number" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "intent-timeline",
|
||||
id: "intentTimeline",
|
||||
type: "Timeline",
|
||||
title: "Message Rate",
|
||||
subtitle: "How many messages were sent per timeframe",
|
||||
size: { w: 8,h: 8 },
|
||||
dependencies: {
|
||||
visible: "modes:messages",
|
||||
values: "intentsDialog-data:timeline-graphData",
|
||||
lines: "intentsDialog-data:timeline-values",
|
||||
timeFormat: "intentsDialog-data:timeline-timeFormat"
|
||||
values: "intentsDialog:intentTimeline-graphData",
|
||||
lines: "intentsDialog:intentTimeline-values",
|
||||
timeFormat: "intentsDialog:intentTimeline-timeFormat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -976,7 +976,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
type: "Scorecard",
|
||||
size: { w: 2,h: 8 },
|
||||
dependencies: {
|
||||
card_conversations_value: "intentsDialog-data:total-conversations",
|
||||
card_conversations_value: "intentsDialog:intentConversions-totalConversations",
|
||||
card_conversations_heading: "::Conversations",
|
||||
card_conversations_color: "::#2196F3",
|
||||
card_conversations_icon: "::chat",
|
||||
|
@ -1052,7 +1052,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
params: ["title","queryspan"],
|
||||
dataSources: [
|
||||
{
|
||||
id: "sentiment-conversations-data",
|
||||
id: "sentimentConversations",
|
||||
type: "ApplicationInsights/Query",
|
||||
dependencies: { queryTimespan: "dialog_sentimentConversations:queryspan" },
|
||||
params: {
|
||||
|
@ -1088,10 +1088,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
],
|
||||
elements: [
|
||||
{
|
||||
id: "top5positive",
|
||||
id: "sentimentConversationsTop5positive",
|
||||
type: "Table",
|
||||
size: { w: 5,h: 8 },
|
||||
dependencies: { values: "sentiment-conversations-data:top5Positive" },
|
||||
dependencies: { values: "sentimentConversations:top5Positive" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [
|
||||
|
@ -1110,10 +1110,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
}
|
||||
},
|
||||
{
|
||||
id: "top5negative",
|
||||
id: "sentimentConversationsTop5negative",
|
||||
type: "Table",
|
||||
size: { w: 5,h: 8 },
|
||||
dependencies: { values: "sentiment-conversations-data:top5Positive" },
|
||||
dependencies: { values: "sentimentConversations:top5Negative" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [
|
||||
|
@ -1275,7 +1275,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
],
|
||||
elements: [
|
||||
{
|
||||
id: "errors-list",
|
||||
id: "errors-selection",
|
||||
type: "SplitPanel",
|
||||
title: "Errors",
|
||||
size: { w: 12,h: 14 },
|
||||
|
|
|
@ -177,14 +177,14 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
return t;
|
||||
};
|
||||
return {
|
||||
'transcriptsAverageTimeWaiting-value': isFinite(avgTimeWaiting) ? timeFormat(avgTimeWaiting) : '-',
|
||||
'transcriptsLongestTimeWaiting-value': isFinite(avgTimeWaiting) ? timeFormat(maxTimeWaiting) : '-',
|
||||
'transcriptsShortestTimeWaiting-value': isFinite(avgTimeWaiting) ? timeFormat(minTimeWaiting) : '-',
|
||||
'transcriptsTimeWaiting-avg': isFinite(avgTimeWaiting) ? timeFormat(avgTimeWaiting) : '-',
|
||||
'transcriptsTimeWaiting-longest': isFinite(avgTimeWaiting) ? timeFormat(maxTimeWaiting) : '-',
|
||||
'transcriptsTimeWaiting-shortest': isFinite(avgTimeWaiting) ? timeFormat(minTimeWaiting) : '-',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
transcriptsTimeline: {
|
||||
timeline: {
|
||||
query: (dependencies) => {
|
||||
var { granularity } = dependencies;
|
||||
return `where name == 'Transcript'
|
||||
|
@ -240,9 +240,9 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
'timeline-graphData': graphData,
|
||||
'timeline-recipients': keys,
|
||||
'timeline-timeFormat': (timespan === '24 hours' ? 'hour' : 'date'),
|
||||
'transcriptsBot-value': totalBot,
|
||||
'transcriptsAgent-value': totalAgent,
|
||||
'transcriptsTotal-value': totalMessages,
|
||||
'timeline-bot': totalBot,
|
||||
'timeline-agent': totalAgent,
|
||||
'timeline-total': totalMessages,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -266,10 +266,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
const waiting = customerTranscripts.filter((customer) => customer.state === 1);
|
||||
const agent = customerTranscripts.filter((customer) => customer.state === 2);
|
||||
return {
|
||||
'customerTotal-value': customerTranscripts.length,
|
||||
'customerBot-value': bot.length,
|
||||
'customerWaiting-value': waiting.length,
|
||||
'customerAgent-value': agent.length,
|
||||
'customerTranscripts-total': customerTranscripts.length,
|
||||
'customerTranscripts-bot': bot.length,
|
||||
'customerTranscripts-waiting': waiting.length,
|
||||
'customerTranscripts-agent': agent.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -280,29 +280,29 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
],
|
||||
elements: [
|
||||
{
|
||||
id: 'customerTotal',
|
||||
id: 'customerTranscripts',
|
||||
type: 'Scorecard',
|
||||
title: 'Users',
|
||||
size: { w: 6, h: 3 },
|
||||
dependencies: {
|
||||
card_total_heading: '::Total Users',
|
||||
card_total_tooltip: "::Total users",
|
||||
card_total_value: 'ai:customerTotal-value',
|
||||
card_total_value: 'ai:customerTranscripts-total',
|
||||
card_total_color: '::#666666',
|
||||
card_total_icon: '::account_circle',
|
||||
card_bot_heading: '::Bot',
|
||||
card_bot_tooltip: "::Total users talking to the bot",
|
||||
card_bot_value: 'ai:customerBot-value',
|
||||
card_bot_value: 'ai:customerTranscripts-bot',
|
||||
card_bot_color: '::#00FF00',
|
||||
card_bot_icon: '::memory',
|
||||
card_agent_heading: '::Agent',
|
||||
card_agent_tooltip: "::Total users talking to a human agent",
|
||||
card_agent_value: 'ai:customerAgent-value',
|
||||
card_agent_value: 'ai:customerTranscripts-agent',
|
||||
card_agent_color: '::#0066FF',
|
||||
card_agent_icon: '::perm_identity',
|
||||
card_waiting_heading: '::Waiting',
|
||||
card_waiting_tooltip: "::Total users waiting for a human agent to respond",
|
||||
card_waiting_value: 'ai:customerWaiting-value',
|
||||
card_waiting_value: 'ai:customerTranscripts-waiting',
|
||||
card_waiting_color: '::#FF6600',
|
||||
card_waiting_icon: '::more_horiz',
|
||||
}
|
||||
|
@ -316,48 +316,48 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
dependencies: {
|
||||
card_average_heading: '::Average',
|
||||
card_average_tooltip: "::Average time for human agent to respond",
|
||||
card_average_value: 'ai:transcriptsAverageTimeWaiting-value',
|
||||
card_average_value: 'ai:transcriptsTimeWaiting-avg',
|
||||
card_average_color: '::#333333',
|
||||
card_average_icon: '::av_timer',
|
||||
card_max_heading: '::Slowest',
|
||||
card_max_tooltip: "::Slowest time for human agent to respond",
|
||||
card_max_value: 'ai:transcriptsLongestTimeWaiting-value',
|
||||
card_max_value: 'ai:transcriptsTimeWaiting-longest',
|
||||
card_max_color: '::#ff0000',
|
||||
card_max_icon: '::timer',
|
||||
card_min_heading: '::Fastest',
|
||||
card_min_tooltip: "::Fastest time for human agent to respond",
|
||||
card_min_value: 'ai:transcriptsShortestTimeWaiting-value',
|
||||
card_min_value: 'ai:transcriptsTimeWaiting-shortest',
|
||||
card_min_color: '::#0066ff',
|
||||
card_min_icon: '::timer',
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'transcriptsTotal',
|
||||
id: 'timelineScores',
|
||||
type: 'Scorecard',
|
||||
title: 'Transcripts',
|
||||
size: { w: 2, h: 8 },
|
||||
dependencies: {
|
||||
card_total_heading: '::Total Msgs',
|
||||
card_total_tooltip: "::Total messages",
|
||||
card_total_value: 'ai:transcriptsTotal-value',
|
||||
card_total_value: 'ai:timeline-total',
|
||||
card_total_color: '::#666666',
|
||||
card_total_icon: '::question_answer',
|
||||
card_bot_heading: '::Bot',
|
||||
card_bot_tooltip: "::Total messages with bot",
|
||||
card_bot_value: 'ai:transcriptsBot-value',
|
||||
card_bot_value: 'ai:timeline-bot',
|
||||
card_bot_color: '::#00FF00',
|
||||
card_bot_icon: '::memory',
|
||||
card_agent_heading: '::Agent',
|
||||
card_agent_tooltip: "::Total messages with a human",
|
||||
card_agent_value: 'ai:transcriptsAgent-value',
|
||||
card_agent_value: 'ai:timeline-agent',
|
||||
card_agent_color: '::#0066FF',
|
||||
card_agent_icon: '::perm_identity'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
id: 'timelineHandoffConversations',
|
||||
id: 'timeline',
|
||||
type: 'Area',
|
||||
title: 'Conversations with bot / human',
|
||||
subtitle: 'How many conversations required hand-off to human',
|
||||
|
@ -374,7 +374,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
},
|
||||
|
||||
{
|
||||
id: 'conversations',
|
||||
id: 'transcripts',
|
||||
type: 'Table',
|
||||
title: 'Recent Conversations',
|
||||
subtitle: 'Monitor bot communications',
|
||||
|
@ -406,7 +406,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
params: ['title', 'conversationId', 'queryspan'],
|
||||
dataSources: [
|
||||
{
|
||||
id: 'transcripts-data',
|
||||
id: 'transcriptsData',
|
||||
type: 'ApplicationInsights/Query',
|
||||
dependencies: {
|
||||
username: 'dialog_transcriptsDialog:title',
|
||||
|
@ -415,52 +415,45 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
secret: 'connection:bot-framework.directLine'
|
||||
},
|
||||
params: {
|
||||
table: 'customEvents',
|
||||
queries: {
|
||||
'userConversationTranscripts':
|
||||
{
|
||||
query: ({ conversationId }) => {
|
||||
return `where name == 'Transcript'
|
||||
| where customDimensions.customerConversationId == '${conversationId}'
|
||||
| extend timestamp=tostring(customDimensions.timestamp)
|
||||
| project timestamp,
|
||||
text=tostring(customDimensions.text),
|
||||
sentimentScore=todouble(customDimensions.sentimentScore),
|
||||
from=tostring(customDimensions.from),
|
||||
state=toint(customDimensions.state)
|
||||
| order by timestamp asc`; },
|
||||
calculated: (transcripts, dependencies) => {
|
||||
if (!transcripts || transcripts.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { secret } = dependencies;
|
||||
const { conversationId } = dependencies;
|
||||
|
||||
let values = transcripts || [];
|
||||
let body, headers = {};
|
||||
let disabled = transcripts[transcripts.length - 1].state !== 0 ? true : false;
|
||||
|
||||
values.map(v => {
|
||||
const lastSentimentScore = v.sentimentScore || 0.5;
|
||||
v['sentiment'] = lastSentimentScore < 0 ? 'error_outline' :
|
||||
lastSentimentScore < 0.2 ? 'sentiment_very_dissatisfied' :
|
||||
lastSentimentScore < 0.4 ? 'sentiment_dissatisfied' :
|
||||
lastSentimentScore < 0.6 ? 'sentiment_neutral' :
|
||||
lastSentimentScore < 0.8 ? 'sentiment_satisfied' : 'sentiment_very_satisfied';
|
||||
});
|
||||
|
||||
body = {
|
||||
'conversationId': conversationId,
|
||||
};
|
||||
headers = {
|
||||
'Authorization': `Bearer ${secret}`
|
||||
};
|
||||
return { 'values': values, 'headers': headers, 'body': body, 'disabled': disabled };
|
||||
}
|
||||
|
||||
}
|
||||
query: ({ conversationId }) => `customEvents
|
||||
| where name == 'Transcript'
|
||||
| where customDimensions.customerConversationId == '${conversationId}'
|
||||
| extend timestamp=tostring(customDimensions.timestamp)
|
||||
| project timestamp,
|
||||
text=tostring(customDimensions.text),
|
||||
sentimentScore=todouble(customDimensions.sentimentScore),
|
||||
from=tostring(customDimensions.from),
|
||||
state=toint(customDimensions.state)
|
||||
| order by timestamp asc`
|
||||
},
|
||||
calculated: (state, dependencies) => {
|
||||
let { values } = state || [];
|
||||
if (!values || values.length < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { secret } = dependencies;
|
||||
const { conversationId } = dependencies;
|
||||
|
||||
let body, headers = {};
|
||||
let disabled = values[values.length - 1].state !== 0 ? true : false;
|
||||
|
||||
values.map(v => {
|
||||
const lastSentimentScore = v.sentimentScore || 0.5;
|
||||
v['sentiment'] = lastSentimentScore < 0 ? 'error_outline' :
|
||||
lastSentimentScore < 0.2 ? 'sentiment_very_dissatisfied' :
|
||||
lastSentimentScore < 0.4 ? 'sentiment_dissatisfied' :
|
||||
lastSentimentScore < 0.6 ? 'sentiment_neutral' :
|
||||
lastSentimentScore < 0.8 ? 'sentiment_satisfied' : 'sentiment_very_satisfied';
|
||||
});
|
||||
|
||||
body = {
|
||||
'conversationId': conversationId,
|
||||
};
|
||||
headers = {
|
||||
'Authorization': `Bearer ${secret}`
|
||||
};
|
||||
return { values, headers, body, disabled };
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -472,9 +465,9 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
size: { w: 2, h: 1 },
|
||||
location: { x: 0, y: 0 },
|
||||
dependencies: {
|
||||
body: 'transcripts-data:body',
|
||||
headers: 'transcripts-data:headers',
|
||||
disabled: 'transcripts-data:disabled',
|
||||
body: 'transcriptsData:body',
|
||||
headers: 'transcriptsData:headers',
|
||||
disabled: 'transcriptsData:disabled',
|
||||
conversationsEndpoint: 'connection:bot-framework.conversationsEndpoint'
|
||||
},
|
||||
props: {
|
||||
|
@ -494,7 +487,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
dependencies: {
|
||||
token: 'connection:bot-framework.directLine',
|
||||
webchatEndpoint: 'connection:bot-framework.webchatEndpoint',
|
||||
dependsOn: 'transcripts-data:disabled'
|
||||
dependsOn: 'transcriptsData:disabled'
|
||||
},
|
||||
props: {
|
||||
url: ({ token, webchatEndpoint }) => `${webchatEndpoint}/?s=${token}`,
|
||||
|
@ -504,12 +497,12 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
}
|
||||
},
|
||||
{
|
||||
id: 'transcripts-list',
|
||||
id: 'transcriptsData',
|
||||
type: 'Table',
|
||||
title: 'Transcripts',
|
||||
size: { w: 12, h: 11 },
|
||||
location: { x: 0, y: 1 },
|
||||
dependencies: { values: 'transcripts-data:values' },
|
||||
dependencies: { values: 'transcriptsData:values' },
|
||||
props: {
|
||||
rowClassNameField: 'from',
|
||||
cols: [
|
||||
|
|
|
@ -86,10 +86,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
return null;
|
||||
}
|
||||
return {
|
||||
'avg-score-value': avgscore[0].avg + '%',
|
||||
'avg-score-color': avgscore[0].avg >= 80 ? '#4caf50' :
|
||||
'avgScore-value': avgscore[0].avg + '%',
|
||||
'avgScore-color': avgscore[0].avg >= 80 ? '#4caf50' :
|
||||
(avgscore[0].avg > 60 ? '#FFc107' : '#F44336'),
|
||||
'avg-score-icon': avgscore[0].avg >= 80 ? 'sentiment_very_satisfied' :
|
||||
'avgScore-icon': avgscore[0].avg >= 80 ? 'sentiment_very_satisfied' :
|
||||
(avgscore[0].avg > 60 ?
|
||||
'sentiment_satisfied' :
|
||||
'sentiment_dissatisfied')
|
||||
|
@ -100,9 +100,9 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
query: () => `
|
||||
where name == 'MBFEvent.QNAEvent'
|
||||
| summarize hits=count() `,
|
||||
calculated: hits =>({ 'score-hits': hits[0].hits })
|
||||
calculated: hits =>({ 'totalHits-value': hits[0].hits })
|
||||
},
|
||||
scoreHits: {
|
||||
'timeline_hits': {
|
||||
query: () => `
|
||||
where name == 'MBFEvent.QNAEvent'
|
||||
| summarize hits=count() by bin(timestamp,1d)
|
||||
|
@ -144,14 +144,14 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
});
|
||||
|
||||
return {
|
||||
"timeline-hits-graphData": timelineValues,
|
||||
"timeline-hits-channelUsage": channelUsage,
|
||||
"timeline-hits-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline-hits-channels": channels
|
||||
"timeline_hits-graphData": timelineValues,
|
||||
"timeline_hits-channelUsage": channelUsage,
|
||||
"timeline_hits-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline_hits-channels": channels
|
||||
};
|
||||
}
|
||||
},
|
||||
users_timeline: {
|
||||
'timeline_users': {
|
||||
query: ({ granularity }) => `
|
||||
where name == 'MBFEvent.QNAEvent'
|
||||
| extend userName=tostring(customDimensions.userName)
|
||||
|
@ -194,10 +194,10 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
});
|
||||
|
||||
return {
|
||||
"timeline-users-graphData": timelineValues,
|
||||
"timeline-users-channelUsage": channelUsage,
|
||||
"timeline-users-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline-users-channels": channels
|
||||
"timeline_users-graphData": timelineValues,
|
||||
"timeline_users-channelUsage": channelUsage,
|
||||
"timeline_users-timeFormat": (timespan === "24 hours" ? 'hour' : 'date'),
|
||||
"timeline_users-channels": channels
|
||||
};
|
||||
}
|
||||
},
|
||||
|
@ -235,7 +235,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
title: "Hit Rate",
|
||||
subtitle: "How many questions were asked per timeframe",
|
||||
size: { w: 5,h: 8 },
|
||||
dependencies: { values: "ai:timeline-hits-graphData",lines: "ai:timeline-hits-channels",timeFormat: "ai:timeline-hits-timeFormat" }
|
||||
dependencies: { values: "ai:timeline_hits-graphData",lines: "ai:timeline_hits-channels",timeFormat: "ai:timeline_hits-timeFormat" }
|
||||
},
|
||||
{
|
||||
id: "channels",
|
||||
|
@ -243,22 +243,22 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
title: "Channel Usage (Users)",
|
||||
subtitle: "Total users sent per channel",
|
||||
size: { w: 5,h: 8 },
|
||||
dependencies: { values: "ai:timeline-users-channelUsage" },
|
||||
dependencies: { values: "ai:timeline_users-channelUsage" },
|
||||
props: { showLegend: true, entityType: 'messages' }
|
||||
},
|
||||
{
|
||||
id: "scorecardAvgScore",
|
||||
type: "Scorecard",
|
||||
title: "Avg Score",
|
||||
size: { w: 2,h: 3 },
|
||||
size: { w: 2,h: 8 },
|
||||
dependencies: {
|
||||
card_avgscore_value: "ai:avg-score-value",
|
||||
card_avgscore_color: "ai:avg-score-color",
|
||||
card_avgscore_icon: "ai:avg-score-icon",
|
||||
card_avgscore_value: "ai:avgScore-value",
|
||||
card_avgscore_color: "ai:avgScore-color",
|
||||
card_avgscore_icon: "ai:avgScore-icon",
|
||||
card_avgscore_heading: "::Avg Score",
|
||||
card_avgscore_onClick: "::onScoreClick",
|
||||
|
||||
card_totalhits_value: "ai:score-hits",
|
||||
card_totalhits_value: "ai:totalHits-value",
|
||||
card_totalhits_color: "::#2196F3",
|
||||
card_totalhits_icon: "::av_timer",
|
||||
card_totalhits_heading: "::Total hits"
|
||||
|
@ -343,7 +343,7 @@ export const config: IDashboardConfig = /*return*/ {
|
|||
id: "top5negative",
|
||||
type: "Table",
|
||||
size: { w: 5,h: 8 },
|
||||
dependencies: { values: "sentiment-conversations-data:top5Positive" },
|
||||
dependencies: { values: "sentiment-conversations-data:top5Negative" },
|
||||
props: {
|
||||
compact: true,
|
||||
cols: [
|
||||
|
|
Загрузка…
Ссылка в новой задаче