Prometheus/Loki: Improve operations header UX (#46346)

* Prometheus/Loki: Improve operations header UX

* More tweaks

* Rename file to match compponent
This commit is contained in:
Torkel Ödegaard 2022-03-10 09:38:53 +01:00 committed by GitHub
parent 9fc9708ba5
commit 806b0e3b23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 126 additions and 132 deletions

View File

@ -1,6 +1,6 @@
import { css } from '@emotion/css';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
import { FlexItem, Stack } from '@grafana/experimental';
import { Stack } from '@grafana/experimental';
import { Button, useStyles2 } from '@grafana/ui';
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
@ -11,8 +11,7 @@ import {
QueryBuilderOperationDef,
QueryBuilderOperationParamDef,
} from '../shared/types';
import { OperationInfoButton } from './OperationInfoButton';
import { OperationName } from './OperationName';
import { OperationHeader } from './OperationHeader';
import { getOperationParamEditor } from './OperationParamEditor';
import { getOperationParamId } from './operationUtils';
@ -121,27 +120,15 @@ export function OperationEditor({
{...provided.draggableProps}
data-testid={`operations.${index}.wrapper`}
>
<div className={styles.header} {...provided.dragHandleProps}>
<OperationName
operation={operation}
def={def}
index={index}
onChange={onChange}
queryModeller={queryModeller}
/>
<FlexItem grow={1} />
<div className={`${styles.operationHeaderButtons} operation-header-show-on-hover`}>
<OperationInfoButton def={def} operation={operation} />
<Button
icon="times"
size="sm"
onClick={() => onRemove(index)}
fill="text"
variant="secondary"
title="Remove operation"
/>
</div>
</div>
<OperationHeader
operation={operation}
dragHandleProps={provided.dragHandleProps}
def={def}
index={index}
onChange={onChange}
onRemove={onRemove}
queryModeller={queryModeller}
/>
<div className={styles.body}>{operationElements}</div>
{restParam}
{index < query.operations.length - 1 && (
@ -205,16 +192,6 @@ const getStyles = (theme: GrafanaTheme2) => {
marginBottom: theme.spacing(1),
position: 'relative',
}),
header: css({
borderBottom: `1px solid ${theme.colors.border.medium}`,
padding: theme.spacing(0.5, 0.5, 0.5, 1),
gap: theme.spacing(1),
display: 'flex',
alignItems: 'center',
'&:hover .operation-header-show-on-hover': css({
opacity: 1,
}),
}),
infoIcon: css({
color: theme.colors.text.secondary,
}),
@ -234,12 +211,6 @@ const getStyles = (theme: GrafanaTheme2) => {
verticalAlign: 'middle',
height: '32px',
}),
operationHeaderButtons: css({
opacity: 0,
transition: theme.transitions.create(['opacity'], {
duration: theme.transitions.duration.short,
}),
}),
paramValue: css({
display: 'table-cell',
paddingBottom: theme.spacing(0.5),

View File

@ -0,0 +1,115 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { FlexItem } from '@grafana/experimental';
import { Button, Select, useStyles2 } from '@grafana/ui';
import React, { useState } from 'react';
import { OperationInfoButton } from './OperationInfoButton';
import { VisualQueryModeller, QueryBuilderOperation, QueryBuilderOperationDef } from './types';
export interface Props {
operation: QueryBuilderOperation;
def: QueryBuilderOperationDef;
index: number;
queryModeller: VisualQueryModeller;
dragHandleProps: any;
onChange: (index: number, update: QueryBuilderOperation) => void;
onRemove: (index: number) => void;
}
interface State {
isOpen?: boolean;
alternatives?: Array<SelectableValue<QueryBuilderOperationDef>>;
}
export const OperationHeader = React.memo<Props>(
({ operation, def, index, onChange, onRemove, queryModeller, dragHandleProps }) => {
const styles = useStyles2(getStyles);
const [state, setState] = useState<State>({});
const onToggleSwitcher = () => {
if (state.isOpen) {
setState({ ...state, isOpen: false });
} else {
const alternatives = queryModeller
.getAlternativeOperations(def.alternativesKey!)
.map((alt) => ({ label: alt.name, value: alt }));
setState({ isOpen: true, alternatives });
}
};
return (
<div className={styles.header}>
{!state.isOpen && (
<>
<div {...dragHandleProps}>{def.name ?? def.id}</div>
<FlexItem grow={1} />
<div className={`${styles.operationHeaderButtons} operation-header-show-on-hover`}>
<Button
icon="angle-down"
size="sm"
onClick={onToggleSwitcher}
fill="text"
variant="secondary"
title="Click to view alternative operations"
/>
<OperationInfoButton def={def} operation={operation} />
<Button
icon="times"
size="sm"
onClick={() => onRemove(index)}
fill="text"
variant="secondary"
title="Remove operation"
/>
</div>
</>
)}
{state.isOpen && (
<div className={styles.selectWrapper}>
<Select
autoFocus
openMenuOnFocus
placeholder="Replace with"
options={state.alternatives}
isOpen={true}
onCloseMenu={onToggleSwitcher}
onChange={(value) => {
if (value.value) {
// Operation should exist if it is selectable
const newDef = queryModeller.getOperationDef(value.value.id)!;
let changedOp = { ...operation, id: value.value.id };
onChange(index, def.changeTypeHandler ? def.changeTypeHandler(changedOp, newDef) : changedOp);
}
}}
/>
</div>
)}
</div>
);
}
);
OperationHeader.displayName = 'OperationHeader';
const getStyles = (theme: GrafanaTheme2) => {
return {
header: css({
borderBottom: `1px solid ${theme.colors.border.medium}`,
padding: theme.spacing(0.5, 0.5, 0.5, 1),
display: 'flex',
alignItems: 'center',
'&:hover .operation-header-show-on-hover': css({
opacity: 1,
}),
}),
operationHeaderButtons: css({
opacity: 0,
transition: theme.transitions.create(['opacity'], {
duration: theme.transitions.duration.short,
}),
}),
selectWrapper: css({
paddingRight: theme.spacing(2),
}),
};
};

View File

@ -1,92 +0,0 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Icon, Select, useStyles2 } from '@grafana/ui';
import React, { useState } from 'react';
import { VisualQueryModeller, QueryBuilderOperation, QueryBuilderOperationDef } from './types';
export interface Props {
operation: QueryBuilderOperation;
def: QueryBuilderOperationDef;
index: number;
queryModeller: VisualQueryModeller;
onChange: (index: number, update: QueryBuilderOperation) => void;
}
interface State {
isOpen?: boolean;
alternatives?: Array<SelectableValue<QueryBuilderOperationDef>>;
}
export const OperationName = React.memo<Props>(({ operation, def, index, onChange, queryModeller }) => {
const styles = useStyles2(getStyles);
const [state, setState] = useState<State>({});
const onToggleSwitcher = () => {
if (state.isOpen) {
setState({ ...state, isOpen: false });
} else {
const alternatives = queryModeller
.getAlternativeOperations(def.alternativesKey!)
.map((alt) => ({ label: alt.name, value: alt }));
setState({ isOpen: true, alternatives });
}
};
const nameElement = <span>{def.name ?? def.id}</span>;
if (!def.alternativesKey) {
return nameElement;
}
return (
<>
{!state.isOpen && (
<button
className={styles.wrapper}
onClick={onToggleSwitcher}
title={'Click to replace with alternative function'}
>
{nameElement}
<Icon className={`${styles.dropdown} operation-header-show-on-hover`} name="angle-down" size="md" />
</button>
)}
{state.isOpen && (
<Select
autoFocus
openMenuOnFocus
placeholder="Replace with"
options={state.alternatives}
isOpen={true}
onCloseMenu={onToggleSwitcher}
onChange={(value) => {
if (value.value) {
// Operation should exist if it is selectable
const newDef = queryModeller.getOperationDef(value.value.id)!;
let changedOp = { ...operation, id: value.value.id };
onChange(index, def.changeTypeHandler ? def.changeTypeHandler(changedOp, newDef) : changedOp);
}
}}
/>
)}
</>
);
});
OperationName.displayName = 'OperationName';
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
display: 'inline-block',
background: 'transparent',
padding: 0,
border: 'none',
boxShadow: 'none',
cursor: 'pointer',
}),
dropdown: css({
opacity: 0,
color: theme.colors.text.secondary,
}),
};
};