diff --git a/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx b/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx index a4b26cd910..4c3209e53a 100644 --- a/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx +++ b/web/ui/mantine-ui/src/pages/query/ExpressionInput.tsx @@ -58,6 +58,7 @@ import { lintKeymap } from "@codemirror/lint"; import { IconAlignJustified, IconBinaryTree, + IconCopy, IconDotsVertical, IconSearch, IconTerminal, @@ -121,6 +122,7 @@ interface ExpressionInputProps { executeQuery: (expr: string) => void; treeShown: boolean; setShowTree: (showTree: boolean) => void; + duplicatePanel: (expr: string) => void; removePanel: () => void; } @@ -128,6 +130,7 @@ const ExpressionInput: FC = ({ initialExpr, metricNames, executeQuery, + duplicatePanel, removePanel, treeShown, setShowTree, @@ -250,6 +253,12 @@ const ExpressionInput: FC = ({ > {treeShown ? "Hide" : "Show"} tree view + } + onClick={() => duplicatePanel(expr)} + > + Duplicate query + } diff --git a/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx b/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx index 5e41be7bb3..fcc7648a77 100644 --- a/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx +++ b/web/ui/mantine-ui/src/pages/query/QueryPanel.tsx @@ -23,6 +23,7 @@ import { FC, Suspense, useCallback, useMemo, useState } from "react"; import { useAppDispatch, useAppSelector } from "../../state/hooks"; import { addQueryToHistory, + duplicatePanel, GraphDisplayMode, GraphResolution, removePanel, @@ -111,6 +112,9 @@ const QueryPanel: FC = ({ idx, metricNames }) => { setSelectedNode(null); } }} + duplicatePanel={(expr: string) => { + dispatch(duplicatePanel({ idx, expr })); + }} removePanel={() => { dispatch(removePanel(idx)); }} diff --git a/web/ui/mantine-ui/src/state/queryPageSlice.ts b/web/ui/mantine-ui/src/state/queryPageSlice.ts index 4cf483e2b6..7a4f7b257a 100644 --- a/web/ui/mantine-ui/src/state/queryPageSlice.ts +++ b/web/ui/mantine-ui/src/state/queryPageSlice.ts @@ -115,6 +115,19 @@ export const queryPageSlice = createSlice({ state.panels.push(newDefaultPanel()); updateURL(state.panels); }, + duplicatePanel: ( + state, + { payload }: PayloadAction<{ idx: number; expr: string }> + ) => { + const newPanel = { + ...state.panels[payload.idx], + id: randomId(), + expr: payload.expr, + }; + // Insert the duplicated panel just below the original panel. + state.panels.splice(payload.idx + 1, 0, newPanel); + updateURL(state.panels); + }, removePanel: (state, { payload }: PayloadAction) => { state.panels.splice(payload, 1); updateURL(state.panels); @@ -153,6 +166,7 @@ export const { setPanels, addPanel, removePanel, + duplicatePanel, setExpr, addQueryToHistory, setShowTree,