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 { 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),
|
||||
|
@ -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