mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
b9a40fc346
commit
49276f2c12
@ -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();
|
||||||
|
|
||||||
|
@ -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({
|
||||||
|
@ -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,
|
||||||
|
@ -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`
|
||||||
|
@ -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: () => ({
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
{{nc.name}} <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>
|
|
||||||
{{nc.name}} <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> 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> Add Tag
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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"
|
||||||
|
@ -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">
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user