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.flows.openPanelMenuItem(e2e.flows.PanelMenuItems.Edit, PANEL_UNDER_TEST);
e2e.components.QueryEditorToolbarItem.button('Query inspector') e2e.components.QueryTab.queryInspectorButton()
.should('be.visible') .should('be.visible')
.click(); .click();

View File

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

View File

@ -13,7 +13,7 @@ export interface Props {
title?: ReactNode; title?: ReactNode;
/** Subtitle shown below the title */ /** Subtitle shown below the title */
subtitle?: ReactNode; 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; closeOnMaskClick?: boolean;
/** Render the drawer inside a container on the page */ /** Render the drawer inside a container on the page */
inline?: boolean; inline?: boolean;
@ -70,7 +70,7 @@ export const Drawer: FC<Props> = ({
children, children,
inline = false, inline = false,
onClose, onClose,
closeOnMaskClick = false, closeOnMaskClick = true,
scrollableContent = false, scrollableContent = false,
title, title,
subtitle, subtitle,

View File

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

View File

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

View File

@ -136,7 +136,7 @@ $divider-border-color: $gray-1;
$tight-form-func-bg: $dark-9; $tight-form-func-bg: $dark-9;
$tight-form-func-highlight-bg: $dark-10; $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-bg: $dark-1;
$code-tag-border: $dark-9; $code-tag-border: $dark-9;

View File

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

View File

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

View File

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

View File

@ -2,244 +2,235 @@
<div class="alert alert-error m-b-2" ng-show="ctrl.error"> <div class="alert alert-error m-b-2" ng-show="ctrl.error">
<icon name="'exclamation-triangle'"></icon> {{ctrl.error}} <icon name="'exclamation-triangle'"></icon> {{ctrl.error}}
</div> </div>
<div class="panel-options-group">
<div class="panel-options-group__body"> <div class="gf-form-group">
<div class="gf-form-group"> <h4 class="section-heading">Rule</h4>
<h4 class="section-heading">Rule</h4> <div class="gf-form-inline">
<div class="gf-form-inline"> <div class="gf-form">
<div class="gf-form"> <span class="gf-form-label width-6">Name</span>
<span class="gf-form-label width-6">Name</span> <input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name" />
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name" /> </div>
</div> <div class="gf-form">
<div class="gf-form"> <span class="gf-form-label width-9">Evaluate every</span>
<span class="gf-form-label width-9">Evaluate every</span> <input
<input class="gf-form-input max-width-6"
class="gf-form-input max-width-6" type="text"
type="text" ng-model="ctrl.alert.frequency"
ng-model="ctrl.alert.frequency" ng-blur="ctrl.checkFrequency()"
ng-blur="ctrl.checkFrequency()" />
/> </div>
</div> <div class="gf-form max-width-11">
<div class="gf-form max-width-11"> <label class="gf-form-label width-5">For</label>
<label class="gf-form-label width-5">For</label> <input
<input type="text"
type="text" class="gf-form-input max-width-6 gf-form-input--has-help-icon"
class="gf-form-input max-width-6 gf-form-input--has-help-icon" ng-model="ctrl.alert.for"
ng-model="ctrl.alert.for" spellcheck="false"
spellcheck="false" placeholder="5m"
placeholder="5m" />
/> <info-popover mode="right-absolute">
<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
If an alert rule has a configured For and the query violates the configured threshold it will first go to Pending. Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been
from OK to Pending. Going from OK to Pending Grafana will not send any notifications. Once the alert rule firing for more than For duration, it will change to Alerting and send alert notifications.
has been firing for more than For duration, it will change to Alerting and send alert notifications. </info-popover>
</info-popover> </div>
</div> </div>
</div> <div class="gf-form" ng-if="ctrl.frequencyWarning">
<div class="gf-form" ng-if="ctrl.frequencyWarning"> <label class="gf-form-label text-warning">
<label class="gf-form-label text-warning"> <icon name="'exclamation-triangle'"></icon> {{ctrl.frequencyWarning}}
<icon name="'exclamation-triangle'"></icon> {{ctrl.frequencyWarning}} </label>
</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>
</div>
<div class="gf-form-group"> <div class="gf-form-inline">
<h4 class="section-heading">Conditions</h4> <div class="gf-form">
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels"> <span class="gf-form-label width-15">If execution error or timeout</span>
<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>
<div class="gf-form">
<div class="gf-form-group"> <span class="gf-form-label query-keyword">SET STATE TO</span>
<h4 class="section-heading">No Data & Error Handling</h4> <div class="gf-form-select-wrapper">
<div class="gf-form-inline"> <select
<div class="gf-form"> class="gf-form-input"
<span class="gf-form-label width-15">If no data or all values are null</span> ng-model="ctrl.alert.executionErrorState"
</div> ng-options="f.value as f.text for f in ctrl.executionErrorModes"
<div class="gf-form"> >
<span class="gf-form-label query-keyword">SET STATE TO</span> </select>
<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> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="panel-options-group"> <h4 class="section-heading">Notifications</h4>
<div class="panel-options-group__header">Notifications</div> <div class="gf-form-inline">
<div class="panel-options-group__body"> <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-inline">
<div class="gf-form"> <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>
<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>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-8">Tags</span> <label class="gf-form-label">
<div class="gf-form-group"> <a class="pointer" tabindex="1" ng-click="ctrl.addAlertRuleTag()">
<div class="gf-form-inline" ng-repeat="(name, value) in ctrl.alert.alertRuleTags"> <icon name="'plus-circle'"></icon>&nbsp;Add Tag
<label class="gf-form-label width-15">{{ name }}</label> </a>
<input </label>
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>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -1,10 +1,9 @@
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// Components // Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
import { QueryOptions } from './QueryOptions'; import { QueryOptions } from './QueryOptions';
import { PanelOptionsGroup } from '@grafana/ui'; import { CustomScrollbar, stylesFactory, Button, HorizontalGroup, Modal } from '@grafana/ui';
import { getLocationSrv } from '@grafana/runtime'; import { getLocationSrv } from '@grafana/runtime';
import { QueryEditorRows } from './QueryEditorRows'; import { QueryEditorRows } from './QueryEditorRows';
// Services // Services
@ -20,6 +19,7 @@ import { addQuery } from 'app/core/utils/query';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard'; import { DashboardQueryEditor, isSharedDashboardQuery } from 'app/plugins/datasource/dashboard';
import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource'; import { expressionDatasource, ExpressionDatasourceID } from 'app/features/expressions/ExpressionDatasource';
import { css } from 'emotion';
import { e2e } from '@grafana/e2e'; import { e2e } from '@grafana/e2e';
interface Props { interface Props {
@ -35,6 +35,7 @@ interface State {
isAddingMixed: boolean; isAddingMixed: boolean;
scrollTop: number; scrollTop: number;
data: PanelData; data: PanelData;
isHelpOpen: boolean;
} }
export class QueriesTab extends PureComponent<Props, State> { export class QueriesTab extends PureComponent<Props, State> {
@ -48,6 +49,7 @@ export class QueriesTab extends PureComponent<Props, State> {
helpContent: null, helpContent: null,
isPickerOpen: false, isPickerOpen: false,
isAddingMixed: false, isAddingMixed: false,
isHelpOpen: false,
scrollTop: 0, scrollTop: 0,
data: { data: {
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
@ -121,6 +123,7 @@ export class QueriesTab extends PureComponent<Props, State> {
openQueryInspector = () => { openQueryInspector = () => {
const { panel } = this.props; const { panel } = this.props;
getLocationSrv().update({ getLocationSrv().update({
query: { inspect: panel.id, inspectTab: 'query' }, query: { inspect: panel.id, inspectTab: 'query' },
partial: true, partial: true,
@ -128,7 +131,7 @@ export class QueriesTab extends PureComponent<Props, State> {
}; };
renderHelp = () => { renderHelp = () => {
return <PluginHelp plugin={this.state.currentDS.meta} type="query_help" />; return;
}; };
/** /**
@ -155,30 +158,45 @@ export class QueriesTab extends PureComponent<Props, State> {
}; };
onScrollBottom = () => { onScrollBottom = () => {
this.setState({ scrollTop: this.state.scrollTop + 10000 }); this.setState({ scrollTop: 1000 });
}; };
renderToolbar = () => { renderTopSection(styles: QueriesTabStyls) {
const { currentDS, isAddingMixed } = this.state; const { panel } = this.props;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name)); const { currentDS, data } = this.state;
return ( return (
<> <div>
<DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} /> <div className={styles.dataSourceRow}>
<div className="flex-grow-1" /> <div className={styles.dataSourceRowItem}>
{showAddButton && ( <DataSourcePicker datasources={this.datasources} onChange={this.onChangeDataSource} current={currentDS} />
<button className="btn navbar-button" onClick={this.onAddQueryClick}> </div>
Add query <div className={styles.dataSourceRowItem}>
</button> <Button variant="secondary" icon="info-circle" title="Open data source help" onClick={this.onOpenHelp} />
)} </div>
{isAddingMixed && this.renderMixedPicker()} <div className={styles.dataSourceRowItem}>
{config.featureToggles.expressions && ( <Button
<button className="btn navbar-button" onClick={this.onAddExpressionClick}> variant="secondary"
Add Expression onClick={this.openQueryInspector}
</button> 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 = () => { renderMixedPicker = () => {
@ -218,7 +236,7 @@ export class QueriesTab extends PureComponent<Props, State> {
this.setState({ scrollTop: target.scrollTop }); this.setState({ scrollTop: target.scrollTop });
}; };
renderQueryBody = () => { renderQueries() {
const { panel, dashboard } = this.props; const { panel, dashboard } = this.props;
const { currentDS, data } = this.state; const { currentDS, data } = this.state;
@ -237,36 +255,83 @@ export class QueriesTab extends PureComponent<Props, State> {
dashboard={dashboard} dashboard={dashboard}
data={data} data={data}
/> />
<PanelOptionsGroup>
<QueryOptions panel={panel} datasource={currentDS} />
</PanelOptionsGroup>
</div> </div>
); );
}; }
render() { renderAddQueryRow(styles: QueriesTabStyls) {
const { scrollTop } = this.state; const { currentDS, isAddingMixed } = this.state;
const queryInspector: EditorToolbarView = { const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name));
title: 'Query inspector',
onClick: this.openQueryInspector,
};
const dsHelp: EditorToolbarView = {
heading: 'Help',
icon: 'question-circle',
render: this.renderHelp,
};
return ( return (
<EditorTabBody <HorizontalGroup spacing="md" align="flex-start">
heading="Data source" {showAddButton && (
renderToolbar={this.renderToolbar} <Button icon="plus" onClick={this.onAddQueryClick} variant="secondary">
toolbarItems={[queryInspector, dsHelp]} Query
setScrollTop={this.setScrollTop} </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} scrollTop={scrollTop}
setScrollTop={this.setScrollTop}
> >
<>{this.renderQueryBody()}</> <div className={styles.innerWrapper}>
</EditorTabBody> {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() { render() {
const { props } = this; const { props } = this;
return ( return props.queries.map((query, index) => (
<div className="query-editor-rows"> <QueryEditorRow
{props.queries.map((query, index) => ( dataSourceValue={query.datasource || props.datasource.value}
<QueryEditorRow key={query.refId}
dataSourceValue={query.datasource || props.datasource.value} panel={props.panel}
key={query.refId} dashboard={props.dashboard}
panel={props.panel} data={props.data}
dashboard={props.dashboard} query={query}
data={props.data} onChange={query => this.onChangeQuery(query, index)}
query={query} onRemoveQuery={this.onRemoveQuery}
onChange={query => this.onChangeQuery(query, index)} onAddQuery={this.onAddQuery}
onRemoveQuery={this.onRemoveQuery} onMoveQuery={this.onMoveQuery}
onAddQuery={this.onAddQuery} inMixedMode={props.datasource.meta.mixed}
onMoveQuery={this.onMoveQuery} />
inMixedMode={props.datasource.meta.mixed} ));
/>
))}
</div>
);
} }
} }

View File

@ -2,15 +2,25 @@
import React, { PureComponent, ChangeEvent, FocusEvent, ReactText } from 'react'; import React, { PureComponent, ChangeEvent, FocusEvent, ReactText } from 'react';
// Utils // Utils
import { rangeUtil, DataSourceSelectItem } from '@grafana/data'; import { rangeUtil, DataSourceSelectItem, PanelData } from '@grafana/data';
// Components // Components
import { EventsWithValidation, LegacyInputStatus, LegacyForms, ValidationEvents, InlineFormLabel } from '@grafana/ui'; import {
EventsWithValidation,
LegacyInputStatus,
LegacyForms,
ValidationEvents,
InlineFormLabel,
stylesFactory,
} from '@grafana/ui';
import { DataSourceOption } from './DataSourceOption'; import { DataSourceOption } from './DataSourceOption';
const { Input, Switch } = LegacyForms; const { Input, Switch } = LegacyForms;
// Types // Types
import { PanelModel } from '../state'; 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 = { const timeRangeValidationEvents: ValidationEvents = {
[EventsWithValidation.onBlur]: [ [EventsWithValidation.onBlur]: [
@ -33,6 +43,7 @@ const emptyToNull = (value: string) => {
interface Props { interface Props {
panel: PanelModel; panel: PanelModel;
datasource: DataSourceSelectItem; datasource: DataSourceSelectItem;
data: PanelData;
} }
interface State { interface State {
@ -42,6 +53,7 @@ interface State {
maxDataPoints: string | ReactText; maxDataPoints: string | ReactText;
interval: string; interval: string;
hideTimeOverride: boolean; hideTimeOverride: boolean;
isOpen: boolean;
} }
export class QueryOptions extends PureComponent<Props, State> { export class QueryOptions extends PureComponent<Props, State> {
@ -95,6 +107,7 @@ export class QueryOptions extends PureComponent<Props, State> {
maxDataPoints: props.panel.maxDataPoints || '', maxDataPoints: props.panel.maxDataPoints || '',
interval: props.panel.interval || '', interval: props.panel.interval || '',
hideTimeOverride: props.panel.hideTimeOverride || false, 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() { render() {
const { hideTimeOverride } = this.state; const { hideTimeOverride } = this.state;
const { relativeTime, timeShift } = this.state; const { relativeTime, timeShift, isOpen } = this.state;
const styles = getStyles();
return ( return (
<div className="gf-form-inline"> <QueryOperationRow
title="Options"
headerElement={this.renderCollapsedText(styles)}
isOpen={isOpen}
onOpen={this.onOpenOptions}
onClose={this.onCloseOptions}
>
{this.renderOptions()} {this.renderOptions()}
<div className="gf-form"> <div className="gf-form">
<InlineFormLabel>Relative time</InlineFormLabel> <InlineFormLabel width={9}>Relative time</InlineFormLabel>
<Input <Input
type="text" type="text"
className="width-6" className="width-6"
@ -202,7 +257,7 @@ export class QueryOptions extends PureComponent<Props, State> {
</div> </div>
<div className="gf-form"> <div className="gf-form">
<span className="gf-form-label">Time shift</span> <span className="gf-form-label width-9">Time shift</span>
<Input <Input
type="text" type="text"
className="width-6" className="width-6"
@ -216,10 +271,29 @@ export class QueryOptions extends PureComponent<Props, State> {
</div> </div>
{(timeShift || relativeTime) && ( {(timeShift || relativeTime) && (
<div className="gf-form-inline"> <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>
)} )}
</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-bg: $dark-9;
$tight-form-func-highlight-bg: $dark-10; $tight-form-func-highlight-bg: $dark-10;
$modal-backdrop-bg: #141619; $modal-backdrop-bg: #2c3235;
$code-tag-bg: $dark-1; $code-tag-bg: $dark-1;
$code-tag-border: $dark-9; $code-tag-border: $dark-9;

View File

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

View File

@ -1,9 +1,3 @@
.panel-editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-wrapper { .panel-wrapper {
height: 100%; height: 100%;
position: relative; 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 { .panel-editor__scroll {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
@ -65,7 +20,7 @@
} }
.panel-editor__content { .panel-editor__content {
padding: 16px; padding: 0 16px 16px 16px;
} }
.panel-in-fullscreen { .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 { .ds-picker {
position: relative; position: relative;
min-width: 200px; min-width: 200px;

View File

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

View File

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