Commit b9b6af94 by kay delaney Committed by GitHub

Dashboards: Adds cheat sheet toggle to supported query editors (#28857)

* Dashboards: Adds cheat sheet toggle to supported query editors
parent 5d0a577f
......@@ -46,6 +46,7 @@ Improve an existing plugin with one of our guides:
- [Add support for annotations]({{< relref "add-support-for-annotations.md" >}})
- [Add support for Explore queries]({{< relref "add-support-for-explore-queries.md" >}})
- [Add support for variables]({{< relref "add-support-for-variables.md" >}})
- [Add a query editor help component]({{< relref "add-query-editor-help.md" >}})
- [Build a logs data source plugin]({{< relref "build-a-logs-data-source-plugin.md" >}})
- [Build a streaming data source plugin]({{< relref "build-a-streaming-data-source-plugin.md" >}})
- [Error handling]({{< relref "error-handling.md" >}})
......
## Add a query editor help component
By adding a help component to your plugin, you can for example create "cheat sheets" with commonly used queries. When the user clicks on one of the examples, it automatically updates the query editor. It's a great way to increase productivity for your users.
1. Create a file `QueryEditorHelp.tsx` in the `src` directory of your plugin, with the following content:
```ts
import React from 'react';
import { QueryEditorHelpProps } from '@grafana/data';
export default (props: QueryEditorHelpProps) => {
return (
<h2>My cheat sheet</h2>
);
};
```
1. Configure the plugin to use the `QueryEditorHelp`.
```ts
import QueryEditorHelp from './QueryEditorHelp';
```
```ts
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor)
.setExploreQueryField(ExploreQueryEditor)
.setQueryEditorHelp(QueryEditorHelp);
```
1. Create a few examples.
```ts
import React from 'react';
import { QueryEditorHelpProps, DataQuery } from '@grafana/data';
const examples = [
{
title: 'Addition',
expression: '1 + 2',
label: 'Add two integers',
},
{
title: 'Subtraction',
expression: '2 - 1',
label: 'Subtract an integer from another',
},
];
export default (props: QueryEditorHelpProps) => {
return (
<div>
<h2>Cheat Sheet</h2>
{examples.map((item, index) => (
<div className="cheat-sheet-item" key={index}>
<div className="cheat-sheet-item__title">{item.title}</div>
{item.expression ? (
<div
className="cheat-sheet-item__example"
onClick={e => props.onClickExample({ refId: 'A', queryText: item.expression } as DataQuery)}
>
<code>{item.expression}</code>
</div>
) : null}
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}
</div>
);
};
```
......@@ -10,7 +10,7 @@ This guide assumes that you're already familiar with how to [Build a data source
With Explore, users can make ad-hoc queries without the use of a dashboard. This is useful when users want to troubleshoot or to learn more about the data.
Your data source already supports Explore by default, and will use the existing query editor for the data source. If you want to offer extended Explore functionality for your data source however, you can define a Explore-specific query editor. Optionally, your plugin can also define a _start page_ for Explore.
Your data source already supports Explore by default, and will use the existing query editor for the data source. If you want to offer extended Explore functionality for your data source however, you can define a Explore-specific query editor.
## Add a query editor for Explore
......@@ -85,79 +85,6 @@ The query editor for Explore is similar to the query editor for the data source
};
```
## Add a start page for Explore
By adding an Explore start page for your plugin, you can for example create "cheat sheets" with commonly used queries. When the user clicks on one of the examples, it automatically updates the query editor, and runs the query. It's a great way to increase productivity for your users.
1. Create a file `ExploreStartPage.tsx` in the `src` directory of your plugin, with the following content:
```ts
import React from 'react';
import { ExploreStartPageProps } from '@grafana/data';
export default (props: ExploreStartPageProps) => {
return (
<h2>My start page</h2>
);
};
```
1. Configure the plugin to use the `ExploreStartPage`.
```ts
import ExploreStartPage from './ExploreStartPage';
```
```ts
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(QueryEditor)
.setExploreQueryField(ExploreQueryEditor)
.setExploreStartPage(ExploreStartPage);
```
1. Create a few examples.
```ts
import React from 'react';
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
const examples = [
{
title: 'Addition',
expression: '1 + 2',
label: 'Add two integers',
},
{
title: 'Subtraction',
expression: '2 - 1',
label: 'Subtract an integer from another',
},
];
export default (props: ExploreStartPageProps) => {
return (
<div>
<h2>Cheat Sheet</h2>
{examples.map((item, index) => (
<div className="cheat-sheet-item" key={index}>
<div className="cheat-sheet-item__title">{item.title}</div>
{item.expression ? (
<div
className="cheat-sheet-item__example"
onClick={e => props.onClickExample({ refId: 'A', queryText: item.expression } as DataQuery)}
>
<code>{item.expression}</code>
</div>
) : null}
<div className="cheat-sheet-item__label">{item.label}</div>
</div>
))}
</div>
);
};
```
## Support multiple Explore modes
Explore lets you query any data source, regardless of whether it returns metrics or logs. You can change which type of query you want to make, by setting the _Explore mode_.
......
......@@ -118,6 +118,8 @@ You can:
| Icon | Description |
|:--:|:---|
| {{< docs-imagebox img="/img/docs/queries/query-editor-help-7-4.png" class="docs-image--no-shadow" max-width="30px" max-height="30px" >}} | Toggle query editor help. If supported by the data source, this will toggle displaying information on how to use its query editor, or provide quick
access to commonly-used queries. |
| {{< docs-imagebox img="/img/docs/queries/duplicate-query-icon-7-0.png" class="docs-image--no-shadow" max-width="30px" max-height="30px" >}} | Copy a query. Duplicating queries is useful when working with multiple complex queries that are similar and you want to either experiment with different variants or do minor alterations. |
| {{< docs-imagebox img="/img/docs/queries/hide-query-icon-7-0.png" class="docs-image--no-shadow" max-width="30px" max-height="30px" >}} | Hide a query. Grafana does not send hidden queries to the data source. |
| {{< docs-imagebox img="/img/docs/queries/remove-query-icon-7-0.png" class="docs-image--no-shadow" max-width="30px" max-height="30px" >}} | Remove a query. Removing a query permanently deletes it, but sometimes you can recover deleted queries by reverting to previously saved versions of the panel. |
......
......@@ -75,11 +75,18 @@ export class DataSourcePlugin<
return this;
}
setExploreStartPage(ExploreStartPage: ComponentType<ExploreStartPageProps>) {
this.components.ExploreStartPage = ExploreStartPage;
setQueryEditorHelp(QueryEditorHelp: ComponentType<QueryEditorHelpProps>) {
this.components.QueryEditorHelp = QueryEditorHelp;
return this;
}
/**
* @deprecated prefer using `setQueryEditorHelp`
*/
setExploreStartPage(ExploreStartPage: ComponentType<QueryEditorHelpProps>) {
return this.setQueryEditorHelp(ExploreStartPage);
}
/*
* @deprecated -- prefer using {@link StandardVariableSupport} or {@link CustomVariableSupport} or {@link DataSourceVariableSupport} in data source instead
* */
......@@ -99,8 +106,8 @@ export class DataSourcePlugin<
this.components.QueryCtrl = pluginExports.QueryCtrl;
this.components.AnnotationsQueryCtrl = pluginExports.AnnotationsQueryCtrl;
this.components.ExploreQueryField = pluginExports.ExploreQueryField;
this.components.ExploreStartPage = pluginExports.ExploreStartPage;
this.components.QueryEditor = pluginExports.QueryEditor;
this.components.QueryEditorHelp = pluginExports.QueryEditorHelp;
this.components.VariableQueryEditor = pluginExports.VariableQueryEditor;
}
}
......@@ -140,7 +147,7 @@ export interface DataSourcePluginComponents<
ExploreQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
QueryEditorHelp?: ComponentType<QueryEditorHelpProps>;
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>;
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
}
......@@ -365,7 +372,7 @@ export interface ExploreQueryFieldProps<
exploreId?: any;
}
export interface ExploreStartPageProps {
export interface QueryEditorHelpProps {
datasource: DataSourceApi;
onClickExample: (query: DataQuery) => void;
exploreId?: any;
......
......@@ -16,7 +16,7 @@ const dummyProps: ExploreProps = {
logs: true,
},
components: {
ExploreStartPage: {},
QueryEditorHelp: {},
},
} as DataSourceApi,
datasourceMissing: false,
......
......@@ -382,8 +382,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const { openDrawer } = this.state;
const exploreClass = split ? 'explore explore-split' : 'explore';
const styles = getStyles(theme);
const StartPage = datasourceInstance?.components?.ExploreStartPage;
const showStartPage = !queryResponse || queryResponse.state === LoadingState.NotStarted;
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
// gets an error without a refID, so non-query-row-related error, like a connection error
const queryErrors =
......@@ -423,16 +422,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
return (
<main className={cx(styles.exploreMain)} style={{ width }}>
<ErrorBoundaryAlert>
{showStartPage && StartPage && (
<div className={'grafana-info-box grafana-info-box--max-lg'}>
<StartPage
onClickExample={this.onClickExample}
datasource={datasourceInstance}
exploreId={exploreId}
/>
</div>
)}
{!showStartPage && (
{showPanels && (
<>
{showMetrics && graphResult && this.renderGraphPanel(width)}
{showTable && this.renderTablePanel(width)}
......
......@@ -26,6 +26,7 @@ import { ExploreItemState, ExploreId } from 'app/types/explore';
import { highlightLogsExpressionAction } from './state/explorePane';
import { ErrorContainer } from './ErrorContainer';
import { changeQuery, modifyQueries, removeQueryRowAction, runQueries } from './state/query';
import { HelpToggle } from '../query/components/HelpToggle';
interface PropsFromParent {
exploreId: ExploreId;
......@@ -119,8 +120,9 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
const ReactQueryEditor = this.setReactQueryEditor();
let QueryEditor: JSX.Element;
if (ReactQueryEditor) {
return (
QueryEditor = (
<ReactQueryEditor
datasource={datasourceInstance}
query={query}
......@@ -133,18 +135,31 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
exploreId={exploreId}
/>
);
} else {
QueryEditor = (
<AngularQueryEditor
error={queryErrors}
datasource={datasourceInstance}
onQueryChange={this.onChange}
onExecuteQuery={this.onRunQuery}
initialQuery={query}
exploreEvents={exploreEvents}
range={range}
textEditModeEnabled={this.state.textEditModeEnabled}
/>
);
}
const DatasourceCheatsheet = datasourceInstance.components?.QueryEditorHelp;
return (
<AngularQueryEditor
error={queryErrors}
datasource={datasourceInstance}
onQueryChange={this.onChange}
onExecuteQuery={this.onRunQuery}
initialQuery={query}
exploreEvents={exploreEvents}
range={range}
textEditModeEnabled={this.state.textEditModeEnabled}
/>
<>
{QueryEditor}
{DatasourceCheatsheet && (
<HelpToggle>
<DatasourceCheatsheet onClickExample={query => this.onChange(query)} datasource={datasourceInstance} />
</HelpToggle>
)}
</>
);
};
......
......@@ -12,7 +12,7 @@ describe('Datasource reducer', () => {
logs: true,
},
components: {
ExploreStartPage: StartPage,
QueryEditorHelp: StartPage,
},
} as DataSourceApi;
const queries: DataQuery[] = [];
......
import { GrafanaTheme } from '@grafana/data';
import { Icon, InfoBox, stylesFactory, useTheme } from '@grafana/ui';
import { css, cx } from 'emotion';
import React, { useState } from 'react';
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
infoBox: css`
margin-top: ${theme.spacing.xs};
`,
}));
export const HelpToggle: React.FunctionComponent = ({ children }) => {
const [isHelpVisible, setIsHelpVisible] = useState(false);
const theme = useTheme();
const styles = getStyles(theme);
return (
<>
<button className="gf-form-label query-keyword pointer" onClick={_ => setIsHelpVisible(!isHelpVisible)}>
Help
<Icon name={isHelpVisible ? 'angle-down' : 'angle-right'} />
</button>
{isHelpVisible && <InfoBox className={cx(styles.infoBox)}>{children}</InfoBox>}
</>
);
};
......@@ -6,7 +6,7 @@ import _ from 'lodash';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { ErrorBoundaryAlert, HorizontalGroup } from '@grafana/ui';
import { ErrorBoundaryAlert, HorizontalGroup, InfoBox } from '@grafana/ui';
import {
DataQuery,
DataSourceApi,
......@@ -48,6 +48,7 @@ interface State {
hasTextEditMode: boolean;
data?: PanelData;
isOpen?: boolean;
showingHelp: boolean;
}
export class QueryEditorRow extends PureComponent<Props, State> {
......@@ -60,6 +61,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
hasTextEditMode: false,
data: undefined,
isOpen: true,
showingHelp: false,
};
componentDidMount() {
......@@ -226,6 +228,20 @@ export class QueryEditorRow extends PureComponent<Props, State> {
this.props.onRunQuery();
};
onToggleHelp = () => {
this.setState(state => ({
showingHelp: !state.showingHelp,
}));
};
onClickExample = (query: DataQuery) => {
this.props.onChange({
...query,
refId: this.props.query.refId,
});
this.onToggleHelp();
};
renderCollapsedText(): string | null {
const { datasource } = this.state;
if (datasource?.getQueryDisplayText) {
......@@ -240,11 +256,16 @@ export class QueryEditorRow extends PureComponent<Props, State> {
renderActions = (props: QueryOperationRowRenderProps) => {
const { query } = this.props;
const { hasTextEditMode } = this.state;
const { hasTextEditMode, datasource } = this.state;
const isDisabled = query.hide;
const hasEditorHelp = datasource?.components?.QueryEditorHelp;
return (
<HorizontalGroup width="auto">
{hasEditorHelp && (
<QueryOperationAction title="Toggle data source help" icon="question-circle" onClick={this.onToggleHelp} />
)}
{hasTextEditMode && (
<QueryOperationAction
title="Toggle text edit mode"
......@@ -286,7 +307,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
render() {
const { query, id, index } = this.props;
const { datasource } = this.state;
const { datasource, showingHelp } = this.state;
const isDisabled = query.hide;
const rowClasses = classNames('query-editor-row', {
......@@ -299,6 +320,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}
const editor = this.renderPluginEditor();
const DatasourceCheatsheet = datasource.components?.QueryEditorHelp;
return (
<div aria-label={selectors.components.QueryEditorRows.rows}>
......@@ -311,7 +333,14 @@ export class QueryEditorRow extends PureComponent<Props, State> {
onOpen={this.onOpen}
>
<div className={rowClasses}>
<ErrorBoundaryAlert>{editor}</ErrorBoundaryAlert>
<ErrorBoundaryAlert>
{showingHelp && DatasourceCheatsheet && (
<InfoBox onDismiss={this.onToggleHelp}>
<DatasourceCheatsheet onClickExample={query => this.onClickExample(query)} datasource={datasource} />
</InfoBox>
)}
{editor}
</ErrorBoundaryAlert>
</div>
</QueryOperationRow>
</div>
......
import React, { PureComponent } from 'react';
import { stripIndent, stripIndents } from 'common-tags';
import { ExploreStartPageProps } from '@grafana/data';
import { QueryEditorHelpProps } from '@grafana/data';
import Prism from 'prismjs';
import tokenizer from '../syntax';
import { flattenTokens } from '@grafana/ui/src/slate-plugins/slate-prism';
......@@ -214,7 +214,7 @@ const exampleCategory = css`
margin-top: 5px;
`;
export default class LogsCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> {
export default class LogsCheatSheet extends PureComponent<QueryEditorHelpProps, { userExamples: string[] }> {
onClickExample(query: CloudWatchLogsQuery) {
this.props.onClickExample(query);
}
......
......@@ -11,7 +11,7 @@ import LogsCheatSheet from './components/LogsCheatSheet';
export const plugin = new DataSourcePlugin<CloudWatchDatasource, CloudWatchQuery, CloudWatchJsonData>(
CloudWatchDatasource
)
.setExploreStartPage(LogsCheatSheet)
.setQueryEditorHelp(LogsCheatSheet)
.setConfigEditor(ConfigEditor)
.setQueryEditor(PanelQueryEditor)
.setExploreMetricsQueryField(PanelQueryEditor)
......
import React, { PureComponent } from 'react';
import { ExploreStartPageProps } from '@grafana/data';
import { QueryEditorHelpProps } from '@grafana/data';
import InfluxCheatSheet from './InfluxCheatSheet';
export default class InfluxStartPage extends PureComponent<ExploreStartPageProps> {
export default class InfluxStartPage extends PureComponent<QueryEditorHelpProps> {
render() {
return <InfluxCheatSheet onClickExample={this.props.onClickExample} />;
}
......
......@@ -17,4 +17,4 @@ export const plugin = new DataSourcePlugin(InfluxDatasource)
.setQueryCtrl(InfluxQueryCtrl)
.setAnnotationQueryCtrl(InfluxAnnotationsQueryCtrl)
.setVariableQueryEditor(VariableQueryEditor)
.setExploreStartPage(InfluxStartPage);
.setQueryEditorHelp(InfluxStartPage);
import React, { PureComponent } from 'react';
import { shuffle } from 'lodash';
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
import { QueryEditorHelpProps, DataQuery } from '@grafana/data';
import LokiLanguageProvider from '../language_provider';
const DEFAULT_EXAMPLES = ['{job="default/prometheus"}'];
......@@ -32,7 +32,7 @@ const LOGQL_EXAMPLES = [
},
];
export default class LokiCheatSheet extends PureComponent<ExploreStartPageProps, { userExamples: string[] }> {
export default class LokiCheatSheet extends PureComponent<QueryEditorHelpProps, { userExamples: string[] }> {
userLabelTimer: NodeJS.Timeout;
state = {
userExamples: DEFAULT_EXAMPLES,
......
......@@ -11,5 +11,5 @@ export const plugin = new DataSourcePlugin(Datasource)
.setQueryEditor(LokiQueryEditor)
.setConfigEditor(ConfigEditor)
.setExploreQueryField(LokiExploreQueryEditor)
.setExploreStartPage(LokiCheatSheet)
.setQueryEditorHelp(LokiCheatSheet)
.setAnnotationQueryCtrl(LokiAnnotationsQueryCtrl);
import React from 'react';
import { ExploreStartPageProps, DataQuery } from '@grafana/data';
import { QueryEditorHelpProps, DataQuery } from '@grafana/data';
const CHEAT_SHEET_ITEMS = [
{
......@@ -25,7 +25,7 @@ const CHEAT_SHEET_ITEMS = [
},
];
const PromCheatSheet = (props: ExploreStartPageProps) => (
const PromCheatSheet = (props: QueryEditorHelpProps) => (
<div>
<h2>PromQL Cheat Sheet</h2>
{CHEAT_SHEET_ITEMS.map((item, index) => (
......
......@@ -17,4 +17,4 @@ export const plugin = new DataSourcePlugin(PrometheusDatasource)
.setConfigEditor(ConfigEditor)
.setExploreMetricsQueryField(PromExploreQueryEditor)
.setAnnotationQueryCtrl(PrometheusAnnotationsQueryCtrl)
.setExploreStartPage(PromCheatSheet);
.setQueryEditorHelp(PromCheatSheet);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment