Commit ddc84cfb authored by Viacheslav Pavlov's avatar Viacheslav Pavlov
Browse files

Update KPI runs diff display

- Replaced name input with select
- fixed some styles
- Added groupBy to selection
parent 4c622873
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="black">
<path d="M24 20.188l-8.315-8.209 8.2-8.282-3.697-3.697-8.212 8.318-8.31-8.203-3.666 3.666 8.321 8.24-8.206 8.313 3.666 3.666 8.237-8.318 8.285 8.203z"/>
</svg>
\ No newline at end of file
......@@ -1567,7 +1567,8 @@
"values": "Values"
},
"changesSection": {
"noData": "Your data will be displayed here"
"noData": "Your data will be displayed here",
"applying": "Applying search"
},
"changesSearchForm": {
"type": "Search type",
......@@ -1577,7 +1578,14 @@
"fromDateToDate": "From date to date",
"days": "Days",
"toDate": "To date",
"fromDate": "From date"
"fromDate": "From date",
"key": "key",
"filters": "Filters"
},
"filter": {
"name": "Field full name",
"value": "Add value",
"info": "Add values by one. To delete one of values click on it."
}
},
"execution": {
......@@ -1633,6 +1641,9 @@
"condition": "Condition",
"description": "Description",
"entity": "Entity"
},
"observationsTable": {
"total": "Total"
}
},
"dimensionForm": {
......
......@@ -199,8 +199,8 @@ const receiveExecutionChanges = (changes) => ({
payload: {changes},
});
export const loadExecutionChanges = (name, {days = null, to = null, from = null}: {days: number, to?: string, from?: string}) => (dispatch) => {
return KpiService.executionRunDiffs(name, days, from, to)
export const loadExecutionChanges = (name, {days = null, to = null, from = null}: {days: number, to?: string, from?: string}, keys) => (dispatch) => {
return KpiService.executionRunDiffs(name, days, from, to, keys)
.then((changes) => dispatch(receiveExecutionChanges(changes)));
};
export const toggleParameterModal = (open: boolean): IReducerAction => ({
......
......@@ -9,7 +9,8 @@
"values": "Values"
},
"changesSection": {
"noData": "Your data will be displayed here"
"noData": "Your data will be displayed here",
"applying": "Applying search"
},
"changesSearchForm": {
"type": "Search type",
......@@ -19,7 +20,14 @@
"fromDateToDate": "From date to date",
"days": "Days",
"toDate": "To date",
"fromDate": "From date"
"fromDate": "From date",
"key": "key",
"filters": "Filters"
},
"filter": {
"name": "Field full name",
"value": "Add value",
"info": "Add values by one. To delete one of values click on it."
}
},
"execution": {
......@@ -75,6 +83,9 @@
"condition": "Condition",
"description": "Description",
"entity": "Entity"
},
"observationsTable": {
"total": "Total"
}
},
"dimensionForm": {
......
......@@ -184,7 +184,9 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
<ChangesSection changes={ executionChanges }
showSnackbar={ showSnackbar }
loadExecutionChanges={ loadExecutionChanges }
executionName={ execution.name }/>
executionName={ execution.name }
availableDimensions={ [...execution.executionDimensions, ...execution.groups] }
/>
:
<RunsSection
executionDetails={ executionDetails }
......
import * as React from 'react';
import {translate} from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
import {FormControl, Grid, Select, MenuItem, TextField, Tooltip, FormLabel} from '@material-ui/core';
import {Info} from '@material-ui/icons';
const styles = (theme) => ({
removable: {
'&:hover': {
color: 'red',
cursor: 'url(images/ICON-REMOVE.svg), pointer',
fontWeight: 700,
},
},
code: {
padding: '2px 4px',
backgroundColor: '#fbe5e1',
color: '#c0341d',
borderRadius: '4px',
},
dimensionSelect: {
marginTop: '1px !important',
},
infoTooltip: {
fontSize: '16px',
},
});
class ChangesFilterField extends React.Component<any> {
public state = {
name: '',
tmpValue: '',
values: [],
};
private handleValueChange = (e) => {
this.setState({tmpValue: e.target.value});
}
private maybeAdd = (text: string) => {
const values = [...this.state.values];
const name = this.state.name;
if (text && text.length > 0) {
if (values.indexOf(text) < 0) {
values.push(text);
}
this.setState({
tmpValue: '',
values,
});
}
const newValues = {};
newValues[name] = values;
return newValues;
}
private maybeRemove = (text: string) => {
const values = [...this.state.values];
const name = this.state.name;
if (text && text.length > 0) {
const index: number = values.indexOf(text);
if (index >= 0) {
values.splice(index, 1);
}
this.setState({
values,
});
}
const newValues = {};
newValues[name] = values;
return newValues;
}
private removeByIndex = (index) => {
const {input} = this.props;
const newValues = this.maybeRemove(this.state.values[index]);
input.onChange(newValues);
}
private handleKeyPres = (event) => {
const {input} = this.props;
const {tmpValue} = this.state;
if (event.key === 'Enter') {
if (tmpValue && tmpValue.length > 0) {
event.preventDefault();
const values = this.maybeAdd(tmpValue);
input.onChange(values);
this.setState({tmpValue: ''});
}
}
}
public render() {
const {values, name, tmpValue} = this.state;
const {input, dimensions, classes, t} = this.props;
return (
<div>
<div>
<code className={ classes.code }>
{ `${name}: [` }
{ values.map((value, index) => <span key={ index }>{ index !== 0 ? ', ' : '' }<span
onClick={ () => this.removeByIndex(index) } className={ classes.removable }>{ `"${value}"` }</span></span>) }
{ `]` }
</code>
<span className="float-right">
<Tooltip
title={ <span className={ classes.infoTooltip }>{ t('kpi.admin.c.changesSearchForm.filter.info') }</span> }
placement="top-start">
<Info/>
</Tooltip>
</span>
</div>
<Grid container>
<Grid item xs={ 12 } md={ 6 }>
<FormControl className="full-width">
<FormLabel>{ t(`kpi.admin.c.changesSearchForm.filter.name`) }</FormLabel>
<Select
className={ classes.dimensionSelect }
value={ name }
name={ `${input.name}.name` }
onChange={ (e) => this.setState({name: e.target.value}) }
>
{ dimensions && dimensions.map((dim) => (
<MenuItem key={ 0 } value={ `${dim.link ? `${dim.link}.` : ''}${dim.field}` }>
{ dim.alias || `${dim.link ? `${dim.link}.` : ''}.${dim.field}` }
</MenuItem>
)) }
</Select>
</FormControl>
</Grid>
<Grid item xs={ 12 } md={ 6 }>
<TextField
value={ tmpValue }
name={ `${input.name}.tempValue` }
label={ t(`kpi.admin.c.changesSearchForm.filter.value`) }
onKeyPress={ this.handleKeyPres }
onChange={ this.handleValueChange }
className="full-width"
/>
</Grid>
</Grid>
</div>
);
}
}
export default translate()(withStyles(styles)(ChangesFilterField));
......@@ -2,6 +2,7 @@ import * as React from 'react';
import { translate } from 'react-i18next';
import * as moment from 'moment';
import { Field, reduxForm } from 'redux-form';
import {withStyles} from '@material-ui/core';
// constants
import { CHANGES_SEARCH_FORM } from 'kpi/constants';
......@@ -10,9 +11,35 @@ import { CHANGES_SEARCH_FORM } from 'kpi/constants';
import Button from '@material-ui/core/Button';
import { TextField } from 'ui/common/text-field';
import RadioSelection from 'ui/common/forms/RadioSelection';
import ItemsEditor from 'ui/common/ItemsEditor';
import ChangesFilterField from './ChangesFilterField';
import Dimension from 'model/kpi/Dimension';
const styleSheet = (theme) => ({
/*tslint:disable*/
itemEditor: {
'& .items-editor-remove_button': {
flexBasis: '100%',
maxWidth: '100%',
left: 'calc(100% - 80px)',
position: 'relative' as 'relative',
},
},
});
interface IChangesSearchFormProps extends React.ClassAttributes<any> {
availableDimensions?: Array<Dimension<any>>;
handleSubmit: any;
submitting: boolean;
invalid: boolean;
t: any;
classes: any;
error: any;
}
class ParameterForm extends React.Component<any> {
class ChangesForm extends React.Component<IChangesSearchFormProps> {
public state = {
mode: 'daysToToday',
......@@ -22,8 +49,12 @@ class ParameterForm extends React.Component<any> {
super(props);
}
private renderFilterField = (key, index, fields, itemLabel) => (
<Field required name={ key } component={ ChangesFilterField } dimensions={ this.props.availableDimensions }/>
)
public render() {
const {t, error, handleSubmit, submitting, invalid} = this.props;
const {t, error, handleSubmit, submitting, invalid, classes} = this.props;
const {mode} = this.state;
return (
......@@ -64,6 +95,12 @@ class ParameterForm extends React.Component<any> {
shrink
/>
}
<div className={ `pb-20 pt-20 ${classes.itemEditor}` }>
<h4>{ t('kpi.admin.c.changesSearchForm.types.filters') }</h4>
<ItemsEditor name="keys" itemLabel={ t('kpi.admin.c.changesSearchForm.types.key') } addItem={ console.log } removeItem={ console.log } component={ this.renderFilterField }/>
</div>
</div>
{ error && <div style={ {color: 'red'} }>{ error }</div> }
......@@ -83,7 +120,7 @@ const validate = (values) => {
return {};
};
export default translate()(reduxForm({
export default translate()(withStyles(styleSheet)(reduxForm({
form: CHANGES_SEARCH_FORM,
initialValues: {
mode: 'daysToToday',
......@@ -92,4 +129,4 @@ export default translate()(reduxForm({
},
validate,
enableReinitialize: true,
})(ParameterForm));
})(ChangesForm)));
import * as React from 'react';
import {translate} from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
// model
import Dimension from 'model/kpi/Dimension';
// ui
import { Grid, Button } from '@material-ui/core';
import PrettyDate from 'ui/common/time/PrettyDate';
......@@ -31,7 +33,8 @@ const styles = (theme) => ({
interface IChangesSectionProps extends React.ClassAttributes<any> {
changes: any;
executionName: string;
loadExecutionChanges: (name, {days, to, from}: { days: number, to: string, from: string }) => void;
availableDimensions?: Array<Dimension<any>>;
loadExecutionChanges: (name, {days, to, from}: { days: number, to: string, from: string }, keys) => void;
classes: any;
t: any;
showSnackbar: (message: string) => void;
......@@ -41,7 +44,13 @@ interface IChangesSectionProps extends React.ClassAttributes<any> {
class ChangesSection extends React.Component<IChangesSectionProps> {
private loadExecutionChanges = (value) => {
const {executionName, loadExecutionChanges} = this.props;
const {executionName, loadExecutionChanges, showSnackbar} = this.props;
const keys = {};
if (value.keys && value.keys.length > 0) {
value.keys.map((key, index) => keys[Object.getOwnPropertyNames(key)[0]] = value.keys[index][Object.getOwnPropertyNames(key)[0]]);
}
if (value.days || value.to || value.from) {
const searchQuery = {
days: value.mode === 'daysToToday' || value.mode === 'daysToDate' ? value.days : null,
......@@ -49,7 +58,8 @@ class ChangesSection extends React.Component<IChangesSectionProps> {
from: value.mode === 'fromDateToDate' ? value.from : null,
};
loadExecutionChanges(executionName, searchQuery);
showSnackbar('kpi.admin.c.changesSection.applying');
loadExecutionChanges(executionName, searchQuery, keys);
}
}
......@@ -72,12 +82,12 @@ class ChangesSection extends React.Component<IChangesSectionProps> {
}
public render() {
const {changes, classes, t} = this.props;
const {changes, classes, availableDimensions, t} = this.props;
return (
<Grid container spacing={ 8 } justify="space-between">
<Grid xs={ 12 } md={ 3 } item>
<ChangesSearchForm onSubmit={ this.loadExecutionChanges }/>
<ChangesSearchForm onSubmit={ this.loadExecutionChanges } availableDimensions={ availableDimensions }/>
</Grid>
<Grid xs={ 12 } md={ 8 } item>
{ changes ?
......@@ -89,7 +99,7 @@ class ChangesSection extends React.Component<IChangesSectionProps> {
{ t('common:action.copyToClipboard') }&nbsp;&nbsp; <FileCopyIcon fontSize="small"/>
</Button>
</div>
<ObservationsTable id={ `changes-table-${index}` } executionType="COUNT" observations={ changes[date] } highlight/>
<ObservationsTable id={ `changes-table-${index}` } executionType="COUNT" observations={ changes[date] } highlight showSum/>
</div>
))
:
......
import * as React from 'react';
import {translate} from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
import Number from 'ui/common/Number';
......@@ -52,14 +53,20 @@ interface IObservationsTableProps extends React.ClassAttributes<any> {
observations: Observation[];
executionType: string;
highlight?: boolean;
showSum?: boolean;
classes: any;
id?: string;
t: any;
}
class ObservationsTable extends React.Component<IObservationsTableProps> {
public render() {
const {executionType, observations, classes, highlight, id} = this.props;
const {executionType, observations, classes, highlight, id, t, showSum} = this.props;
const total = observations && observations.length > 0 && observations.map((obs) => obs.value).reduce((acc, current) => acc + current);
return (
<table className={ classes.root } id={ id }>
{ observations && observations.length > 0 &&
......@@ -75,17 +82,33 @@ class ObservationsTable extends React.Component<IObservationsTableProps> {
</tr>
</thead>
}
{ observations && observations.length > 0 &&
<tbody className={ classes.body }>
{ observations && observations.length > 0 && observations.sort((a, b) => COMPARE_OBSERVATIONS(a, b)).map((obs, i) => (
<tr key={ `${i}-${obs.id}` }>
{ obs.dimensions.map((dim, i) => (
<td key={ `${i}-${dim.value}` }>{ dim.value }</td>
)) }
<td className={ `text-right ${ highlight && (obs.value >= 0 ? classes.highlightPositive : classes.highlightNegative) }` }><Number value={ obs.value }/></td>
{ obs.stdDev !== null && <td className="text-right"><Number value={ obs.stdDev }/></td> }
{ observations.sort((a, b) => COMPARE_OBSERVATIONS(a, b)).map((obs, i) => (
<tr key={ `${i}-${obs.id}` }>
{ obs.dimensions.map((dim, i) => (
<td key={ `${i}-${dim.value}` }>{ dim.value }</td>
)) }
<td
className={ `text-right ${highlight && (obs.value >= 0 ? classes.highlightPositive : classes.highlightNegative)}` }>
<Number value={ obs.value }/></td>
{ obs.stdDev !== null && <td className="text-right"><Number value={ obs.stdDev }/></td> }
</tr>
)) }
{ showSum &&
<tr key={ `total` }>
<td><b>{ t('kpi.admin.c.observationsTable.total') }</b></td>
<td
className={ `text-right ${highlight && total >= 0 ? classes.highlightPositive : classes.highlightNegative}` }
colSpan={ observations[0].dimensions.length }
>
<b>{ total }</b></td>
</tr>
)) }
}
</tbody>
}
</table>
);
}
......@@ -93,4 +116,4 @@ class ObservationsTable extends React.Component<IObservationsTableProps> {
}
export default withStyles(styles)(ObservationsTable);
export default translate()(withStyles(styles)(ObservationsTable));
......@@ -259,9 +259,10 @@ class KpiService {
* @param name name
* @param days days
* @param from from
* @param keys keys
* @param to to
*/
public static executionRunDiffs(name: string, days?: number, from?: string, to?: string): Promise<any> {
public static executionRunDiffs(name: string, days?: number, from?: string, to?: string, keys?: any): Promise<any> {
const qs = QueryString.stringify({
days: days || undefined,
......@@ -270,11 +271,11 @@ class KpiService {
}, {});
const apiUrl = URL_EXECUTION_RUN_DIFFS.expand({ name }) + (qs ? `?${qs}` : '');
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
const content = { data: keys };
return axiosBackend.request({
url: apiUrl,
method: 'GET',
method: 'POST',
...content,
}).then(({ data }) => data as any);
}
......
......@@ -37,7 +37,7 @@ const renderMembers = ({ fields, itemLabel, itemEditor, addItem, removeItem, hea
<Grid item xs={ 10 } md={ 11 }>
{ itemEditor(member, index, fields, itemLabel) }
</Grid>
<Grid item xs={ 2 } md={ 1 }>
<Grid item xs={ 2 } md={ 1 } className="items-editor-remove_button">
<Button type="button" onClick={ onRemoveMember(member, index) }>{ t('common:action.remove') }</Button>
</Grid>
</Grid>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment