Commit 80a8888f authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Matija Obreza

Execution run diff

KPI run diff updates

- Sort diffs by date (descending)
- Copy to clipboard boilerplate added
- Added missing `key={ ... }` to lists of elements

- Fixed changesItem header style
- Fixed copy from changesItem function
- Removed unnecessary form header
parent cd25a28e
......@@ -1558,6 +1558,20 @@
"type": "Type",
"values": "Values"
},
"changesSection": {
"noData": "Your data will be displayed here"
},
"changesSearchForm": {
"type": "Search type",
"types": {
"daysToToday": "Days before today",
"daysToDate": "Days before date",
"fromDateToDate": "From date to date",
"days": "Days",
"toDate": "To date",
"fromDate": "From date"
}
},
"execution": {
"type":{
"COUNT": "Count",
......
......@@ -4,13 +4,13 @@ import { showSnackbar } from 'actions/snackbar';
// constants
import {
ADMIN_REMOVE_EXEC, ADMIN_RECEIVE_PARAMS,
ADMIN_RECEIVE_PARAM, ADMIN_RECEIVE_DIMS,
ADMIN_RECEIVE_EXECS, ADMIN_RECEIVE_EXEC,
ADMIN_RECEIVE_EXEC_LASTRUN, ADMIN_RECEIVE_DIM,
ADMIN_RECEIVE_EXEC_RUNS, ADMIN_APPEND_EXEC_RUNS,
ADMIN_RECEIVE_EXEC_DET, ADMIN_REMOVE_DIM,
ADMIN_REMOVE_PARAM,
ADMIN_REMOVE_EXEC, ADMIN_RECEIVE_PARAMS,
ADMIN_RECEIVE_PARAM, ADMIN_RECEIVE_DIMS,
ADMIN_RECEIVE_EXECS, ADMIN_RECEIVE_EXEC,
ADMIN_RECEIVE_EXEC_LASTRUN, ADMIN_RECEIVE_DIM,
ADMIN_RECEIVE_EXEC_RUNS, ADMIN_APPEND_EXEC_RUNS,
ADMIN_RECEIVE_EXEC_DET, ADMIN_REMOVE_DIM,
ADMIN_REMOVE_PARAM, ADMIN_RECEIVE_EXEC_CHANGES,
} from 'kpi/constants';
// model
......@@ -184,3 +184,12 @@ const removeParameter = (parameter: KPIParameter): IReducerAction => ({
type: ADMIN_REMOVE_PARAM, payload: parameter,
});
const receiveExecutionChanges = (changes) => ({
type: ADMIN_RECEIVE_EXEC_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)
.then((changes) => dispatch(receiveExecutionChanges(changes)));
};
......@@ -8,6 +8,7 @@
// export const INSTITUTE_FILTERFORM = 'Form/institutes/INSTITUTE_FILTERFORM';
export const CHANGES_SEARCH_FORM = 'Form/kpi/CHANGES_SEARCH_FORM';
export const PARAMETER_FORM = 'Form/kpi/PARAMETER_FORM';
export const DIMENSION_FORM = 'Form/kpi/DIMENSION_FORM';
export const EXEC_FORM = 'Form/kpi/EXEC_FORM';
......@@ -18,7 +19,8 @@ export const ADMIN_REMOVE_EXEC = 'kpi/admin/REMOVE_EXEC';
export const ADMIN_RECEIVE_EXEC_DET = 'kpi/admin/RECEIVE_EXEC_DET';
export const ADMIN_RECEIVE_EXEC_LASTRUN = 'kpi/admin/RECEIVE_LASTRUN';
export const ADMIN_RECEIVE_EXEC_RUNS = 'kpi/admin/RECEIVE_EXEC_RUNS';
export const ADMIN_APPEND_EXEC_RUNS = 'kpi/admin/APPEND_EXEC_RUNS ';
export const ADMIN_APPEND_EXEC_RUNS = 'kpi/admin/APPEND_EXEC_RUNS';
export const ADMIN_RECEIVE_EXEC_CHANGES = 'kpi/admin/RECEIVE_EXEC_CHANGES';
export const ADMIN_RECEIVE_PARAMS = 'kpi/admin/RECEIVE_PARAMS';
export const ADMIN_RECEIVE_PARAM = 'kpi/admin/RECEIVE_PARAM';
......
......@@ -5,6 +5,7 @@ import {
ADMIN_RECEIVE_DIM,
ADMIN_RECEIVE_DIMS,
ADMIN_RECEIVE_EXEC,
ADMIN_RECEIVE_EXEC_CHANGES,
ADMIN_RECEIVE_EXEC_DET,
ADMIN_RECEIVE_EXEC_LASTRUN,
ADMIN_RECEIVE_EXEC_RUNS,
......@@ -24,6 +25,7 @@ const INITIAL_STATE: {
exec: {
page: Page<Execution>,
details: Execution,
changes: any,
},
dim: {
page: Page<Dimension<any>>,
......@@ -37,6 +39,7 @@ const INITIAL_STATE: {
exec: {
page: {},
details: new Execution(),
changes: null,
},
dim: {
page: null,
......@@ -81,6 +84,7 @@ export default function admin(state = INITIAL_STATE, action: IReducerAction) {
details: {
execution: {$set: execution},
},
changes: {$set: null},
},
});
} else {
......@@ -257,6 +261,15 @@ export default function admin(state = INITIAL_STATE, action: IReducerAction) {
},
});
}
case ADMIN_RECEIVE_EXEC_CHANGES: {
const {changes} = action.payload;
return update(state, {
exec: {
changes: {$set: changes},
},
});
}
default:
return state;
}
......
......@@ -18,7 +18,7 @@ const adminRoutes = [
exact: true,
},
{
path: '/kpi/:shortName',
path: '/kpi/:shortName/:tab?',
component: Loadable({
loader: () => import(/* webpackMode:"lazy", webpackChunkName: "kpi" */'kpi/ui/admin/ExecutionDisplay'),
}),
......
......@@ -8,6 +8,20 @@
"type": "Type",
"values": "Values"
},
"changesSection": {
"noData": "Your data will be displayed here"
},
"changesSearchForm": {
"type": "Search type",
"types": {
"daysToToday": "Days before today",
"daysToDate": "Days before date",
"fromDateToDate": "From date to date",
"days": "Days",
"toDate": "To date",
"fromDate": "From date"
}
},
"execution": {
"type":{
"COUNT": "Count",
......
......@@ -2,31 +2,34 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { translate } from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
// Actions
import { deleteExecution, executeExecution, getExecution, listDimensions, listParameters, loadMoreExecutionRuns } from 'kpi/actions/admin';
import {
deleteExecution,
executeExecution,
getExecution,
listDimensions,
listParameters,
loadExecutionChanges,
loadMoreExecutionRuns,
} from 'kpi/actions/admin';
import { showSnackbar } from 'actions/snackbar';
// Models
import ExecutionDetails from 'model/kpi/ExecutionDetails';
import ExecutionRun from 'model/kpi/ExecutionRun';
import Execution from 'model/kpi/Execution';
// Service
import KpiService from 'service/genesys/KpiService';
// UI
import { PageContents, PageSection } from 'ui/layout/PageLayout';
import ContentHeaderWithButton from 'ui/common/heading/ContentHeaderWithButton';
import Permissions from 'ui/common/permission/Permissions';
import PrettyDate from 'ui/common/time/PrettyDate';
import Loading from 'ui/common/Loading';
import { Properties, PropertiesItem } from 'ui/common/Properties';
import Grid from '@material-ui/core/Grid/Grid';
import Button from '@material-ui/core/Button';
import RunTable from './c/RunTable';
import ExecutionDialog from './ExecutionDialog';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import RunsSection from 'kpi/ui/admin/c/RunsSection';
import Tabs, { Tab } from 'ui/common/Tabs';
import ChangesSection from 'kpi/ui/admin/c/ChangesSection';
import withStyles from '@material-ui/core/styles/withStyles';
import PageTitle from 'ui/common/PageTitle';
import DateInput from './c/DateInput';
// import { parse } from 'query-string';
......@@ -39,20 +42,18 @@ const styles = (theme) => ({
display: 'initial' as 'initial',
},
},
loadMoreButton: {
display: 'flex' as 'flex',
justifyContent: 'center' as 'center',
},
});
/*tslint:enable*/
interface IExecutionProps extends React.ClassAttributes<any> {
classes: any;
t?: any;
currentTab: string;
shortName: string;
loadMoreExecutionRuns: (name: string, page: number) => void;
loadExecutionChanges: any;
executionDetails: ExecutionDetails;
executionChanges: any;
getExecution: any;
executeExecution: any;
deleteExecution: any;
......@@ -67,10 +68,6 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
({}) => listDimensions({}),
];
public state = {
currentRun: null,
};
private delete = () => {
const {deleteExecution, executionDetails} = this.props;
console.log('Executing');
......@@ -94,54 +91,12 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
// document.removeEventListener('copy', this.copyRunTable, false);
}
private copyRunTable = (e) => {
// if (e.currentTarget.activeElement.id === 'copyBtn') {
// e.clipboardData.setData('text/html', document.getElementById('runTable').innerHTML);
e.preventDefault();
const table: any = document.querySelector('#runTable');
window.getSelection().removeAllRanges();
const range = document.createRange();
range.selectNode(table);
window.getSelection().addRange(range);
console.log('Table copied to clipboard.');
document.execCommand('copy');
window.getSelection().removeAllRanges();
const { showSnackbar, t } = this.props;
showSnackbar(t('common:action.copiedToClipboard'));
// window.getSelection().removeAllRanges();
// }
}
private execute = () => {
const { executeExecution, executionDetails } = this.props;
console.log('Executing');
executeExecution(executionDetails.execution.name);
}
private loadRunInfo = (runInfo: ExecutionRun) => {
const {executionDetails: {lastRun, execution: {name}}} = this.props;
if (runInfo.timestamp === lastRun.timestamp) {
this.setState({currentRun: null});
} else {
KpiService.executionRun(name, runInfo.id).then((run) => this.setState({currentRun: run}));
}
}
private loadRunInfoByDate = (date: string) => {
const { shortName, t, showSnackbar } = this.props;
if (shortName) {
KpiService.executionRunByDate(shortName, date).then((run) => {
if (!run) {
showSnackbar(t('kpi.admin.p.executionDisplay.nothingToLoad'));
} else {
this.setState({ currentRun: run });
}
});
}
}
public componentWillReceiveProps(nextProps) {
const { shortName, executionDetails, getExecution } = nextProps;
if (!executionDetails || !executionDetails.execution || executionDetails.execution.name !== shortName) {
......@@ -150,21 +105,12 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
}
}
private loadMoreExecutionRuns = () => {
const { shortName, executionDetails, loadMoreExecutionRuns } = this.props;
if (shortName && executionDetails && executionDetails.runs) {
loadMoreExecutionRuns(shortName, executionDetails.runs.number + 1);
}
}
public render() {
const { t, classes, shortName, executionDetails } = this.props;
const {t, classes, shortName, executionDetails, showSnackbar, loadMoreExecutionRuns, currentTab, loadExecutionChanges, executionChanges} = this.props;
const stillLoading = !executionDetails || !executionDetails.execution || executionDetails.execution.name !== shortName;
const execution = executionDetails === null ? null : executionDetails.execution;
const runs = executionDetails === null || !executionDetails.runs ? null : executionDetails.runs.content;
const lastRun = executionDetails === null ? null : executionDetails.lastRun;
const {currentRun} = this.state;
if (stillLoading) {
return <Loading />;
......@@ -222,41 +168,32 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
</PageSection>
</Grid>
<Grid item sm={ 12 } md={ 6 }>
<PageSection title={ currentRun === null ? t('kpi.admin.p.executionDisplay.lastRun') : <span>{ t('kpi.admin.p.executionDisplay.runFrom') }<PrettyDate value={ currentRun.timestamp }/></span> }
action={
<span className={ classes.runInfoHeaderActions }>
{ (currentRun || lastRun) && <DateInput initialValue={ currentRun ? currentRun.timestamp : lastRun.timestamp } onSubmit={ this.loadRunInfoByDate }/> }
<Button id="copyBtn" onClick={ this.copyRunTable }>
{ t('common:action.copyToClipboard') }&nbsp;&nbsp;
<FileCopyIcon fontSize="small"/>
</Button>
</span>
}
>
<RunTable execution={ execution } runInfo={ currentRun || lastRun } />
</PageSection>
</Grid>
<Grid item xs={ 12 } md={ 6 }>
{ runs && runs.length > 0 &&
<PageSection title={ t('kpi.admin.p.executionDisplay.runs') }>
<Properties>
{ runs.map((runInfo, i) => (
<PropertiesItem numeric key={ runInfo.id } title={ <PrettyDate value={ runInfo.timestamp }/> }>
<a onClick={ () => this.loadRunInfo(runInfo) }>{ t('kpi.common.showRun') }</a>
</PropertiesItem>
)) }
<PropertiesItem>
{ !executionDetails.runs.last &&
<a onClick={ this.loadMoreExecutionRuns } className={ classes.loadMoreButton }>
{ t('kpi.admin.p.executionDisplay.loadMore') }
</a>
}
</PropertiesItem>
</Properties>
</PageSection>
<Grid item xs={ 12 } className="full-width">
<PageSection
classes={ {sectionTitle: classes.pageSectionTitle} }
title={
<Tabs
tab={ currentTab }
>
<Tab name="" to={ `/admin/kpi/${ shortName }` }>{ t('Runs') }</Tab>
<Tab name="changes" to={ `/admin/kpi/${ shortName }/changes` }>{ t('Changes') }</Tab>
</Tabs>
}
</Grid>
>
{ currentTab === 'changes' ?
<ChangesSection changes={ executionChanges }
showSnackbar={ showSnackbar }
loadExecutionChanges={ loadExecutionChanges }
executionName={ execution.name }/>
:
<RunsSection
executionDetails={ executionDetails }
showSnackbar={ showSnackbar }
loadMoreExecutionRuns={ loadMoreExecutionRuns }
/>
}
</PageSection>
</Grid>
</Grid>
</PageContents>
</div>
......@@ -266,11 +203,14 @@ class ExecutionDisplay extends React.Component<IExecutionProps, any> {
const mapStateToProps = (state, ownProps) => ({
executionDetails: state.kpi.admin.exec.details,
executionChanges: state.kpi.admin.exec.changes,
shortName: ownProps.match.params.shortName,
currentTab: ownProps.match.params.tab,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
getExecution,
loadExecutionChanges,
loadMoreExecutionRuns,
executeExecution,
deleteExecution,
......
import * as React from 'react';
import { translate } from 'react-i18next';
import * as moment from 'moment';
import { Field, reduxForm } from 'redux-form';
// constants
import { CHANGES_SEARCH_FORM } from 'kpi/constants';
// ui
import Button from '@material-ui/core/Button';
import { TextField } from 'ui/common/text-field';
import RadioSelection from 'ui/common/forms/RadioSelection';
class ParameterForm extends React.Component<any> {
public state = {
mode: 'daysToToday',
};
public constructor(props: any) {
super(props);
}
public render() {
const {t, error, handleSubmit, submitting, invalid} = this.props;
const {mode} = this.state;
return (
<form onSubmit={ handleSubmit }>
<div>
<Field
name={ `mode` }
component={ RadioSelection }
options={ ['daysToToday', 'daysToDate', 'fromDateToDate'] }
formLabel={ t(`kpi.admin.c.changesSearchForm.type`) }
singleColumn
renderOptionLabel={ (type) => t(`kpi.admin.c.changesSearchForm.types.${ type }`) }
onChange={ (e, value) => this.setState({mode: value}) }
/>
{ (mode === 'daysToToday' || mode === 'daysToDate') &&
< Field
name={ `days` }
component={ TextField }
type="number"
label={ t(`kpi.admin.c.changesSearchForm.types.days`) }
/>
}
{ (mode === 'daysToDate' || mode === 'fromDateToDate') &&
<Field
name={ `to` }
component={ TextField }
type="date"
label={ t(`kpi.admin.c.changesSearchForm.types.toDate`) }
shrink
/>
}
{ mode === 'fromDateToDate' &&
<Field
name={ `from` }
component={ TextField }
type="date"
label={ t(`kpi.admin.c.changesSearchForm.types.fromDate`) }
shrink
/>
}
</div>
{ error && <div style={ {color: 'red'} }>{ error }</div> }
<Button variant="contained" type="submit" style={ {marginRight: '1rem', marginTop: '1rem'} }
disabled={ submitting || invalid }>{ t('common:action.search') }</Button>
</form>
);
}
}
const validate = (values) => {
if (values.from && values.to) {
if (new Date(values.from).getDate() > new Date(values.to).getDate()) {
return {from: 'From must be before to'};
}
}
return {};
};
export default translate()(reduxForm({
form: CHANGES_SEARCH_FORM,
initialValues: {
mode: 'daysToToday',
days: 10,
to: moment(new Date()).format('YYYY-MM-DD'),
},
validate,
enableReinitialize: true,
})(ParameterForm));
import * as React from 'react';
import {translate} from 'react-i18next';
import withStyles from '@material-ui/core/styles/withStyles';
// ui
import { Grid, Button } from '@material-ui/core';
import PrettyDate from 'ui/common/time/PrettyDate';
import ObservationsTable from 'kpi/ui/admin/c/ObservationsTable';
import ChangesSearchForm from 'kpi/ui/admin/c/ChangesSearchForm';
import FileCopyIcon from '@material-ui/icons/FileCopy';
const styles = (theme) => ({
changesItem: {
marginTop: '14px',
},
changesItemHeader: {
display: 'flex' as 'flex',
justifyContent: 'space-between' as 'space-between',
},
noData: {
width: '100%',
height: '100%',
backgroundColor: '#f8f7f5',
color: 'grey',
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
justifyContent: 'center' as 'center',
},
});
interface IChangesSectionProps extends React.ClassAttributes<any> {
changes: any;
executionName: string;
loadExecutionChanges: (name, {days, to, from}: { days: number, to: string, from: string }) => void;
classes: any;
t: any;
showSnackbar: (message: string) => void;
}
class ChangesSection extends React.Component<IChangesSectionProps> {
private loadExecutionChanges = (value) => {
const {executionName, loadExecutionChanges} = this.props;
if (value.days || value.to || value.from) {
const searchQuery = {
days: value.mode === 'daysToToday' || value.mode === 'daysToDate' ? value.days : null,
to: value.mode === 'fromDateToDate' || value.mode === 'daysToDate' ? value.to : null,
from: value.mode === 'fromDateToDate' ? value.from : null,
};
loadExecutionChanges(executionName, searchQuery);
}
}
private copyRunTable = (e, index) => {
// if (e.currentTarget.activeElement.id === 'copyBtn') {
// e.clipboardData.setData('text/html', document.getElementById('runTable').innerHTML);
e.preventDefault();
const table: any = document.querySelector(`#changes-table-${index}`);
window.getSelection().removeAllRanges();
const range = document.createRange();
range.selectNode(table);
window.getSelection().addRange(range);
console.log('Table copied to clipboard.');
document.execCommand('copy');
window.getSelection().removeAllRanges();
const { showSnackbar, t } = this.props;
showSnackbar(t('common:action.copiedToClipboard'));
// window.getSelection().removeAllRanges();
// }
}
public render() {
const {changes, classes, t} = this.props;
return (
<Grid container spacing={ 8 } justify="space-between">
<Grid xs={ 12 } md={ 3 } item>
<ChangesSearchForm onSubmit={ this.loadExecutionChanges }/>
</Grid>
<Grid xs={ 12 } md={ 8 } item>
{ changes ?
Object.keys(changes).sort((a, b) => (new Date(b)).getTime() - (new Date(a).getTime())).map((date, index) => (
<div key={ date } className={ classes.changesItem }>
<div className={ classes.changesItemHeader }>
<h3><PrettyDate value={ date }/></h3>
<Button id={ `copy-button-${index}` } onClick={ (e) => this.copyRunTable(e, index) }>
{ t('common:action.copyToClipboard') }&nbsp;&nbsp; <FileCopyIcon fontSize="small"/>
</Button>
</div>
<ObservationsTable id={ `changes-table-${index}` } executionType="COUNT" observations={ changes[date] } highlight/>
</div>
))
:
<div className={ classes.noData }><h2>{ t('kpi.admin.c.changesSection.noData') }</h2></div>
}
</Grid>
</Grid>
);
}
}
export default translate()(withStyles(styles)(ChangesSection));
......@@ -14,6 +14,17 @@ const styles = (theme) => ({
background: 'green',
},
/*tslint:disable*/
highlightPositive: {
color: '#88ba42',
fontWeight: 500,