Prometheus: Highlight operations added in the query builder (#47961)

* Highlight newly added operations

* Better diff for the operations change

* Changed the highlight style
This commit is contained in:
Andrej Ocenas 2022-04-21 10:26:27 +02:00 committed by GitHub
parent 5c3be630f2
commit ff5aef194c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 4 deletions

View File

@ -1,8 +1,8 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { DataSourceApi, GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Button, Icon, Tooltip, useStyles2 } from '@grafana/ui';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import {
VisualQueryModeller,
@ -24,6 +24,7 @@ export interface Props {
onChange: (index: number, update: QueryBuilderOperation) => void;
onRemove: (index: number) => void;
onRunQuery: () => void;
highlight?: boolean;
}
export function OperationEditor({
@ -35,9 +36,12 @@ export function OperationEditor({
queryModeller,
query,
datasource,
highlight,
}: Props) {
const styles = useStyles2(getStyles);
const def = queryModeller.getOperationDef(operation.id);
const shouldHighlight = useHighlight(highlight);
if (!def) {
return <span>Operation {operation.id} not found</span>;
}
@ -122,7 +126,7 @@ export function OperationEditor({
<Draggable draggableId={`operation-${index}`} index={index}>
{(provided) => (
<div
className={styles.card}
className={cx(styles.card, shouldHighlight && styles.cardHighlight)}
ref={provided.innerRef}
{...provided.draggableProps}
data-testid={`operations.${index}.wrapper`}
@ -150,6 +154,29 @@ export function OperationEditor({
);
}
/**
* When highlight is switched on makes sure it is switched of right away, so we just flash the highlight and then fade
* out.
* @param highlight
*/
function useHighlight(highlight?: boolean) {
const [keepHighlight, setKeepHighlight] = useState(true);
useEffect(() => {
let t: any;
if (highlight) {
t = setTimeout(() => {
setKeepHighlight(false);
}, 1);
} else {
setKeepHighlight(true);
}
return () => clearTimeout(t);
}, [highlight]);
return keepHighlight && highlight;
}
function renderAddRestParamButton(
paramDef: QueryBuilderOperationParamDef,
onAddRestParam: () => void,
@ -198,6 +225,11 @@ const getStyles = (theme: GrafanaTheme2) => {
borderRadius: theme.shape.borderRadius(1),
marginBottom: theme.spacing(1),
position: 'relative',
transition: 'all 1s ease-in 0s',
}),
cardHighlight: css({
boxShadow: `0px 0px 4px 0px ${theme.colors.primary.border}`,
border: `1px solid ${theme.colors.primary.border}`,
}),
infoIcon: css({
marginLeft: theme.spacing(0.5),

View File

@ -4,6 +4,7 @@ import { Stack } from '@grafana/experimental';
import { Button, Cascader, CascaderOption, useStyles2 } from '@grafana/ui';
import React, { useState } from 'react';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import { useMountedState, usePrevious } from 'react-use';
import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from '../shared/types';
import { OperationEditor } from './OperationEditor';
@ -26,6 +27,8 @@ export function OperationList<T extends QueryWithOperations>({
const styles = useStyles2(getStyles);
const { operations } = query;
const opsToHighlight = useOperationsHighlight(operations);
const [cascaderOpen, setCascaderOpen] = useState(false);
const onOperationChange = (index: number, update: QueryBuilderOperation) => {
@ -86,7 +89,7 @@ export function OperationList<T extends QueryWithOperations>({
<div className={styles.operationList} ref={provided.innerRef} {...provided.droppableProps}>
{operations.map((op, index) => (
<OperationEditor
key={index}
key={op.id + index}
queryModeller={queryModeller}
index={index}
operation={op}
@ -95,6 +98,7 @@ export function OperationList<T extends QueryWithOperations>({
onChange={onOperationChange}
onRemove={onRemove}
onRunQuery={onRunQuery}
highlight={opsToHighlight[index]}
/>
))}
{provided.placeholder}
@ -125,6 +129,49 @@ export function OperationList<T extends QueryWithOperations>({
);
}
/**
* Returns indexes of operations that should be highlighted. We check the diff of operations added but at the same time
* we want to highlight operations only after the initial render, so we check for mounted state and calculate the diff
* only after.
* @param operations
*/
function useOperationsHighlight(operations: QueryBuilderOperation[]) {
const isMounted = useMountedState();
const prevOperations = usePrevious(operations);
if (!isMounted()) {
return operations.map(() => false);
}
if (!prevOperations) {
return operations.map(() => true);
}
let newOps: boolean[] = [];
if (prevOperations.length - 1 === operations.length && operations.every((op) => prevOperations.includes(op))) {
// In case we remove one op and does not change any ops then don't highlight anything.
return operations.map(() => false);
}
if (prevOperations.length + 1 === operations.length && prevOperations.every((op) => operations.includes(op))) {
// If we add a single op just find it and highlight just that.
const newOp = operations.find((op) => !prevOperations.includes(op));
newOps = operations.map((op) => {
return op === newOp;
});
} else {
// Default diff of all ops.
newOps = operations.map((op, index) => {
return !isSameOp(op.id, prevOperations[index]?.id);
});
}
return newOps;
}
function isSameOp(op1?: string, op2?: string) {
return op1 === op2 || `__${op1}_by` === op2 || op1 === `__${op2}_by`;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
heading: css({