Merge pull request #13844 from grafana/davkal/explore-empty-page

Explore: Pluggable components from datasource plugins
This commit is contained in:
David
2018-10-30 14:54:34 +01:00
committed by GitHub
15 changed files with 244 additions and 97 deletions
+57 -38
View File
@@ -4,6 +4,7 @@ import Select from 'react-select';
import _ from 'lodash'; import _ from 'lodash';
import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore'; import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore';
import { RawTimeRange } from 'app/types/series';
import kbn from 'app/core/utils/kbn'; import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors'; import colors from 'app/core/utils/colors';
import store from 'app/core/store'; import store from 'app/core/store';
@@ -16,14 +17,14 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
import ErrorBoundary from './ErrorBoundary';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import Graph from './Graph'; import Graph from './Graph';
import Logs from './Logs'; import Logs from './Logs';
import Table from './Table'; import Table from './Table';
import ErrorBoundary from './ErrorBoundary';
import TimePicker from './TimePicker'; import TimePicker from './TimePicker';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { RawTimeRange } from 'app/types/series'; import { DataSource } from 'app/types/datasources';
const MAX_HISTORY_ITEMS = 100; const MAX_HISTORY_ITEMS = 100;
@@ -148,7 +149,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} }
} }
async setDatasource(datasource) { async setDatasource(datasource: DataSource) {
const supportsGraph = datasource.meta.metrics; const supportsGraph = datasource.meta.metrics;
const supportsLogs = datasource.meta.logs; const supportsLogs = datasource.meta.logs;
const supportsTable = datasource.meta.metrics; const supportsTable = datasource.meta.metrics;
@@ -176,8 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
query: this.queryExpressions[i], query: this.queryExpressions[i],
})); }));
// Custom components
const StartPage = datasource.pluginExports.ExploreStartPage;
this.setState( this.setState(
{ {
StartPage,
datasource, datasource,
datasourceError, datasourceError,
history, history,
@@ -330,6 +335,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
); );
}; };
// Use this in help pages to set page to a single query
onClickQuery = query => {
const nextQueries = [{ query, key: generateQueryKey() }];
this.queryExpressions = nextQueries.map(q => q.query);
this.setState({ queries: nextQueries }, this.onSubmit);
};
onClickSplit = () => { onClickSplit = () => {
const { onChangeSplit } = this.props; const { onChangeSplit } = this.props;
if (onChangeSplit) { if (onChangeSplit) {
@@ -721,6 +733,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
render() { render() {
const { position, split } = this.props; const { position, split } = this.props;
const { const {
StartPage,
datasource, datasource,
datasourceError, datasourceError,
datasourceLoading, datasourceLoading,
@@ -760,6 +773,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
); );
const loading = queryTransactions.some(qt => !qt.done); const loading = queryTransactions.some(qt => !qt.done);
const showStartPages = StartPage && queryTransactions.length === 0;
return ( return (
<div className={exploreClass} ref={this.getRef}> <div className={exploreClass} ref={this.getRef}>
@@ -847,43 +861,48 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
supportsLogs={supportsLogs} supportsLogs={supportsLogs}
transactions={queryTransactions} transactions={queryTransactions}
/> />
<div className="result-options">
{supportsGraph ? (
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
Graph
</button>
) : null}
{supportsTable ? (
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
Table
</button>
) : null}
{supportsLogs ? (
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
Logs
</button>
) : null}
</div>
<main className="m-t-2"> <main className="m-t-2">
<ErrorBoundary> <ErrorBoundary>
{supportsGraph && {showStartPages && <StartPage onClickQuery={this.onClickQuery} />}
showingGraph && ( {!showStartPages && (
<Graph <>
data={graphResult} <div className="result-options">
height={graphHeight} {supportsGraph ? (
loading={graphLoading} <button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
id={`explore-graph-${position}`} Graph
range={graphRange} </button>
split={split} ) : null}
/> {supportsTable ? (
)} <button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
{supportsTable && showingTable ? ( Table
<div className="panel-container m-t-2"> </button>
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} /> ) : null}
</div> {supportsLogs ? (
) : null} <button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null} Logs
</button>
) : null}
</div>
{supportsGraph &&
showingGraph && (
<Graph
data={graphResult}
height={graphHeight}
loading={graphLoading}
id={`explore-graph-${position}`}
range={graphRange}
split={split}
/>
)}
{supportsTable && showingTable ? (
<div className="panel-container m-t-2">
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
</div>
) : null}
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null}
</>
)}
</ErrorBoundary> </ErrorBoundary>
</main> </main>
</div> </div>
+21 -19
View File
@@ -27,7 +27,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
return suggestions && suggestions.length > 0; return suggestions && suggestions.length > 0;
} }
interface TypeaheadFieldProps { interface QueryFieldProps {
additionalPlugins?: any[]; additionalPlugins?: any[];
cleanText?: (text: string) => string; cleanText?: (text: string) => string;
initialValue: string | null; initialValue: string | null;
@@ -35,14 +35,14 @@ interface TypeaheadFieldProps {
onFocus?: () => void; onFocus?: () => void;
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
onValueChanged?: (value: Value) => void; onValueChanged?: (value: Value) => void;
onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
placeholder?: string; placeholder?: string;
portalOrigin?: string; portalOrigin?: string;
syntax?: string; syntax?: string;
syntaxLoaded?: boolean; syntaxLoaded?: boolean;
} }
export interface TypeaheadFieldState { export interface QueryFieldState {
suggestions: CompletionItemGroup[]; suggestions: CompletionItemGroup[];
typeaheadContext: string | null; typeaheadContext: string | null;
typeaheadIndex: number; typeaheadIndex: number;
@@ -60,7 +60,7 @@ export interface TypeaheadInput {
wrapperNode: Element; wrapperNode: Element;
} }
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> { export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
menuEl: HTMLElement | null; menuEl: HTMLElement | null;
placeholdersBuffer: PlaceholdersBuffer; placeholdersBuffer: PlaceholdersBuffer;
plugins: any[]; plugins: any[];
@@ -72,7 +72,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || ''); this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || '');
// Base plugins // Base plugins
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins]; this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
this.state = { this.state = {
suggestions: [], suggestions: [],
@@ -102,7 +102,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
} }
} }
componentWillReceiveProps(nextProps: TypeaheadFieldProps) { componentWillReceiveProps(nextProps: QueryFieldProps) {
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) { if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
// Need a bogus edit to re-render the editor after syntax has fully loaded // Need a bogus edit to re-render the editor after syntax has fully loaded
const change = this.state.value const change = this.state.value
@@ -434,19 +434,21 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
render() { render() {
return ( return (
<div className="slate-query-field"> <div className="slate-query-field-wrapper">
{this.renderMenu()} <div className="slate-query-field">
<Editor {this.renderMenu()}
autoCorrect={false} <Editor
onBlur={this.handleBlur} autoCorrect={false}
onKeyDown={this.onKeyDown} onBlur={this.handleBlur}
onChange={this.onChange} onKeyDown={this.onKeyDown}
onFocus={this.handleFocus} onChange={this.onChange}
placeholder={this.props.placeholder} onFocus={this.handleFocus}
plugins={this.plugins} placeholder={this.props.placeholder}
spellCheck={false} plugins={this.plugins}
value={this.state.value} spellCheck={false}
/> value={this.state.value}
/>
</div>
</div> </div>
); );
} }
+6 -5
View File
@@ -2,9 +2,9 @@ import React, { PureComponent } from 'react';
import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore'; import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore';
// TODO make this datasource-plugin-dependent import DefaultQueryField from './QueryField';
import QueryField from './PromQueryField'; import QueryTransactionStatus from './QueryTransactionStatus';
import QueryTransactions from './QueryTransactions'; import { DataSource } from 'app/types';
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -24,7 +24,7 @@ interface QueryRowEventHandlers {
interface QueryRowCommonProps { interface QueryRowCommonProps {
className?: string; className?: string;
datasource: any; datasource: DataSource;
history: HistoryItem[]; history: HistoryItem[];
// Temporarily // Temporarily
supportsLogs?: boolean; supportsLogs?: boolean;
@@ -82,10 +82,11 @@ class QueryRow extends PureComponent<QueryRowProps> {
const transactionWithError = transactions.find(t => t.error !== undefined); const transactionWithError = transactions.find(t => t.error !== undefined);
const hint = getFirstHintFromTransactions(transactions); const hint = getFirstHintFromTransactions(transactions);
const queryError = transactionWithError ? transactionWithError.error : null; const queryError = transactionWithError ? transactionWithError.error : null;
const QueryField = datasource.pluginExports.ExploreQueryField || DefaultQueryField;
return ( return (
<div className="query-row"> <div className="query-row">
<div className="query-row-status"> <div className="query-row-status">
<QueryTransactions transactions={transactions} /> <QueryTransactionStatus transactions={transactions} />
</div> </div>
<div className="query-row-field"> <div className="query-row-field">
<QueryField <QueryField
@@ -1,17 +1,17 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { QueryTransaction as QueryTransactionModel } from 'app/types/explore'; import { QueryTransaction } from 'app/types/explore';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
function formatLatency(value) { function formatLatency(value) {
return `${(value / 1000).toFixed(1)}s`; return `${(value / 1000).toFixed(1)}s`;
} }
interface QueryTransactionProps { interface QueryTransactionStatusItemProps {
transaction: QueryTransactionModel; transaction: QueryTransaction;
} }
class QueryTransaction extends PureComponent<QueryTransactionProps> { class QueryTransactionStatusItem extends PureComponent<QueryTransactionStatusItemProps> {
render() { render() {
const { transaction } = this.props; const { transaction } = this.props;
const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading'; const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading';
@@ -26,16 +26,16 @@ class QueryTransaction extends PureComponent<QueryTransactionProps> {
} }
} }
interface QueryTransactionsProps { interface QueryTransactionStatusProps {
transactions: QueryTransactionModel[]; transactions: QueryTransaction[];
} }
export default class QueryTransactions extends PureComponent<QueryTransactionsProps> { export default class QueryTransactionStatus extends PureComponent<QueryTransactionStatusProps> {
render() { render() {
const { transactions } = this.props; const { transactions } = this.props;
return ( return (
<div className="query-transactions"> <div className="query-transactions">
{transactions.map((t, i) => <QueryTransaction key={`${t.query}:${t.resultType}`} transaction={t} />)} {transactions.map((t, i) => <QueryTransactionStatusItem key={`${t.query}:${t.resultType}`} transaction={t} />)}
</div> </div>
); );
} }
@@ -8,9 +8,10 @@ import { importPluginModule } from './plugin_loader';
// Types // Types
import { DataSourceApi } from 'app/types/series'; import { DataSourceApi } from 'app/types/series';
import { DataSource } from 'app/types';
export class DatasourceSrv { export class DatasourceSrv {
datasources: any; datasources: { [name: string]: DataSource };
/** @ngInject */ /** @ngInject */
constructor(private $q, private $injector, private $rootScope, private templateSrv) { constructor(private $q, private $injector, private $rootScope, private templateSrv) {
@@ -61,9 +62,10 @@ export class DatasourceSrv {
throw new Error('Plugin module is missing Datasource constructor'); throw new Error('Plugin module is missing Datasource constructor');
} }
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig }); const instance: DataSource = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
instance.meta = pluginDef; instance.meta = pluginDef;
instance.name = name; instance.name = name;
instance.pluginExports = plugin;
this.datasources[name] = instance; this.datasources[name] = instance;
deferred.resolve(instance); deferred.resolve(instance);
}) })
@@ -0,0 +1,35 @@
import React from 'react';
const CHEAT_SHEET_ITEMS = [
{
title: 'Request Rate',
expression: 'rate(http_request_total[5m])',
label:
'Given an HTTP request counter, this query calculates the per-second average request rate over the last 5 minutes.',
},
{
title: '95th Percentile of Request Latencies',
expression: 'histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[5m])) by (le))',
label: 'Calculates the 95th percentile of HTTP request rate over 5 minute windows.',
},
{
title: 'Alerts Firing',
expression: 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing"}[24h])) by (alertname))',
label: 'Sums up the alerts that have been firing over the last 24 hours.',
},
];
export default (props: any) => (
<div>
<h1>PromQL Cheat Sheet</h1>
{CHEAT_SHEET_ITEMS.map(item => (
<div className="cheat-sheet-item" key={item.expression}>
<div className="cheat-sheet-item__title">{item.title}</div>
<div className="cheat-sheet-item__expression" onClick={e => props.onClickQuery(item.expression)}>
<code>{item.expression}</code>
</div>
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}
</div>
);
@@ -7,11 +7,10 @@ import Prism from 'prismjs';
import { TypeaheadOutput } from 'app/types/explore'; import { TypeaheadOutput } from 'app/types/explore';
// dom also includes Element polyfills // dom also includes Element polyfills
import { getNextCharacter, getPreviousCousin } from './utils/dom'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom';
import BracesPlugin from './slate-plugins/braces'; import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import RunnerPlugin from './slate-plugins/runner'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
import TypeaheadField, { TypeaheadInput, TypeaheadFieldState } from './QueryField';
const HISTOGRAM_GROUP = '__histograms__'; const HISTOGRAM_GROUP = '__histograms__';
const METRIC_MARK = 'metric'; const METRIC_MARK = 'metric';
@@ -51,10 +50,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
return [...options, ...metricsOptions]; return [...options, ...metricsOptions];
} }
export function willApplySuggestion( export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string {
suggestion: string,
{ typeaheadContext, typeaheadText }: TypeaheadFieldState
): string {
// Modify suggestion based on context // Modify suggestion based on context
switch (typeaheadContext) { switch (typeaheadContext) {
case 'context-labels': { case 'context-labels': {
@@ -261,19 +257,17 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
)} )}
</div> </div>
<div className="prom-query-field-wrapper"> <div className="prom-query-field-wrapper">
<div className="slate-query-field-wrapper"> <TypeaheadField
<TypeaheadField additionalPlugins={this.plugins}
additionalPlugins={this.plugins} cleanText={cleanText}
cleanText={cleanText} initialValue={initialQuery}
initialValue={initialQuery} onTypeahead={this.onTypeahead}
onTypeahead={this.onTypeahead} onWillApplySuggestion={willApplySuggestion}
onWillApplySuggestion={willApplySuggestion} onValueChanged={this.onChangeQuery}
onValueChanged={this.onChangeQuery} placeholder="Enter a PromQL query"
placeholder="Enter a PromQL query" portalOrigin="prometheus"
portalOrigin="prometheus" syntaxLoaded={syntaxLoaded}
syntaxLoaded={syntaxLoaded} />
/>
</div>
{error ? <div className="prom-query-field-info text-error">{error}</div> : null} {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
{hint ? ( {hint ? (
<div className="prom-query-field-info text-warning"> <div className="prom-query-field-info text-warning">
@@ -0,0 +1,60 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import PromCheatSheet from './PromCheatSheet';
const TAB_MENU_ITEMS = [
{
text: 'Start',
id: 'start',
icon: 'fa fa-rocket',
},
];
export default class PromStart extends PureComponent<any, { active: string }> {
state = {
active: 'start',
};
onClickTab = active => {
this.setState({ active });
};
render() {
const { active } = this.state;
const customCss = '';
return (
<div style={{ margin: '45px 0', border: '1px solid #ddd', borderRadius: 5 }}>
<div className="page-header-canvas">
<div className="page-container">
<div className="page-header">
<nav>
<ul className={`gf-tabs ${customCss}`}>
{TAB_MENU_ITEMS.map((tab, idx) => {
const tabClasses = classNames({
'gf-tabs-link': true,
active: tab.id === active,
});
return (
<li className="gf-tabs-item" key={tab.id}>
<a className={tabClasses} onClick={() => this.onClickTab(tab.id)}>
<i className={tab.icon} />
{tab.text}
</a>
</li>
);
})}
</ul>
</nav>
</div>
</div>
</div>
<div className="page-container page-body">
{active === 'start' && <PromCheatSheet onClickQuery={this.props.onClickQuery} />}
</div>
</div>
);
}
}
@@ -2,6 +2,9 @@ import { PrometheusDatasource } from './datasource';
import { PrometheusQueryCtrl } from './query_ctrl'; import { PrometheusQueryCtrl } from './query_ctrl';
import { PrometheusConfigCtrl } from './config_ctrl'; import { PrometheusConfigCtrl } from './config_ctrl';
import PrometheusStartPage from './components/PromStart';
import PromQueryField from './components/PromQueryField';
class PrometheusAnnotationsQueryCtrl { class PrometheusAnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html'; static templateUrl = 'partials/annotations.editor.html';
} }
@@ -11,4 +14,6 @@ export {
PrometheusQueryCtrl as QueryCtrl, PrometheusQueryCtrl as QueryCtrl,
PrometheusConfigCtrl as ConfigCtrl, PrometheusConfigCtrl as ConfigCtrl,
PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl, PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl,
PromQueryField as ExploreQueryField,
PrometheusStartPage as ExploreStartPage,
}; };
+5 -1
View File
@@ -1,5 +1,5 @@
import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector'; import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector';
import { Plugin } from './plugins'; import { Plugin, PluginExports, PluginMeta } from './plugins';
export interface DataSource { export interface DataSource {
id: number; id: number;
@@ -16,6 +16,10 @@ export interface DataSource {
isDefault: boolean; isDefault: boolean;
jsonData: { authType: string; defaultRegion: string }; jsonData: { authType: string; defaultRegion: string };
readOnly: boolean; readOnly: boolean;
meta?: PluginMeta;
pluginExports?: PluginExports;
init?: () => void;
testDatasource?: () => Promise<any>;
} }
export interface DataSourcesState { export interface DataSourcesState {
+1
View File
@@ -146,6 +146,7 @@ export interface TextMatch {
} }
export interface ExploreState { export interface ExploreState {
StartPage?: any;
datasource: any; datasource: any;
datasourceError: any; datasourceError: any;
datasourceLoading: boolean | null; datasourceLoading: boolean | null;
+8
View File
@@ -6,6 +6,8 @@ export interface PluginExports {
ConfigCtrl?: any; ConfigCtrl?: any;
AnnotationsQueryCtrl?: any; AnnotationsQueryCtrl?: any;
PanelOptions?: any; PanelOptions?: any;
ExploreQueryField?: any;
ExploreStartPage?: any;
} }
export interface PanelPlugin { export interface PanelPlugin {
@@ -25,6 +27,12 @@ export interface PluginMeta {
name: string; name: string;
info: PluginMetaInfo; info: PluginMetaInfo;
includes: PluginInclude[]; includes: PluginInclude[];
// Datasource-specific
metrics?: boolean;
logs?: boolean;
explore?: boolean;
annotations?: boolean;
} }
export interface PluginInclude { export interface PluginInclude {
+17 -1
View File
@@ -52,7 +52,7 @@
} }
.result-options { .result-options {
margin-top: 2 * $panel-margin; margin: 2 * $panel-margin 0;
} }
.time-series-disclaimer { .time-series-disclaimer {
@@ -322,3 +322,19 @@
.ReactTable .rt-tr .rt-td:last-child { .ReactTable .rt-tr .rt-td:last-child {
text-align: right; text-align: right;
} }
// TODO Experimental
.cheat-sheet-item {
margin: 2*$panel-margin 0;
width: 50%;
}
.cheat-sheet-item__title {
font-size: $font-size-h3;
}
.cheat-sheet-item__expression {
margin: $panel-margin/2 0;
cursor: pointer;
}
+1 -1
View File
@@ -16,7 +16,7 @@ module.exports = {
publicPath: "public/build/", publicPath: "public/build/",
}, },
resolve: { resolve: {
extensions: ['.ts', '.tsx', '.es6', '.js', '.json'], extensions: ['.ts', '.tsx', '.es6', '.js', '.json', '.svg'],
alias: { alias: {
}, },
modules: [ modules: [