From 3062ccf4347b84d24f6056f89481b9d125586a2b Mon Sep 17 00:00:00 2001 From: Evan Hicks Date: Fri, 17 Jan 2025 15:38:25 -0500 Subject: [PATCH] feat(admin): Add a standardized component for copying query results At lot of components offer the ability to copy the results of a run query, but each had a slightly different way of providing that functionality. That meant that not all functionality across the components. Add a standard component for these buttons. This will also ensure that system queries now have a CSV copy results. --- snuba/admin/package.json | 13 ++-- .../cardinality_analyzer/query_display.tsx | 55 +++----------- .../clickhouse_queries/query_display.tsx | 40 +++++----- snuba/admin/static/mql_queries/index.tsx | 71 +++++------------ .../admin/static/production_queries/index.tsx | 72 +++++------------- snuba/admin/static/querylog/query_display.tsx | 76 +++---------------- snuba/admin/static/tracing/query_display.tsx | 20 ++--- .../static/utils/query_result_copier.tsx | 34 +++++++++ 8 files changed, 124 insertions(+), 257 deletions(-) create mode 100644 snuba/admin/static/utils/query_result_copier.tsx diff --git a/snuba/admin/package.json b/snuba/admin/package.json index cd46c39a5e0..db2be23d781 100644 --- a/snuba/admin/package.json +++ b/snuba/admin/package.json @@ -14,8 +14,8 @@ "@mantine/core": "6.0.21", "@mantine/dates": "6.0.21", "@mantine/hooks": "6.0.21", - "@mantine/prism": "^6.0.15", - "@mantine/tiptap": "^6.0.15", + "@mantine/prism": "^6.0.21", + "@mantine/tiptap": "^6.0.21", "@sentry/react": "^7.88.0", "@tiptap/extension-code-block-lowlight": "^2.0.3", "@tiptap/extension-link": "^2.0.3", @@ -40,10 +40,11 @@ "devDependencies": { "@emotion/react": "^11.13.3", "@jest/globals": "^29.4.3", - "@mantine/core": "^6.0.15", - "@mantine/hooks": "^6.0.15", - "@mantine/prism": "^6.0.15", - "@mantine/tiptap": "^6.0.15", + "@mantine/core": "6.0.21", + "@mantine/dates": "6.0.21", + "@mantine/hooks": "6.0.21", + "@mantine/prism": "^6.0.21", + "@mantine/tiptap": "^6.0.21", "@sentry/esbuild-plugin": "^2.22.7", "@tabler/icons-react": "^3.17.0", "@testing-library/react": "^14.0.0", diff --git a/snuba/admin/static/cardinality_analyzer/query_display.tsx b/snuba/admin/static/cardinality_analyzer/query_display.tsx index d3d1d54119b..a6c2428fd8c 100644 --- a/snuba/admin/static/cardinality_analyzer/query_display.tsx +++ b/snuba/admin/static/cardinality_analyzer/query_display.tsx @@ -11,6 +11,7 @@ import { CardinalityQueryResult, PredefinedQuery, } from "SnubaAdmin/cardinality_analyzer/types"; +import QueryResultCopier from "SnubaAdmin/utils/query_result_copier"; enum ClipboardFormats { CSV = "csv", @@ -42,24 +43,6 @@ function QueryDisplay(props: { return CSV.sheet([queryResult.column_names, ...queryResult.rows]); } - function copyText( - queryResult: CardinalityQueryResult, - format: ClipboardFormats - ) { - let formatter: (input: CardinalityQueryResult) => string = (s) => - s.toString(); - - if (format === ClipboardFormats.JSON) { - formatter = JSON.stringify; - } - - if (format === ClipboardFormats.CSV) { - formatter = convertResultsToCSV; - } - - window.navigator.clipboard.writeText(formatter(queryResult)); - } - function executeQuery() { return props.api .executeCardinalityQuery(query as CardinalityQueryRequest) @@ -92,22 +75,10 @@ function QueryDisplay(props: { return (

{queryResult.input_query}

-

- -

-

- -

+ {props.resultDataPopulator(queryResult)}
); @@ -115,18 +86,10 @@ function QueryDisplay(props: { return ( - - + {props.resultDataPopulator(queryResult)} ); diff --git a/snuba/admin/static/clickhouse_queries/query_display.tsx b/snuba/admin/static/clickhouse_queries/query_display.tsx index b7b0199713a..2535260ec91 100644 --- a/snuba/admin/static/clickhouse_queries/query_display.tsx +++ b/snuba/admin/static/clickhouse_queries/query_display.tsx @@ -3,6 +3,7 @@ import Client from "SnubaAdmin/api_client"; import { Collapse } from "SnubaAdmin/collapse"; import QueryEditor from "SnubaAdmin/query_editor"; import ExecuteButton from "SnubaAdmin/utils/execute_button"; +import QueryResultCopier from "SnubaAdmin/utils/query_result_copier"; import { SelectItem, Switch, Alert } from "@mantine/core"; import { Prism } from "@mantine/prism"; @@ -99,10 +100,6 @@ function QueryDisplay(props: { }); } - function copyText(text: string) { - window.navigator.clipboard.writeText(text); - } - function getHosts(nodeData: ClickhouseNodeData[]): SelectItem[] { let node_info = nodeData.find((el) => el.storage_name === query.storage)!; // populate the hosts entries marking distributed hosts that are not also local @@ -142,6 +139,17 @@ function QueryDisplay(props: { setQueryError(error); } + function convertResultsToCSV(queryResult: QueryResult) { + let output = queryResult.column_names.join(","); + for (const row of queryResult.rows) { + const escaped = row.map((v) => + typeof v == "string" && v.includes(",") ? '"' + v + '"' : v + ); + output = output + "\n" + escaped.join(","); + } + return output; + } + return (
@@ -202,14 +210,11 @@ function QueryDisplay(props: { return (

{queryResult.input_query}

-

- -

+ {props.resultDataPopulator(queryResult)}
); @@ -217,12 +222,11 @@ function QueryDisplay(props: { return ( - + {props.resultDataPopulator(queryResult)} ); diff --git a/snuba/admin/static/mql_queries/index.tsx b/snuba/admin/static/mql_queries/index.tsx index 441d0ac478a..171a9441d26 100644 --- a/snuba/admin/static/mql_queries/index.tsx +++ b/snuba/admin/static/mql_queries/index.tsx @@ -23,6 +23,7 @@ import { } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { CSV } from "../cardinality_analyzer/CSV"; +import QueryResultCopier from "SnubaAdmin/utils/query_result_copier"; const MQLQueryExample = `(sum(d:transactions/duration@millisecond{status_code: 200}) by transaction + sum(d:transactions/duration@millisecond) by transaction) * 100.0`; @@ -158,31 +159,13 @@ function MQLQueries(props: { api: Client }) { queryResult: queryResultHistory[0], })}
- - - - + )} - + ); } @@ -311,11 +294,11 @@ function QueryResultQuotaAllowance(props: { queryResult: QueryResult }) { if (policy.max_threads < 10 && policy.explanation.reason != null) { reasonHeader.push( policyName + - ": " + - policy.explanation.reason + - ". MQL Query executed with " + - policy.max_threads + - " threads." + ": " + + policy.explanation.reason + + ". MQL Query executed with " + + policy.max_threads + + " threads." ); } }); @@ -339,28 +322,10 @@ function QueryResultHistoryItem(props: { queryResult: QueryResult }) { Execution Duration (ms): {props.queryResult.duration_ms} {QueryResultQuotaAllowance({ queryResult: props.queryResult })} - - - - +
- - - - +
- - - - +
; const HISTORY_KEY = "querylog"; @@ -64,11 +65,6 @@ function QueryDisplay(props: { return output; } - function copyText(queryResult: QuerylogResult, format: string) { - const formatter = format == "csv" ? convertResultsToCSV : JSON.stringify; - window.navigator.clipboard.writeText(formatter(queryResult)); - } - return (

Construct a Querylog Query

@@ -98,22 +94,10 @@ function QueryDisplay(props: { return (

{queryResult.input_query}

-

- -

-

- -

+ {props.resultDataPopulator(queryResult)}
); @@ -121,18 +105,10 @@ function QueryDisplay(props: { return ( - - + {props.resultDataPopulator(queryResult)} ); @@ -148,38 +124,4 @@ const executeActionsStyle = { marginTop: 8, }; -const executeButtonStyle = { - height: 30, - border: 0, - padding: "4px 20px", - marginRight: 10, -}; - -const selectStyle = { - marginRight: 8, - height: 30, -}; - -function TextArea(props: { - value: string; - onChange: (nextValue: string) => void; -}) { - const { value, onChange } = props; - return ( -