QueryTab: Design updates (#23906)

* WIP: first stage

* Another take

* argghhh

* Updated

* My brain is mush

* Minor progress

* Progres

* Starting to work

* Fixes

* fixed e2e
This commit is contained in:
Torkel Ödegaard 2020-04-26 21:59:14 +02:00 committed by GitHub
parent b9a40fc346
commit 49276f2c12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 465 additions and 469 deletions

View File

@ -27,7 +27,7 @@ e2e.scenario({
e2e.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST);
e2e.components.QueryEditorToolbarItem.button('Query inspector')
e2e.components.QueryTab.queryInspectorButton()
.should('be.visible')
.click();

View File

@ -77,6 +77,7 @@ export const Components = {
QueryTab: componentFactory({
selectors: {
content: 'Query editor tab content',
queryInspectorButton: 'Query inspector button',
},
}),
AlertTab: componentFactory({

View File

@ -13,7 +13,7 @@ export interface Props {
title?: ReactNode;
/** Subtitle shown below the title */
subtitle?: ReactNode;
/** Should the Drawer be closable by clicking on the mask */
/** Should the Drawer be closable by clicking on the mask, defaults to true */
closeOnMaskClick?: boolean;
/** Render the drawer inside a container on the page */
inline?: boolean;
@ -70,7 +70,7 @@ export const Drawer: FC<Props> = ({
children,
inline = false,
onClose,
closeOnMaskClick = false,
closeOnMaskClick = true,
scrollableContent = false,
title,
subtitle,

View File

@ -3,7 +3,7 @@ import { GrafanaTheme } from '@grafana/data';
import { stylesFactory } from '../../themes';
export const getModalStyles = stylesFactory((theme: GrafanaTheme) => {
const backdropBackground = theme.colors.bg1;
const backdropBackground = theme.colors.bg3;
return {
modal: css`

View File

@ -329,12 +329,12 @@ export function SelectBase<T>({
zIndex: theme.zIndex.dropdown,
}),
//These are required for the menu positioning to function
menu: ({ top, bottom, width, position }: any) => ({
menu: ({ top, bottom, position }: any) => ({
top,
bottom,
width,
position,
marginBottom: !!bottom ? '10px' : '0',
'min-width': '100%',
zIndex: theme.zIndex.dropdown,
}),
container: () => ({

View File

@ -136,7 +136,7 @@ $divider-border-color: $gray-1;
$tight-form-func-bg: $dark-9;
$tight-form-func-highlight-bg: $dark-10;
$modal-backdrop-bg: ${theme.colors.bg1};
$modal-backdrop-bg: ${theme.colors.bg3};
$code-tag-bg: $dark-1;
$code-tag-border: $dark-9;

View File

@ -6,6 +6,7 @@ import { useUpdateEffect } from 'react-use';
interface QueryOperationRowProps {
title?: ((props: { isOpen: boolean }) => React.ReactNode) | React.ReactNode;
headerElement?: React.ReactNode;
actions?:
| ((props: { isOpen: boolean; openRow: () => void; closeRow: () => void }) => React.ReactNode)
| React.ReactNode;
@ -19,6 +20,7 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
children,
actions,
title,
headerElement,
onClose,
onOpen,
isOpen,
@ -64,6 +66,7 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
>
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
{title && <span className={styles.title}>{titleElement}</span>}
{headerElement}
</div>
{actions && actionsElement}
</HorizontalGroup>
@ -76,7 +79,7 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
return {
wrapper: css`
margin-bottom: ${theme.spacing.formSpacingBase * 2}px;
margin-bottom: ${theme.spacing.md};
`,
header: css`
padding: 0 ${theme.spacing.sm};
@ -90,6 +93,9 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
`,
collapseIcon: css`
color: ${theme.colors.textWeak};
&:hover {
color: ${theme.colors.text};
}
`,
titleWrapper: css`
display: flex;

View File

@ -2,9 +2,8 @@
import React, { PureComponent } from 'react';
// Components
import { LegacyForms } from '@grafana/ui';
import { Select } from '@grafana/ui';
import { SelectableValue, DataSourceSelectItem } from '@grafana/data';
const { Select } = LegacyForms;
export interface Props {
onChange: (ds: DataSourceSelectItem) => void;
@ -66,23 +65,22 @@ export class DataSourcePicker extends PureComponent<Props> {
};
return (
<div className="gf-form-inline">
<Select
className="ds-picker"
isMulti={false}
isClearable={false}
backspaceRemovesValue={false}
onChange={this.onChange}
options={options}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={500}
placeholder={placeholder}
noOptionsMessage={() => 'No datasources found'}
value={value}
/>
</div>
<Select
className="ds-picker select-container"
isMulti={false}
isClearable={false}
backspaceRemovesValue={false}
onChange={this.onChange}
options={options}
autoFocus={autoFocus}
onBlur={onBlur}
openMenuOnFocus={openMenuOnFocus}
maxMenuHeight={500}
menuPlacement="bottom"
placeholder={placeholder}
noOptionsMessage="No datasources found"
value={value}
/>
);
}
}

View File

@ -205,7 +205,7 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
};
return (
<EditorTabBody heading="Alert" toolbarItems={toolbarItems}>
<EditorTabBody toolbarItems={toolbarItems}>
<div aria-label={e2e.components.AlertTab.selectors.content}>
{alert && hasTransformations && (
<Alert

View File

@ -2,244 +2,235 @@
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
<icon name="'exclamation-triangle'"></icon> {{ctrl.error}}
</div>
<div class="panel-options-group">
<div class="panel-options-group__body">
<div class="gf-form-group">
<h4 class="section-heading">Rule</h4>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name" />
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Evaluate every</span>
<input
class="gf-form-input max-width-6"
type="text"
ng-model="ctrl.alert.frequency"
ng-blur="ctrl.checkFrequency()"
/>
</div>
<div class="gf-form max-width-11">
<label class="gf-form-label width-5">For</label>
<input
type="text"
class="gf-form-input max-width-6 gf-form-input--has-help-icon"
ng-model="ctrl.alert.for"
spellcheck="false"
placeholder="5m"
/>
<info-popover mode="right-absolute">
If an alert rule has a configured For and the query violates the configured threshold it will first go
from OK to Pending. Going from OK to Pending Grafana will not send any notifications. Once the alert rule
has been firing for more than For duration, it will change to Alerting and send alert notifications.
</info-popover>
</div>
</div>
<div class="gf-form" ng-if="ctrl.frequencyWarning">
<label class="gf-form-label text-warning">
<icon name="'exclamation-triangle'"></icon> {{ctrl.frequencyWarning}}
</label>
<div class="gf-form-group">
<h4 class="section-heading">Rule</h4>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-6">Name</span>
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name" />
</div>
<div class="gf-form">
<span class="gf-form-label width-9">Evaluate every</span>
<input
class="gf-form-input max-width-6"
type="text"
ng-model="ctrl.alert.frequency"
ng-blur="ctrl.checkFrequency()"
/>
</div>
<div class="gf-form max-width-11">
<label class="gf-form-label width-5">For</label>
<input
type="text"
class="gf-form-input max-width-6 gf-form-input--has-help-icon"
ng-model="ctrl.alert.for"
spellcheck="false"
placeholder="5m"
/>
<info-popover mode="right-absolute">
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK
to Pending. Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been
firing for more than For duration, it will change to Alerting and send alert notifications.
</info-popover>
</div>
</div>
<div class="gf-form" ng-if="ctrl.frequencyWarning">
<label class="gf-form-label text-warning">
<icon name="'exclamation-triangle'"></icon> {{ctrl.frequencyWarning}}
</label>
</div>
</div>
<div class="gf-form-group">
<h4 class="section-heading">Conditions</h4>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<metric-segment-model
css-class="query-keyword width-5"
ng-if="$index"
property="conditionModel.operator.type"
options="ctrl.evalOperators"
custom="false"
></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part width-9"
part="conditionModel.reducerPart"
handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)"
>
</query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div>
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part"
part="conditionModel.queryPart"
handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)"
>
</query-part-editor>
</div>
<div class="gf-form">
<metric-segment-model
property="conditionModel.evaluator.type"
options="ctrl.evalFunctions"
custom="false"
css-class="query-keyword"
on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"
></metric-segment-model>
<input
class="gf-form-input max-width-9"
type="number"
step="any"
ng-hide="conditionModel.evaluator.params.length === 0"
ng-model="conditionModel.evaluator.params[0]"
ng-change="ctrl.evaluatorParamsChanged()"
/>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input
class="gf-form-input max-width-9"
type="number"
step="any"
ng-if="conditionModel.evaluator.params.length === 2"
ng-model="conditionModel.evaluator.params[1]"
ng-change="ctrl.evaluatorParamsChanged()"
/>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
<icon name="'trash-alt'"></icon>
</a>
</label>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown">
<icon name="'plus-circle'"></icon>
</a>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
</li>
</ul>
</label>
</div>
</div>
<div class="gf-form-group">
<h4 class="section-heading">No Data & Error Handling</h4>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-15">If no data or all values are null</span>
</div>
<div class="gf-form">
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input"
ng-model="ctrl.alert.noDataState"
ng-options="f.value as f.text for f in ctrl.noDataModes"
>
</select>
</div>
</div>
</div>
<div class="gf-form-group">
<h4 class="section-heading">Conditions</h4>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<metric-segment-model
css-class="query-keyword width-5"
ng-if="$index"
property="conditionModel.operator.type"
options="ctrl.evalOperators"
custom="false"
></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part width-9"
part="conditionModel.reducerPart"
handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)"
>
</query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div>
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part"
part="conditionModel.queryPart"
handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)"
>
</query-part-editor>
</div>
<div class="gf-form">
<metric-segment-model
property="conditionModel.evaluator.type"
options="ctrl.evalFunctions"
custom="false"
css-class="query-keyword"
on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"
></metric-segment-model>
<input
class="gf-form-input max-width-9"
type="number"
step="any"
ng-hide="conditionModel.evaluator.params.length === 0"
ng-model="conditionModel.evaluator.params[0]"
ng-change="ctrl.evaluatorParamsChanged()"
/>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input
class="gf-form-input max-width-9"
type="number"
step="any"
ng-if="conditionModel.evaluator.params.length === 2"
ng-model="conditionModel.evaluator.params[1]"
ng-change="ctrl.evaluatorParamsChanged()"
/>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
<icon name="'trash-alt'"></icon>
</a>
</label>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown">
<icon name="'plus-circle'"></icon>
</a>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
</li>
</ul>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-15">If execution error or timeout</span>
</div>
<div class="gf-form-group">
<h4 class="section-heading">No Data & Error Handling</h4>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-15">If no data or all values are null</span>
</div>
<div class="gf-form">
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input"
ng-model="ctrl.alert.noDataState"
ng-options="f.value as f.text for f in ctrl.noDataModes"
>
</select>
</div>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-15">If execution error or timeout</span>
</div>
<div class="gf-form">
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input"
ng-model="ctrl.alert.executionErrorState"
ng-options="f.value as f.text for f in ctrl.executionErrorModes"
>
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label query-keyword">SET STATE TO</span>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input"
ng-model="ctrl.alert.executionErrorState"
ng-options="f.value as f.text for f in ctrl.executionErrorModes"
>
</select>
</div>
</div>
</div>
</div>
<div class="panel-options-group">
<div class="panel-options-group__header">Notifications</div>
<div class="panel-options-group__body">
<h4 class="section-heading">Notifications</h4>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Send to</span>
</div>
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
<span class="gf-form-label">
<icon name="'{{nc.iconClass}}'"></icon>
&nbsp;{{nc.name}}&nbsp;<span ng-if="nc.isDefault">(default)</span>
<icon
name="'times'"
class="pointer muted"
ng-click="ctrl.removeNotification(nc)"
ng-if="nc.isDefault === false"
></icon>
</span>
</div>
<div class="gf-form">
<metric-segment
segment="ctrl.addNotificationSegment"
get-options="ctrl.getNotifications()"
on-change="ctrl.notificationAdded()"
></metric-segment>
</div>
</div>
<div class="gf-form gf-form--v-stretch">
<span class="gf-form-label width-8">Message</span>
<textarea
class="gf-form-input"
rows="10"
ng-model="ctrl.alert.message"
placeholder="Notification message details..."
></textarea>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Tags</span>
<div class="gf-form-group">
<div class="gf-form-inline" ng-repeat="(name, value) in ctrl.alert.alertRuleTags">
<label class="gf-form-label width-15">{{ name }}</label>
<input
class="gf-form-input width-15"
placeholder="Tag value..."
ng-model="ctrl.alert.alertRuleTags[name]"
type="text"
/>
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeAlertRuleTag(name)">
<icon name="'trash-alt'"></icon>
</a>
</label>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-8">Send to</span>
<input
class="gf-form-input width-15"
placeholder="New tag name..."
ng-model="ctrl.newAlertRuleTag.name"
type="text"
/>
<input
class="gf-form-input width-15"
placeholder="New tag value..."
ng-model="ctrl.newAlertRuleTag.value"
type="text"
/>
</div>
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
<span class="gf-form-label">
<icon name="'{{nc.iconClass}}'"></icon>
&nbsp;{{nc.name}}&nbsp;<span ng-if="nc.isDefault">(default)</span>
<icon
name="'times'"
class="pointer muted"
ng-click="ctrl.removeNotification(nc)"
ng-if="nc.isDefault === false"
></icon>
</span>
</div>
<div class="gf-form">
<metric-segment
segment="ctrl.addNotificationSegment"
get-options="ctrl.getNotifications()"
on-change="ctrl.notificationAdded()"
></metric-segment>
</div>
</div>
<div class="gf-form gf-form--v-stretch">
<span class="gf-form-label width-8">Message</span>
<textarea
class="gf-form-input"
rows="10"
ng-model="ctrl.alert.message"
placeholder="Notification message details..."
></textarea>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Tags</span>
<div class="gf-form-group">
<div class="gf-form-inline" ng-repeat="(name, value) in ctrl.alert.alertRuleTags">
<label class="gf-form-label width-15">{{ name }}</label>
<input
class="gf-form-input width-15"
placeholder="Tag value..."
ng-model="ctrl.alert.alertRuleTags[name]"
type="text"
/>
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.removeAlertRuleTag(name)">
<icon name="'trash-alt'"></icon>
</a>
</label>
</div>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<input
class="gf-form-input width-15"
placeholder="New tag name..."
ng-model="ctrl.newAlertRuleTag.name"
type="text"
/>
<input
class="gf-form-input width-15"
placeholder="New tag value..."
ng-model="ctrl.newAlertRuleTag.value"
type="text"
/>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.addAlertRuleTag()">
<icon name="'plus-circle'"></icon>&nbsp;Add Tag
</a>
</label>
</div>
</div>
</div>
<label class="gf-form-label">
<a class="pointer" tabindex="1" ng-click="ctrl.addAlertRuleTag()">
<icon name="'plus-circle'"></icon>&nbsp;Add Tag
</a>
</label>
</div>
</div>
</div>

View File

@ -15,7 +15,9 @@ interface Props {
export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
return (
<div className="gf-form gf-form--flex-end">
<InlineFormLabel tooltip={tooltipInfo}>{label}</InlineFormLabel>
<InlineFormLabel width={9} tooltip={tooltipInfo}>
{label}
</InlineFormLabel>
<Input
type="text"
className="gf-form-input width-6"

View File

@ -7,7 +7,6 @@ import { e2e } from '@grafana/e2e';
interface Props {
children: JSX.Element;
heading: string;
renderToolbar?: () => JSX.Element;
toolbarItems?: EditorToolbarView[];
scrollTop?: number;
@ -110,16 +109,13 @@ export class EditorTabBody extends PureComponent<Props, State> {
}
render() {
const { children, renderToolbar, heading, toolbarItems, scrollTop, setScrollTop } = this.props;
const { children, renderToolbar, toolbarItems, scrollTop, setScrollTop } = this.props;
const { openView, fadeIn, isOpen } = this.state;
return (
<>
<div className="toolbar">
<div className="toolbar__left">
<div className="toolbar__heading">{heading}</div>
{renderToolbar && renderToolbar()}
</div>
{renderToolbar && renderToolbar()}
{toolbarItems.map(item => this.renderButton(item))}
</div>
<div className="panel-editor__scroll">

View File

@ -1,10 +1,9 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup } from '@grafana/ui';
import { CustomScrollbar, stylesFactory, Button, HorizontalGroup, Modal } from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime';
import { QueryEditorRows } from './QueryEditorRows';
// Services
@ -20,6 +19,7 @@ import { addQuery } from 'app/core/utils/query';
import { Unsubscribable } from 'rxjs';
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
import { css } from 'emotion';
import { e2e } from '@grafana/e2e';
interface Props {
@ -35,6 +35,7 @@ interface State {
isAddingMixed: boolean;
scrollTop: number;
data: PanelData;
isHelpOpen: boolean;
}
export class QueriesTab extends PureComponent<Props, State> {
@ -48,6 +49,7 @@ export class QueriesTab extends PureComponent<Props, State> {
helpContent: null,
isPickerOpen: false,
isAddingMixed: false,
isHelpOpen: false,
scrollTop: 0,
data: {
state: LoadingState.NotStarted,
@ -121,6 +123,7 @@ export class QueriesTab extends PureComponent<Props, State> {
openQueryInspector = () => {
const { panel } = this.props;
getLocationSrv().update({
query: { inspect: panel.id, inspectTab: 'query' },
partial: true,
@ -128,7 +131,7 @@ export class QueriesTab extends PureComponent<Props, State> {
};
renderHelp = () => {
return <PluginHelp plugin={this.state.currentDS.meta} type="query_help" />;
return;
};
/**
@ -155,30 +158,45 @@ export class QueriesTab extends PureComponent<Props, State> {
};
onScrollBottom = () => {
this.setState({ scrollTop: this.state.scrollTop + 10000 });
this.setState({ scrollTop: 1000 });
};
renderToolbar = () => {
const { currentDS, isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name));
renderTopSection(styles: QueriesTabStyls) {
const { panel } = this.props;
const { currentDS, data } = this.state;
return (
<>
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
<div className="flex-grow-1" />
{showAddButton && (
<button className="btn navbar-button" onClick={this.onAddQueryClick}>
Add query
</button>
)}
{isAddingMixed && this.renderMixedPicker()}
{config.featureToggles.expressions && (
<button className="btn navbar-button" onClick={this.onAddExpressionClick}>
Add Expression
</button>
)}
</>
<div>
<div className={styles.dataSourceRow}>
<div className={styles.dataSourceRowItem}>
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
</div>
<div className={styles.dataSourceRowItem}>
<Button variant="secondary" icon="info-circle" title="Open data source help" onClick={this.onOpenHelp} />
</div>
<div className={styles.dataSourceRowItem}>
<Button
variant="secondary"
onClick={this.openQueryInspector}
aria-label={e2e.components.QueryTab.selectors.queryInspectorButton}
>
Query inspector
</Button>
</div>
<div className={styles.dataSourceRowItemOptions}>
<QueryOptions panel={panel} datasource={currentDS} data={data} />
</div>
</div>
</div>
);
}
onOpenHelp = () => {
this.setState({ isHelpOpen: true });
};
onCloseHelp = () => {
this.setState({ isHelpOpen: false });
};
renderMixedPicker = () => {
@ -218,7 +236,7 @@ export class QueriesTab extends PureComponent<Props, State> {
this.setState({ scrollTop: target.scrollTop });
};
renderQueryBody = () => {
renderQueries() {
const { panel, dashboard } = this.props;
const { currentDS, data } = this.state;
@ -237,36 +255,83 @@ export class QueriesTab extends PureComponent<Props, State> {
dashboard={dashboard}
data={data}
/>
<PanelOptionsGroup>
<QueryOptions panel={panel} datasource={currentDS} />
</PanelOptionsGroup>
</div>
);
};
}
render() {
const { scrollTop } = this.state;
const queryInspector: EditorToolbarView = {
title: 'Query inspector',
onClick: this.openQueryInspector,
};
const dsHelp: EditorToolbarView = {
heading: 'Help',
icon: 'question-circle',
render: this.renderHelp,
};
renderAddQueryRow(styles: QueriesTabStyls) {
const { currentDS, isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name));
return (
<EditorTabBody
heading="Data source"
renderToolbar={this.renderToolbar}
toolbarItems={[queryInspector, dsHelp]}
setScrollTop={this.setScrollTop}
<HorizontalGroup spacing="md" align="flex-start">
{showAddButton && (
<Button icon="plus" onClick={this.onAddQueryClick} variant="secondary">
Query
</Button>
)}
{isAddingMixed && this.renderMixedPicker()}
{config.featureToggles.expressions && (
<Button icon="plus" onClick={this.onAddExpressionClick} variant="secondary">
Expression
</Button>
)}
</HorizontalGroup>
);
}
render() {
const { scrollTop, isHelpOpen } = this.state;
const styles = getStyles();
return (
<CustomScrollbar
autoHeightMin="100%"
autoHide={true}
updateAfterMountMs={300}
scrollTop={scrollTop}
setScrollTop={this.setScrollTop}
>
<>{this.renderQueryBody()}</>
</EditorTabBody>
<div className={styles.innerWrapper}>
{this.renderTopSection(styles)}
<div className={styles.queriesWrapper}>{this.renderQueries()}</div>
{this.renderAddQueryRow(styles)}
{isHelpOpen && (
<Modal title="Data source help" isOpen={true} onDismiss={this.onCloseHelp}>
<PluginHelp plugin={this.state.currentDS.meta} type="query_help" />
</Modal>
)}
</div>
</CustomScrollbar>
);
}
}
const getStyles = stylesFactory(() => {
const { theme } = config;
return {
innerWrapper: css`
display: flex;
flex-direction: column;
height: 100%;
padding: ${theme.spacing.md};
`,
dataSourceRow: css`
display: flex;
margin-bottom: ${theme.spacing.md};
`,
dataSourceRowItem: css`
margin-right: ${theme.spacing.inlineFormMargin};
`,
dataSourceRowItemOptions: css`
flex-grow: 1;
`,
queriesWrapper: css`
padding-bottom: 16px;
`,
};
});
type QueriesTabStyls = ReturnType<typeof getStyles>;

View File

@ -73,24 +73,20 @@ export class QueryEditorRows extends PureComponent<Props> {
render() {
const { props } = this;
return (
<div className="query-editor-rows">
{props.queries.map((query, index) => (
<QueryEditorRow
dataSourceValue={query.datasource || props.datasource.value}
key={query.refId}
panel={props.panel}
dashboard={props.dashboard}
data={props.data}
query={query}
onChange={query => this.onChangeQuery(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
onMoveQuery={this.onMoveQuery}
inMixedMode={props.datasource.meta.mixed}
/>
))}
</div>
);
return props.queries.map((query, index) => (
<QueryEditorRow
dataSourceValue={query.datasource || props.datasource.value}
key={query.refId}
panel={props.panel}
dashboard={props.dashboard}
data={props.data}
query={query}
onChange={query => this.onChangeQuery(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
onMoveQuery={this.onMoveQuery}
inMixedMode={props.datasource.meta.mixed}
/>
));
}
}

View File

@ -2,15 +2,25 @@
import React, { PureComponent, ChangeEvent, FocusEvent, ReactText } from 'react';
// Utils
import { rangeUtil, DataSourceSelectItem } from '@grafana/data';
import { rangeUtil, DataSourceSelectItem, PanelData } from '@grafana/data';
// Components
import { EventsWithValidation, LegacyInputStatus, LegacyForms, ValidationEvents, InlineFormLabel } from '@grafana/ui';
import {
EventsWithValidation,
LegacyInputStatus,
LegacyForms,
ValidationEvents,
InlineFormLabel,
stylesFactory,
} from '@grafana/ui';
import { DataSourceOption } from './DataSourceOption';
const { Input, Switch } = LegacyForms;
// Types
import { PanelModel } from '../state';
import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOperationRow';
import { config } from 'app/core/config';
import { css } from 'emotion';
const timeRangeValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [
@ -33,6 +43,7 @@ const emptyToNull = (value: string) => {
interface Props {
panel: PanelModel;
datasource: DataSourceSelectItem;
data: PanelData;
}
interface State {
@ -42,6 +53,7 @@ interface State {
maxDataPoints: string | ReactText;
interval: string;
hideTimeOverride: boolean;
isOpen: boolean;
}
export class QueryOptions extends PureComponent<Props, State> {
@ -95,6 +107,7 @@ export class QueryOptions extends PureComponent<Props, State> {
maxDataPoints: props.panel.maxDataPoints || '',
interval: props.panel.interval || '',
hideTimeOverride: props.panel.hideTimeOverride || false,
isOpen: false,
};
}
@ -180,15 +193,57 @@ export class QueryOptions extends PureComponent<Props, State> {
});
};
onOpenOptions = () => {
this.setState({ isOpen: true });
};
onCloseOptions = () => {
this.setState({ isOpen: false });
};
renderCollapsedText(styles: StylesType): React.ReactNode | undefined {
const { data } = this.props;
const { isOpen, maxDataPoints, interval } = this.state;
if (isOpen) {
return undefined;
}
let mdDesc = maxDataPoints;
if (maxDataPoints === '' && data.request) {
mdDesc = `auto = ${data.request.maxDataPoints}`;
}
let intervalDesc = interval;
if (intervalDesc === '' && data.request) {
intervalDesc = `auto = ${data.request.interval}`;
}
return (
<>
{<div className={styles.collapsedText}>MD = {mdDesc}</div>}
{<div className={styles.collapsedText}>Interval = {intervalDesc}</div>}
</>
);
}
render() {
const { hideTimeOverride } = this.state;
const { relativeTime, timeShift } = this.state;
const { relativeTime, timeShift, isOpen } = this.state;
const styles = getStyles();
return (
<div className="gf-form-inline">
<QueryOperationRow
title="Options"
headerElement={this.renderCollapsedText(styles)}
isOpen={isOpen}
onOpen={this.onOpenOptions}
onClose={this.onCloseOptions}
>
{this.renderOptions()}
<div className="gf-form">
<InlineFormLabel>Relative time</InlineFormLabel>
<InlineFormLabel width={9}>Relative time</InlineFormLabel>
<Input
type="text"
className="width-6"
@ -202,7 +257,7 @@ export class QueryOptions extends PureComponent<Props, State> {
</div>
<div className="gf-form">
<span className="gf-form-label">Time shift</span>
<span className="gf-form-label width-9">Time shift</span>
<Input
type="text"
className="width-6"
@ -216,10 +271,29 @@ export class QueryOptions extends PureComponent<Props, State> {
</div>
{(timeShift || relativeTime) && (
<div className="gf-form-inline">
<Switch label="Hide time info" checked={hideTimeOverride} onChange={this.onToggleTimeOverride} />
<Switch
label="Hide time info"
labelClass="width-9"
checked={hideTimeOverride}
onChange={this.onToggleTimeOverride}
/>
</div>
)}
</div>
</QueryOperationRow>
);
}
}
const getStyles = stylesFactory(() => {
const { theme } = config;
return {
collapsedText: css`
margin-left: ${theme.spacing.md};
font-size: ${theme.typography.size.sm};
color: ${theme.colors.textWeak};
`,
};
});
type StylesType = ReturnType<typeof getStyles>;

View File

@ -138,7 +138,7 @@ $divider-border-color: $gray-1;
$tight-form-func-bg: $dark-9;
$tight-form-func-highlight-bg: $dark-10;
$modal-backdrop-bg: #141619;
$modal-backdrop-bg: #2c3235;
$code-tag-bg: $dark-1;
$code-tag-border: $dark-9;

View File

@ -109,6 +109,7 @@ $input-border: 1px solid $input-border-color;
font-size: $font-size-sm;
background-color: $input-label-bg;
height: $input-height;
line-height: $input-height;
margin-right: $space-xs;
border-radius: $input-border-radius;
justify-content: space-between;

View File

@ -1,9 +1,3 @@
.panel-editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-wrapper {
height: 100%;
position: relative;
@ -16,45 +10,6 @@
}
}
.panel-editor-container__editor {
margin-top: $space-lg;
display: flex;
flex-direction: row;
flex: 1 1 0;
position: relative;
min-height: 0;
}
.panel-editor__right {
display: flex;
flex-direction: column;
flex-grow: 1;
background: $input-bg;
margin: 0 20px 0 84px;
width: calc(100% - 84px);
border-radius: 3px;
box-shadow: $panel-editor-shadow;
min-height: 0;
}
.panel-editor__close {
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl);
position: absolute;
left: 11px;
top: 5px;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
i {
flex-grow: 1;
text-align: center;
font-size: 20px;
}
}
.panel-editor__scroll {
flex-grow: 1;
min-width: 0;
@ -65,7 +20,7 @@
}
.panel-editor__content {
padding: 16px;
padding: 0 16px 16px 16px;
}
.panel-in-fullscreen {
@ -86,88 +41,6 @@
}
}
.panel-editor-container__resizer {
position: relative;
margin-top: -3px;
}
.panel-editor-resizer__handle {
position: relative;
display: block;
background: $vertical-resize-handle-bg;
width: 150px;
margin-left: -75px;
height: 6px;
cursor: ns-resize;
border-radius: 3px;
margin: 0 auto;
&::before {
content: ' ';
position: absolute;
left: 10px;
right: 10px;
top: 2px;
border-top: 2px dotted $vertical-resize-handle-dots;
}
&:hover::before {
border-color: $vertical-resize-handle-dots-hover;
}
}
.panel-editor-tabs {
z-index: 2;
display: flex;
flex-direction: column;
position: absolute;
top: 44px;
left: 20px;
align-items: flex-start;
&::before {
content: '';
display: block;
position: absolute;
top: 10px;
bottom: 10px;
left: 21px;
width: 2px;
background: $panel-editor-tabs-line-color;
}
}
.panel-editor-tabs__item {
margin-bottom: 25px;
position: relative;
z-index: 1;
text-align: center;
&:last-child {
margin-bottom: 0;
}
}
.panel-editor-tabs__link {
display: inline-block;
&.active {
position: relative;
}
.gicon {
height: 44px;
width: 53px;
margin-right: 5px;
transition: transform 0.1s ease 0.1s;
&:hover {
filter: $panel-editor-side-menu-shadow;
transform: scale(1.1);
}
}
}
.ds-picker {
position: relative;
min-width: 200px;

View File

@ -11,10 +11,6 @@
color: $gray-2;
}
.query-editor-rows {
margin: 20px 0;
}
.tight-form-func {
background: $tight-form-func-bg;

View File

@ -2,12 +2,9 @@
display: flex;
align-content: center;
align-items: center;
padding: 3px 20px 3px 20px;
padding: 16px;
position: relative;
flex: 0 0 auto;
background: $toolbar-bg;
border-radius: 3px;
height: 44px;
}
.toolbar__heading {