From f368cac79625080ced05a3de2d6d2996d9671bbb Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 21 Apr 2022 04:39:41 -0400 Subject: [PATCH] Prometheus: Highlight operations added in the query builder (#47961) (#48033) * Highlight newly added operations * Better diff for the operations change * Changed the highlight style (cherry picked from commit ff5aef194c0f86d51e62c37a74ed16634bf01dde) Co-authored-by: Andrej Ocenas --- .../querybuilder/shared/OperationEditor.tsx | 38 ++++++++++++-- .../querybuilder/shared/OperationList.tsx | 49 ++++++++++++++++++- 2 files changed, 83 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx index 4574ff7f829..92df358c34a 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx @@ -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 Operation {operation.id} not found; } @@ -122,7 +126,7 @@ export function OperationEditor({ {(provided) => (
{ + 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), diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx index e5b5dec7442..026d9eaf911 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx @@ -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({ 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({
{operations.map((op, index) => ( ({ onChange={onOperationChange} onRemove={onRemove} onRunQuery={onRunQuery} + highlight={opsToHighlight[index]} /> ))} {provided.placeholder} @@ -125,6 +129,49 @@ export function OperationList({ ); } +/** + * 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({