{
+ 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({