[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:
parent
2b7808b9ea
commit
c7b095d9da
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>“{ value }”</code>
|
||||
<code>“{value}”</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>[ [ “{date}”, “{value}” ], ... ]</code>
|
||||
<code>
|
||||
[ [ “{date}”, “{value}” ], ... ]
|
||||
</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 = {
|
||||
|
|
10
src/core_plugins/metrics/public/components/no_data.js
Normal file
10
src/core_plugins/metrics/public/components/no_data.js
Normal 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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue