[TSVB] Display messaging to indicate no data vs empty charts (#14299)

* Visualization shows no data message vs empty charts
* added no data messaging to markdown variables
* Parse errors from handlebars and display clean errors to users
This commit is contained in:
Matt Apperson 2017-10-04 14:00:37 -04:00 committed by GitHub
parent 2b7808b9ea
commit c7b095d9da
8 changed files with 237 additions and 74 deletions

View file

@ -7,6 +7,7 @@ function ErrorComponent(props) {
let additionalInfo;
const type = _.get(error, 'error.caused_by.type');
let reason = _.get(error, 'error.caused_by.reason');
const title = _.get(error, 'error.caused_by.title');
if (!reason) {
reason = _.get(error, 'message');
@ -17,22 +18,22 @@ function ErrorComponent(props) {
reason = _.get(error, 'error.caused_by.caused_by.reason');
additionalInfo = (
<div className="metrics_error__additional">
<div className="metrics_error__reason">{ reason }</div>
<div className="metrics_error__stack">{ scriptStack.join('\n')}</div>
<div className="metrics_error__reason">{reason}</div>
<div className="metrics_error__stack">{scriptStack.join('\n')}</div>
</div>
);
} else if (reason) {
additionalInfo = (
<div className="metrics_error__additional">
<div className="metrics_error__reason">{ reason }</div>
<div className="metrics_error__reason">{reason}</div>
</div>
);
}
return (
<div className="metrics_error">
<div className="merics_error__title">The request for this panel failed.</div>
{ additionalInfo }
<div className="merics_error__title">{title || 'The request for this panel failed.'}</div>
{additionalInfo}
</div>
);
}

View file

@ -2,9 +2,33 @@ import _ from 'lodash';
import handlebars from 'handlebars/dist/handlebars';
export default function replaceVars(str, args = {}, vars = {}) {
try {
const template = handlebars.compile(str);
return template(_.assign({}, vars, { args }));
const template = handlebars.compile(str, { strict: true });
const string = template(_.assign({}, vars, { args }));
return string;
} catch (e) {
return str;
// user is probably typing and so its not formed correctly
if (e.toString().indexOf('Parse error') !== -1) {
return str;
// Unknown variable
} else if (e.message.indexOf('not defined in') !== -1) {
const badVar = e.message.split(/"/)[1];
e.error = {
caused_by: {
reason: `{{${badVar}}} is an unknown variable`,
title: 'Error processing your markdown'
}
};
} else {
e.error = {
caused_by: {
reason: 'Please verify you are only using markdown, known variables, and built-in Handlebars expressions',
title: 'Error processing your markdown'
}
};
}
return e;
}
}

View file

@ -13,7 +13,6 @@ import 'brace/mode/markdown';
import 'brace/theme/github';
class MarkdownEditor extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
@ -48,12 +47,10 @@ class MarkdownEditor extends Component {
rows.push(
<tr key={key}>
<td>
<a onClick={this.handleVarClick(snippet)}>
{ snippet }
</a>
<a onClick={this.handleVarClick(snippet)}>{snippet}</a>
</td>
<td>
<code>&ldquo;{ value }&rdquo;</code>
<code>&ldquo;{value}&rdquo;</code>
</td>
</tr>
);
@ -67,12 +64,12 @@ class MarkdownEditor extends Component {
rows.push(
<tr key={key}>
<td>
<a onClick={this.handleVarClick(snippet)}>
{ `{{ ${key} }}` }
</a>
<a onClick={this.handleVarClick(snippet)}>{`{{ ${key} }}`}</a>
</td>
<td>
<code>[ [ &ldquo;{date}&rdquo;, &ldquo;{value}&rdquo; ], ... ]</code>
<code>
[ [ &ldquo;{date}&rdquo;, &ldquo;{value}&rdquo; ], ... ]
</code>
</td>
</tr>
);
@ -92,7 +89,6 @@ class MarkdownEditor extends Component {
walk(variables);
return (
<div className="vis_editor__markdown">
<div className="vis_editor__markdown-editor">
@ -109,7 +105,13 @@ class MarkdownEditor extends Component {
/>
</div>
<div className="vis_editor__markdown-variables">
<div>The following variables can be used in the Markdown by using the Handlebar (mustache) syntax. <a href="http://handlebarsjs.com/expressions.html" target="_BLANK">Click here for documentation</a> on the available expressions.</div>
<div>
The following variables can be used in the Markdown by using the Handlebar (mustache) syntax.{' '}
<a href="http://handlebarsjs.com/expressions.html" target="_BLANK">
Click here for documentation
</a>{' '}
on the available expressions.
</div>
<table className="table">
<thead>
<tr>
@ -117,13 +119,19 @@ class MarkdownEditor extends Component {
<th>Value</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
<tbody>{rows}</tbody>
</table>
<div className="vis_editor__markdown-code-desc">There is also a special variable named <code>_all</code> which you can use to access the entire tree. This is useful for creating lists with data from a group by...</div>
{rows.length === 0 && (
<div className="vis_editor__no-markdown-variables">No variables avaliable for the selected data metrics.</div>
)}
<div className="vis_editor__markdown-code-desc">
There is also a special variable named <code>_all</code> which you can use to access the entire tree. This is useful for
creating lists with data from a group by...
</div>
<pre>
<code>{`# All servers:
<code>
{`# All servers:
{{#each _all}}
- {{ label }} {{ last.formatted }}
@ -134,7 +142,6 @@ class MarkdownEditor extends Component {
</div>
);
}
}
MarkdownEditor.propTypes = {

View file

@ -0,0 +1,10 @@
import React from 'react';
function NoDataComponent() {
return (
<div className="metrics_issue">
<div className="metrics_issue__title">No data to display for the selected metrics .</div>
</div>
);
}
export default NoDataComponent;

View file

@ -5,12 +5,13 @@ import color from 'color';
import Markdown from 'react-markdown';
import replaceVars from '../../lib/replace_vars';
import convertSeriesToVars from '../../lib/convert_series_to_vars';
import ErrorComponent from '../../error';
function MarkdownVisualization(props) {
const { backgroundColor, model, visData, dateFormat } = props;
const series = _.get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model, dateFormat);
const style = { };
const style = {};
let reversed = props.reversed;
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor) {
@ -19,23 +20,25 @@ function MarkdownVisualization(props) {
}
let markdown;
if (model.markdown) {
const markdownSource = replaceVars(model.markdown, {}, {
_all: variables,
...variables
});
const markdownSource = replaceVars(
model.markdown,
{},
{
_all: variables,
...variables
}
);
let className = 'thorMarkdown';
let contentClassName = `thorMarkdown__content ${model.markdown_vertical_align}`;
if (model.markdown_scrollbars) contentClassName += ' scrolling';
if (reversed) className += ' reversed';
const markdownError = markdownSource instanceof Error ? markdownSource : null;
markdown = (
<div className={className}>
<style type="text/css">
{model.markdown_css}
</style>
{markdownError && <ErrorComponent error={markdownError} />}
<style type="text/css">{model.markdown_css}</style>
<div className={contentClassName}>
<div id={`markdown-${model.id}`}>
<Markdown escapeHtml={true} source={markdownSource}/>
</div>
<div id={`markdown-${model.id}`}>{!markdownError && <Markdown escapeHtml={true} source={markdownSource} />}</div>
</div>
</div>
);

View file

@ -8,6 +8,7 @@ import topN from './vis_types/top_n/vis';
import gauge from './vis_types/gauge/vis';
import markdown from './vis_types/markdown/vis';
import Error from './error';
import NoData from './no_data';
const types = {
timeseries,
@ -24,10 +25,19 @@ function Visualization(props) {
if (error) {
return (
<div className={props.className}>
<Error error={error}/>
<Error error={error} />
</div>
);
}
const noData = _.get(visData, `${model.id}.series`).length === 0;
if (noData) {
return (
<div className={props.className}>
<NoData />
</div>
);
}
const component = types[model.type];
if (component) {
return React.createElement(component, {
@ -40,7 +50,7 @@ function Visualization(props) {
visData: props.visData
});
}
return (<div className={props.className} />);
return <div className={props.className} />;
}
Visualization.defaultProps = {

View file

@ -58,15 +58,45 @@
margin: 0 10px 0 0;
}
}
.vis_editor__input { padding: 8px 10px; border-radius: @borderRadius; border: 1px solid @grayLight; }
.vis_editor__input-grows { .vis_editor__input; flex-grow: 1; }
.vis_editor__input-grows-100 { .vis_editor__input-grows; width: 100%; }
.vis_editor__row { display: flex; align-items: center; }
.vis_editor__item { flex-grow: 1; }
.vis_editor__row_item { flex-grow: 1; margin-right: 10px; }
.vis_editor__subhead { font-size: 12px; color: @gray; margin: 5px 0; }
.vis_editor__subhead-main { font-size: 18px; color: @gray; margin: 10px 10px 5px; }
.vis_editor__note { font-size: 14px; color: @gray; margin: 5px 0 10px 0; }
.vis_editor__input {
padding: 8px 10px;
border-radius: @borderRadius;
border: 1px solid @grayLight;
}
.vis_editor__input-grows {
.vis_editor__input;
flex-grow: 1;
}
.vis_editor__input-grows-100 {
.vis_editor__input-grows;
width: 100%;
}
.vis_editor__row {
display: flex;
align-items: center;
}
.vis_editor__item {
flex-grow: 1;
}
.vis_editor__row_item {
flex-grow: 1;
margin-right: 10px;
}
.vis_editor__subhead {
font-size: 12px;
color: @gray;
margin: 5px 0;
}
.vis_editor__subhead-main {
font-size: 18px;
color: @gray;
margin: 10px 10px 5px;
}
.vis_editor__note {
font-size: 14px;
color: @gray;
margin: 5px 0 10px 0;
}
// color_picker.js
.vis_editor__color_picker {
@ -74,19 +104,40 @@
align-items: center;
position: relative;
}
.vis_editor__color_picker-swatch { border: 1px solid @grayDark; width: 20px; height: 20px; border-radius: 4px; }
.vis_editor__color_picker-swatch {
border: 1px solid @grayDark;
width: 20px;
height: 20px;
border-radius: 4px;
}
.vis_editor__color_picker-swatch-empty {
.vis_editor__color_picker-swatch;
background-color: transparent;
background-size: 20px 20px;
background-image: repeating-linear-gradient(-45deg, #C00, #C00 2px, transparent 2px, transparent 16px);
background-image: repeating-linear-gradient(
-45deg,
#c00,
#c00 2px,
transparent 2px,
transparent 16px
);
}
.vis_editor__color_picker-clear {
margin-left: 5px;
color: #C00;
color: #c00;
}
.vis_editor__color_picker-popover {
position: absolute;
top: 20px;
z-index: 2;
}
.vis_editor__color_picker-cover {
position: fixed;
top: 0px;
right: 0px;
left: 0px;
bottom: 0px;
}
.vis_editor__color_picker-popover { position: absolute; top: 20px; z-index: 2 }
.vis_editor__color_picker-cover { position: fixed; top: 0px; right: 0px; left: 0px; bottom: 0px; }
// data_format_picker
.vis_editor__data_format_picker-container {
@ -97,12 +148,16 @@
.vis_editor__data_format_picker-custom_row {
display: flex;
align-items: center;
> .vis_editor__label { margin-left: 10px; }
> .vis_editor__label {
margin-left: 10px;
}
}
// index_pattern.js
.vis_editor__index_pattern-fields { margin-right: 10px; flex-grow: 1; }
.vis_editor__index_pattern-fields {
margin-right: 10px;
flex-grow: 1;
}
// series.js
// mainRow == .vis_editor__container
@ -113,7 +168,6 @@
&:first-child {
border-top: none;
}
}
.vis_editor__series-row {
.vis_editor__container;
@ -125,7 +179,9 @@
.vis_editor__series-details {
.vis_editor__row;
flex-grow: 1;
> * { margin-right: 10px; }
> * {
margin-right: 10px;
}
> .vis_editor__sort {
cursor: move;
margin-right: 0px;
@ -139,17 +195,35 @@
}
// series_config.js
.vis_editor__series_config-subhead { .vis_editor__subhead; margin: 10px 0 5px; }
.vis_editor__series_config-subhead {
.vis_editor__subhead;
margin: 10px 0 5px;
}
// series_editor.js
.vis_editor__series_editor-container { margin-bottom: 20px; }
.vis_editor__series_editor-container {
margin-bottom: 20px;
}
//split.js
.vis_editor__split-container { .vis_editor__row; flex-grow: 1; }
.vis_editor__split-filter { .vis_editor__input; flex-grow: 1; }
.vis_editor__split-selects { .vis_editor__item; }
.vis_editor__split-aggs { flex-grow: 1; }
.vis_editor__split-term_count { .vis_editor__input; width: 50; }
.vis_editor__split-container {
.vis_editor__row;
flex-grow: 1;
}
.vis_editor__split-filter {
.vis_editor__input;
flex-grow: 1;
}
.vis_editor__split-selects {
.vis_editor__item;
}
.vis_editor__split-aggs {
flex-grow: 1;
}
.vis_editor__split-term_count {
.vis_editor__input;
width: 50;
}
// vis_picker.js
.vis_editor__vis_picker-container {
@ -232,7 +306,9 @@
.vis_editor__agg_row-icon {
margin-right: 10px;
color: @gray;
&.last { color: @grayDark }
&.last {
color: @grayDark;
}
}
.vis_editor__series_row {
@ -241,7 +317,10 @@
margin-bottom: 2px;
padding: 10px;
align-items: center;
.vis_editor__label { margin-bottom: 5px; font-size: 12px; }
.vis_editor__label {
margin-bottom: 5px;
font-size: 12px;
}
}
.vis_editor__agg_row {
@ -325,7 +404,7 @@
margin-right: 10px;
}
.vis_editor__calc_vars-var{
.vis_editor__calc_vars-var {
flex-grow: 1;
margin-right: 10px;
}
@ -355,13 +434,24 @@
max-height: 500px;
overflow: auto;
width: 50%;
.table a { text-decoration: none }
.table a {
text-decoration: none;
}
pre {
border-radius: 0;
border: none;
}
}
.vis_editor__no-markdown-variables {
margin-top: 60px;
margin-bottom: 10px;
padding-bottom: 60px;
text-align: center;
font-weight: bold;
border-bottom: 1px solid #d9d9d9;
}
.vis_editor__markdown-code-desc {
margin-bottom: 10px;
}
@ -411,7 +501,9 @@
.vis_editor__annotations-missing {
padding: 30px 10px;
font-size: 16px;
p { font-size: 16px; }
p {
font-size: 16px;
}
text-align: center;
}

View file

@ -1,11 +1,27 @@
.metrics_issue {
display: flex;
flex-direction: column;
flex: 1 0 auto;
background-color: #b3ccd5;
color: #0079a5;
justify-content: center;
padding: 20px;
}
.metrics_issue__title {
text-align: center;
font-size: 18px;
font-weight: bold;
}
.metrics_error {
display: flex;
flex-direction: column;
flex: 1 0 auto;
background-color: #FFD9D9;
color: #C00;
background-color: #ffd9d9;
color: #c00;
justify-content: center;
padding: 20px
padding: 20px;
}
.merics_error__title {
@ -25,8 +41,8 @@
.metrics_error__stack {
margin-top: 10px;
color: #FFF;
border: 10px solid #FFF;
color: #fff;
border: 10px solid #fff;
background-color: #000;
font-family: "Courier New", Courier, monospace;
white-space: pre;