Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds visible error explanations for certain classes of errors. Closes 590. #609

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/distributions/editor/TextForm/TextForm.js
Expand Up @@ -42,7 +42,7 @@ export class TextForm extends Component{
guesstimate: {input, guesstimateType},
onEscape,
size,
hasErrors,
errors,
onChangeInput,
onAddData,
onChangeGuesstimateType,
Expand All @@ -66,7 +66,7 @@ export class TextForm extends Component{
onBlur={this._handleBlur.bind(this)}
onChangeData={onAddData}
ref='TextInput'
hasErrors={hasErrors}
errors={errors}
width={shouldBeWide ? 'NARROW' : "WIDE"}
/>

Expand Down
54 changes: 33 additions & 21 deletions src/components/distributions/editor/TextForm/TextInput.js
Expand Up @@ -2,9 +2,11 @@ import React, {Component, PropTypes} from 'react'

import $ from 'jquery'
import {EditorState, Editor, ContentState, Modifier, CompositeDecorator} from 'draft-js'
import ReactTooltip from 'react-tooltip'

import {isData, formatData} from 'lib/guesstimator/formatter/formatters/Data'
import {getFactParams, addText, addSuggestionToEditorState, STATIC_DECORATOR, STATIC_DECORATOR_LIST} from 'lib/factParser'
import {INTERNAL_ERROR} from 'lib/errors/modelErrors'

export default class TextInput extends Component{
displayName: 'Guesstimate-TextInput'
Expand Down Expand Up @@ -45,7 +47,7 @@ export default class TextInput extends Component{
}
this.setState(newState)

const text = newState.editorState.getCurrentContent().getPlainText('')
const text = newState.editorState.getCurrentContent().getPlainText('').trim()
if (text === this.props.value) { return }
if (isData(text)) {
this.props.onChangeData(formatData(text))
Expand Down Expand Up @@ -80,27 +82,37 @@ export default class TextInput extends Component{
}

render() {
const [{hasErrors, width, value}, {editorState}] = [this.props, this.state]
const className = `TextInput ${width}` + (_.isEmpty(value) && hasErrors ? ' hasErrors' : '')
const ReactTooltipParams = {class: 'metric-errors-tooltip', delayShow: 0, delayHide: 0, type: 'error', place: 'left', effect: 'solid'}
const [{errors, width, value}, {editorState}] = [this.props, this.state]
const hasErrors = !_.isEmpty(errors)
const className = `TextInput ${width}` + (!_.isEmpty(value) && hasErrors ? ' hasErrors' : '')
const displayedError = errors.find(e => e.type !== INTERNAL_ERROR)
return (
<span
className={className}
onClick={this.focus.bind(this)}
onKeyDown={e => {e.stopPropagation()}}
onFocus={this.handleFocus.bind(this)}
>
<Editor
onFocus={this.props.onFocus}
onEscape={this.props.onEscape}
editorState={editorState}
handleReturn={e => this.props.onReturn(e.shiftKey)}
onTab={this.handleTab.bind(this)}
onBlur={this.handleBlur.bind(this)}
onChange={this.onChange.bind(this)}
ref='editor'
placeholder={'value'}
/>
</span>
<div>
{hasErrors && !!displayedError &&
<ReactTooltip {...ReactTooltipParams} id='errors'> <span>{displayedError.message}</span> </ReactTooltip>
}
<span
className={className}
onClick={this.focus.bind(this)}
onKeyDown={e => {e.stopPropagation()}}
onFocus={this.handleFocus.bind(this)}
data-tip
data-for='errors'
>
<Editor
onFocus={this.props.onFocus}
onEscape={this.props.onEscape}
editorState={editorState}
handleReturn={e => this.props.onReturn(e.shiftKey)}
onTab={this.handleTab.bind(this)}
onBlur={this.handleBlur.bind(this)}
onChange={this.onChange.bind(this)}
ref='editor'
placeholder={'value'}
/>
</span>
</div>
)
}
}
6 changes: 2 additions & 4 deletions src/components/distributions/editor/index.js
Expand Up @@ -91,11 +91,9 @@ export default class Guesstimate extends Component{
const {size, guesstimate, onOpen, errors} = this.props
if(guesstimate.metric !== this.props.metricId) { return false }

const isLarge = (size === 'large')
const hasData = !!guesstimate.data
const formClasses = `Guesstimate${size === 'large' ? ' large' : ''}`

let formClasses = 'Guesstimate'
formClasses += isLarge ? ' large' : ''

return (
<div className={formClasses}>
Expand All @@ -120,7 +118,7 @@ export default class Guesstimate extends Component{
onReturn={this.handleReturn.bind(this)}
onTab={this.handleTab.bind(this)}
size={size}
hasErrors={!_.isEmpty(errors)}
errors={errors}
ref='TextForm'
/>
}
Expand Down
9 changes: 9 additions & 0 deletions src/components/distributions/editor/style.css
Expand Up @@ -4,6 +4,15 @@
@mixin small-input;
}

.Guesstimate .metric-errors-tooltip.metric-errors-tooltip.metric-errors-tooltip {
text-align: center;
opacity: 1;
padding: 4px 13px;
border-radius: 2px;
transition: none;
z-index: 10000000 !important;
}

.Guesstimate .TextInput {
line-height: 1.3em;
font-size: 0.95em;
Expand Down
6 changes: 4 additions & 2 deletions src/components/metrics/card/MetricCardViewSection/index.js
Expand Up @@ -9,10 +9,12 @@ import StatTable from 'gComponents/simulations/stat_table/index'
import {MetricToken} from 'gComponents/metrics/card/token/index'
import SensitivitySection from 'gComponents/metrics/card/SensitivitySection/SensitivitySection'

import {INFINITE_LOOP_ERROR, INPUT_ERROR} from 'lib/errors/modelErrors'

import './style.css'

const isBreak = (errors) => {return errors[0] && (errors[0] === 'BROKEN_UPSTREAM' || errors[0] === 'BROKEN_INPUT' )}
const isInfiniteLoop = (errors) => {return errors[0] && (errors[0] === 'INFINITE_LOOP')}
const isBreak = (errors) => _.some(errors, e => e.type === INPUT_ERROR)
const isInfiniteLoop = (errors) => _.some(errors, e => e.type === INFINITE_LOOP_ERROR)

// We have to display this section after it disappears
// to ensure that the metric card gets selected after click.
Expand Down
5 changes: 3 additions & 2 deletions src/components/metrics/card/index.js
Expand Up @@ -205,7 +205,7 @@ export default class MetricCard extends Component {
}

_errors() {
if (this.props.isTitle){ return [] }
if (this.props.isTitle) { return [] }
const errors = _.get(this.props.metric, 'simulation.sample.errors') || []
return errors.filter(e => !!e)
}
Expand Down Expand Up @@ -246,6 +246,7 @@ export default class MetricCard extends Component {
{this.state.modalIsOpen &&
<MetricModal
metric={metric}
errors={errors}
closeModal={this.closeModal.bind(this)}
onChangeGuesstimateDescription={this.onChangeGuesstimateDescription.bind(this)}
/>
Expand Down Expand Up @@ -281,7 +282,7 @@ export default class MetricCard extends Component {
onOpen={this.openModal.bind(this)}
ref='DistributionEditor'
size='small'
errors={errors}
errors={this.state.modalIsOpen ? [] : errors}
onReturn={this.props.onReturn}
onTab={this.props.onTab}
/>
Expand Down
13 changes: 7 additions & 6 deletions src/components/metrics/modal/index.js
Expand Up @@ -61,7 +61,7 @@ export class MetricModal extends Component {
}
const showSimulation = this.showSimulation()

const {closeModal, metric, onChangeGuesstimateDescription} = this.props
const {closeModal, metric, errors, onChangeGuesstimateDescription} = this.props
const sortedSampleValues = _.get(metric, 'simulation.sample.sortedValues')
const stats = _.get(metric, 'simulation.stats')
const guesstimate = metric.guesstimate
Expand Down Expand Up @@ -107,11 +107,12 @@ export class MetricModal extends Component {
<div className='container bottom'>
<div className='row editingInputSection'>
<div className='col-sm-12'>
<DistributionEditor
guesstimate={metric.guesstimate}
metricId={metric.id}
size={'large'}
/>
<DistributionEditor
guesstimate={metric.guesstimate}
errors={errors}
metricId={metric.id}
size={'large'}
/>
</div>
</div>
<div className='row guesstimateDescriptionSection'>
Expand Down
2 changes: 1 addition & 1 deletion src/components/spaces/show/Toolbar/index.js
Expand Up @@ -92,7 +92,7 @@ export class SpaceToolbar extends Component {
onAllowEdits,
onForbidEdits,
} = this.props
const ReactTooltipParams = {class: 'small-tooltip', delayShow: 0, delayHide: 0, place: 'bottom', effect: 'solid'}
const ReactTooltipParams = {class: 'header-action-tooltip', delayShow: 0, delayHide: 0, place: 'bottom', effect: 'solid'}

let view_mode_header = (<span><Icon name='eye'/> Viewing </span>)
if (editableByMe && editsAllowed) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/spaces/show/style.css
Expand Up @@ -201,7 +201,7 @@
min-height: 30em;
}

.small-tooltip.small-tooltip.small-tooltip {
.header-action-tooltip.header-action-tooltip.header-action-tooltip {
background: rgba(60, 64, 68, 0.95);
opacity: 1;
padding: 4px 13px;
Expand Down
13 changes: 7 additions & 6 deletions src/lib/engine/dgraph.js
@@ -1,16 +1,17 @@
/* @flow */
import * as graph from './graph';
import * as _guesstimate from './guesstimate';
import type {DGraph, Sample} from './types.js'
import * as graph from './graph'
import * as _guesstimate from './guesstimate'
import type {DGraph, Sample} from './types'
import {INTERNAL_ERROR} from 'lib/errors/modelErrors'

//borrowing a function from the graph library
const metric = graph.metric;
const metric = graph.metric

export function runSimulation(dGraph:DGraph, metricId:string, n:number) {
const m = metric(dGraph, metricId);
const m = metric(dGraph, metricId)
if (!m) {
console.warn('Unknown metric referenced')
return Promise.resolve({errors: ['Unknown metric referenced']})
return Promise.resolve({errors: [{type: INTERNAL_ERROR, message: 'Unknown metric referenced'}]})
}
return _guesstimate.sample(m.guesstimate, dGraph, n)
}
Expand Down
25 changes: 16 additions & 9 deletions src/lib/engine/graph.js
@@ -1,23 +1,25 @@
import * as _metric from './metric';
import * as _dgraph from './dgraph';
import * as _space from './space';
import BasicGraph from '../basic_graph/basic-graph.js'
import * as _metric from './metric'
import * as _dgraph from './dgraph'
import * as _space from './space'

import BasicGraph from 'lib/basic_graph/basic-graph'
import {INFINITE_LOOP_ERROR} from 'lib/errors/modelErrors'

export function create(graphAttributes){
return _.pick(graphAttributes, ['metrics', 'guesstimates', 'simulations']);
return _.pick(graphAttributes, ['metrics', 'guesstimates', 'simulations'])
}

export function denormalize(graph){
let metrics = _.map(graph.metrics, m => _metric.denormalize(m, graph));
return {metrics};
let metrics = _.map(graph.metrics, m => _metric.denormalize(m, graph))
return {metrics}
}

export function runSimulation(graph, metricId, n) {
return _dgraph.runSimulation(denormalize(graph), metricId, n)
}

export function metric(graph, id){
return graph.metrics.find(m => (m.id === id));
return graph.metrics.find(m => (m.id === id))
}

function basicGraph(graph){
Expand All @@ -36,7 +38,12 @@ export function dependencyList(graph, spaceId) {
export function dependencyTree(oGraph, graphFilters) {
const {spaceId, metricId, onlyHead, notHead, onlyUnsimulated} = graphFilters

if (onlyHead) { return [[metricId, 0]] }
if (onlyHead) {
const existingErrors = _.get(oGraph.simulations.find(s => s.metric === metricId), 'sample.errors')
// This is a hack to prevent the error type from changing while editing metrics with infinite loops.
// TODO(matthew): Store denormalized check so this hack is not necessary.
if (!_.some(existingErrors, e => e.type === INFINITE_LOOP_ERROR)) { return [[metricId, 0]] }
}

let graph = oGraph
if (spaceId) { graph = _space.subset(oGraph, spaceId) }
Expand Down
45 changes: 27 additions & 18 deletions src/lib/engine/guesstimate.js
@@ -1,7 +1,8 @@
/* @flow */

import type {Guesstimate, Distribution, DGraph, Graph, Simulation} from './types.js'
import {Guesstimator} from '../guesstimator/index.js'
import type {Guesstimate, Distribution, DGraph, Graph, Simulation} from './types'
import {Guesstimator} from '../guesstimator/index'
import {INPUT_ERROR} from 'lib/errors/modelErrors'

export function equals(l, r) {
return (
Expand All @@ -16,15 +17,15 @@ export const attributes = ['metric', 'input', 'guesstimateType', 'description',
export function sample(guesstimate: Guesstimate, dGraph: DGraph, n: number = 1) {
const metric = guesstimate.metric

const [parseErrors, item] = Guesstimator.parse(guesstimate)
if (parseErrors.length > 0) {
return Promise.resolve({ metric, sample: {values: [], errors: parseErrors} })
}
let errors = []
const [parseError, item] = Guesstimator.parse(guesstimate)
if (!_.isEmpty(parseError)) {errors.push(parseError)}

const [inputs, inputErrors] = item.needsExternalInputs() ? _inputMetricsWithValues(guesstimate, dGraph) : [{}, []]
errors.push(...inputErrors)

if (inputErrors.length > 0) {
return Promise.resolve({ metric, sample: {values: [], errors: inputErrors} })
if (!_.isEmpty(errors)) {
return Promise.resolve({ metric, sample: {values: [], errors} })
}

return item.sample(n, inputs).then(sample => ({ metric, sample }))
Expand Down Expand Up @@ -53,23 +54,31 @@ export function newGuesstimateType(newGuesstimate) {
//Check if a function; if not, return []
export function inputMetrics(guesstimate: Guesstimate, dGraph: DGraph): Array<Object> {
if (!_.has(dGraph, 'metrics')){ return [] }
return dGraph.metrics.filter( m => (guesstimate.input || '').includes(m.readableId) );
}

function _formatInputError(errorMsg) {
if (errorMsg === 'BROKEN_INPUT' || errorMsg === 'BROKEN_UPSTREAM') {
return 'BROKEN_UPSTREAM'
}
return 'BROKEN_INPUT'
return dGraph.metrics.filter( m => (guesstimate.input || '').includes(m.readableId) )
}

function _inputMetricsWithValues(guesstimate: Guesstimate, dGraph: DGraph): Object{
let inputs = {}
let errors = []
inputMetrics(guesstimate, dGraph).map(m => {
inputs[m.readableId] = _.get(m, 'simulation.sample.values')
const inputErrors = _.get(m, 'simulation.sample.errors')
errors = errors.concat(inputErrors ? inputErrors.map(_formatInputError) : [])

const inputErrors = _.get(m, 'simulation.sample.errors') || []
if (_.isEmpty(inputs[m.readableId]) && _.isEmpty(inputErrors)) {
errors.push({type: INPUT_ERROR, message: `Empty input ${m.readableId}`})
}

errors.push(...inputErrors.map(
({type, message}) => {
if (type === INPUT_ERROR) {
return {
type,
message: message.includes('upstream') ? message : message.replace('input', 'upstream input')
}
}
return {type: INPUT_ERROR, message: `Broken input ${m.readableId}`}
}
))
})
return [inputs, _.uniq(errors)]
}
8 changes: 4 additions & 4 deletions src/lib/engine/simulation.js
@@ -1,4 +1,4 @@
import * as sample from './sample';
import * as sample from './sample'

export function combine(simulations) {
let recentSimulations = simulations
Expand All @@ -12,19 +12,19 @@ export function combine(simulations) {
metric: recentSimulations[0].metric,
propagationId: recentSimulations[0].propagationId,
sample: sample.combine(recentSimulations.map(s => s.sample))
};
}
}

export function hasValues(simulation) {
return (values(simulation).length > 0);
return (values(simulation).length > 0)
}

export function values(simulation) {
return _.get(simulation, 'sample.values') || []
}

export function hasErrors(simulation) {
return (errors(simulation).length > 0);
return errors(simulation).length > 0
}

export function errors(simulation) {
Expand Down