mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9fc9708ba5
commit
806b0e3b23
@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
|
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { FlexItem, Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Button, useStyles2 } from '@grafana/ui';
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
@ -11,8 +11,7 @@ import {
|
|||||||
QueryBuilderOperationDef,
|
QueryBuilderOperationDef,
|
||||||
QueryBuilderOperationParamDef,
|
QueryBuilderOperationParamDef,
|
||||||
} from '../shared/types';
|
} from '../shared/types';
|
||||||
import { OperationInfoButton } from './OperationInfoButton';
|
import { OperationHeader } from './OperationHeader';
|
||||||
import { OperationName } from './OperationName';
|
|
||||||
import { getOperationParamEditor } from './OperationParamEditor';
|
import { getOperationParamEditor } from './OperationParamEditor';
|
||||||
import { getOperationParamId } from './operationUtils';
|
import { getOperationParamId } from './operationUtils';
|
||||||
|
|
||||||
@ -121,27 +120,15 @@ export function OperationEditor({
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
data-testid={`operations.${index}.wrapper`}
|
data-testid={`operations.${index}.wrapper`}
|
||||||
>
|
>
|
||||||
<div className={styles.header} {...provided.dragHandleProps}>
|
<OperationHeader
|
||||||
<OperationName
|
|
||||||
operation={operation}
|
operation={operation}
|
||||||
|
dragHandleProps={provided.dragHandleProps}
|
||||||
def={def}
|
def={def}
|
||||||
index={index}
|
index={index}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onRemove={onRemove}
|
||||||
queryModeller={queryModeller}
|
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>
|
|
||||||
<div className={styles.body}>{operationElements}</div>
|
<div className={styles.body}>{operationElements}</div>
|
||||||
{restParam}
|
{restParam}
|
||||||
{index < query.operations.length - 1 && (
|
{index < query.operations.length - 1 && (
|
||||||
@ -205,16 +192,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
marginBottom: theme.spacing(1),
|
marginBottom: theme.spacing(1),
|
||||||
position: 'relative',
|
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({
|
infoIcon: css({
|
||||||
color: theme.colors.text.secondary,
|
color: theme.colors.text.secondary,
|
||||||
}),
|
}),
|
||||||
@ -234,12 +211,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
height: '32px',
|
height: '32px',
|
||||||
}),
|
}),
|
||||||
operationHeaderButtons: css({
|
|
||||||
opacity: 0,
|
|
||||||
transition: theme.transitions.create(['opacity'], {
|
|
||||||
duration: theme.transitions.duration.short,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
paramValue: css({
|
paramValue: css({
|
||||||
display: 'table-cell',
|
display: 'table-cell',
|
||||||
paddingBottom: theme.spacing(0.5),
|
paddingBottom: theme.spacing(0.5),
|
||||||
|
@ -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),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
@ -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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user