Commit 49276f2c by Torkel Ödegaard Committed by GitHub

QueryTab: Design updates (#23906)

* WIP: first stage

* Another take

* argghhh

* Updated

* My brain is mush

* Minor progress

* Progres

* Starting to work

* Fixes

* fixed e2e
parent b9a40fc3
......@@ -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();
......
......@@ -77,6 +77,7 @@ export const Components = {
QueryTab: componentFactory({
selectors: {
content: 'Query editor tab content',
queryInspectorButton: 'Query inspector button',
},
}),
AlertTab: componentFactory({
......
......@@ -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,
......
......@@ -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`
......
......@@ -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: () => ({
......
......@@ -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;
......
......@@ -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;
......
......@@ -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}
/>
);
}
}
......
......@@ -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
......
......@@ -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"
......
......@@ -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">
......
// 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>
);
};
}
renderAddQueryRow(styles: QueriesTabStyls) {
const { currentDS, isAddingMixed } = this.state;
const showAddButton = !(isAddingMixed || isSharedDashboardQuery(currentDS.name));
return (
<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 } = this.state;
const queryInspector: EditorToolbarView = {
title: 'Query inspector',
onClick: this.openQueryInspector,
};
const dsHelp: EditorToolbarView = {
heading: 'Help',
icon: 'question-circle',
render: this.renderHelp,
};
const { scrollTop, isHelpOpen } = this.state;
const styles = getStyles();
return (
<EditorTabBody
heading="Data source"
renderToolbar={this.renderToolbar}
toolbarItems={[queryInspector, dsHelp]}
setScrollTop={this.setScrollTop}
<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>;
......@@ -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}
/>
));
}
}
......@@ -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>;
......@@ -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;
......
......@@ -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;
......
.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;
......
......@@ -11,10 +11,6 @@
color: $gray-2;
}
.query-editor-rows {
margin: 20px 0;
}
.tight-form-func {
background: $tight-form-func-bg;
......
......@@ -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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment