Skip to content

Commit

Permalink
feat(admin): Add a standardized component for copying query results
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
evanh committed Jan 17, 2025
1 parent 614e99f commit 3062ccf
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 257 deletions.
13 changes: 7 additions & 6 deletions snuba/admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
55 changes: 9 additions & 46 deletions snuba/admin/static/cardinality_analyzer/query_display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
CardinalityQueryResult,
PredefinedQuery,
} from "SnubaAdmin/cardinality_analyzer/types";
import QueryResultCopier from "SnubaAdmin/utils/query_result_copier";

enum ClipboardFormats {
CSV = "csv",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -92,41 +75,21 @@ function QueryDisplay(props: {
return (
<div key={idx}>
<p>{queryResult.input_query}</p>
<p>
<button
style={copyButtonStyle}
onClick={() => copyText(queryResult, ClipboardFormats.JSON)}
>
Copy to clipboard (JSON)
</button>
</p>
<p>
<button
style={copyButtonStyle}
onClick={() => copyText(queryResult, ClipboardFormats.CSV)}
>
Copy to clipboard (CSV)
</button>
</p>
<QueryResultCopier
jsonInput={JSON.stringify(queryResult)}
csvInput={convertResultsToCSV(queryResult)}
/>
{props.resultDataPopulator(queryResult)}
</div>
);
}

return (
<Collapse key={idx} text={queryResult.input_query}>
<button
style={copyButtonStyle}
onClick={() => copyText(queryResult, ClipboardFormats.JSON)}
>
Copy to clipboard (JSON)
</button>
<button
style={copyButtonStyle}
onClick={() => copyText(queryResult, ClipboardFormats.CSV)}
>
Copy to clipboard (CSV)
</button>
<QueryResultCopier
jsonInput={JSON.stringify(queryResult)}
csvInput={convertResultsToCSV(queryResult)}
/>
{props.resultDataPopulator(queryResult)}
</Collapse>
);
Expand Down
40 changes: 22 additions & 18 deletions snuba/admin/static/clickhouse_queries/query_display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<div>
<form style={query.sudo ? sudoForm : standardForm}>
Expand Down Expand Up @@ -202,27 +210,23 @@ function QueryDisplay(props: {
return (
<div key={idx}>
<p>{queryResult.input_query}</p>
<p>
<button
style={executeButtonStyle}
onClick={() => copyText(JSON.stringify(queryResult))}
>
Copy to clipboard
</button>
</p>
<QueryResultCopier
rawInput={queryResult.trace_output || ""}
jsonInput={JSON.stringify(queryResult)}
csvInput={convertResultsToCSV(queryResult)}
/>
{props.resultDataPopulator(queryResult)}
</div>
);
}

return (
<Collapse key={idx} text={queryResult.input_query}>
<button
style={executeButtonStyle}
onClick={() => copyText(JSON.stringify(queryResult))}
>
Copy to clipboard
</button>
<QueryResultCopier
rawInput={queryResult.trace_output || ""}
jsonInput={JSON.stringify(queryResult)}
csvInput={convertResultsToCSV(queryResult)}
/>
{props.resultDataPopulator(queryResult)}
</Collapse>
);
Expand Down
71 changes: 18 additions & 53 deletions snuba/admin/static/mql_queries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`;

Expand Down Expand Up @@ -158,31 +159,13 @@ function MQLQueries(props: { api: Client }) {
queryResult: queryResultHistory[0],
})}
</div>
<Button.Group>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
JSON.stringify(queryResultHistory[0])
)
}
>
Copy to clipboard (JSON)
</Button>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
CSV.sheet([
queryResultHistory[0].columns,
...queryResultHistory[0].rows,
])
)
}
>
Copy to clipboard (CSV)
</Button>
</Button.Group>
<QueryResultCopier
jsonInput={JSON.stringify(queryResultHistory[0])}
csvInput={CSV.sheet([
queryResultHistory[0].columns,
...queryResultHistory[0].rows,
])}
/>
<Space h="md" />
<Table
headerData={queryResultHistory[0].columns}
Expand Down Expand Up @@ -216,7 +199,7 @@ function MQLQueries(props: { api: Client }) {
</>
)}
</div>
</div>
</div >
);
}

Expand Down Expand Up @@ -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."
);
}
});
Expand All @@ -339,28 +322,10 @@ function QueryResultHistoryItem(props: { queryResult: QueryResult }) {
Execution Duration (ms): {props.queryResult.duration_ms}
{QueryResultQuotaAllowance({ queryResult: props.queryResult })}
</div>
<Button.Group>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
JSON.stringify(props.queryResult)
)
}
>
Copy to clipboard (JSON)
</Button>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
CSV.sheet([props.queryResult.columns, ...props.queryResult.rows])
)
}
>
Copy to clipboard (CSV)
</Button>
</Button.Group>
<QueryResultCopier
jsonInput={JSON.stringify(props.queryResult)}
csvInput={CSV.sheet([props.queryResult.columns, ...props.queryResult.rows])}
/>
<Space h="md" />
<Table
headerData={props.queryResult.columns}
Expand Down
72 changes: 20 additions & 52 deletions snuba/admin/static/production_queries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CustomSelect, getParamFromStorage } from "SnubaAdmin/select";
import { useDisclosure } from "@mantine/hooks";
import { CSV } from "SnubaAdmin/cardinality_analyzer/CSV";
import { getRecentHistory, setRecentHistory } from "SnubaAdmin/query_history";
import QueryResultCopier from "SnubaAdmin/utils/query_result_copier";

const HISTORY_KEY = "production_queries";
function ProductionQueries(props: { api: Client }) {
Expand Down Expand Up @@ -133,31 +134,13 @@ function ProductionQueries(props: { api: Client }) {
queryResult: queryResultHistory[0],
})}
</div>
<Button.Group>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
JSON.stringify(queryResultHistory[0])
)
}
>
Copy to clipboard (JSON)
</Button>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
CSV.sheet([
queryResultHistory[0].columns,
...queryResultHistory[0].rows,
])
)
}
>
Copy to clipboard (CSV)
</Button>
</Button.Group>
<QueryResultCopier
jsonInput={JSON.stringify(queryResultHistory[0])}
csvInput={CSV.sheet([
queryResultHistory[0].columns,
...queryResultHistory[0].rows,
])}
/>
<Space h="md" />
<Table
headerData={queryResultHistory[0].columns}
Expand Down Expand Up @@ -266,11 +249,11 @@ function QueryResultQuotaAllowance(props: { queryResult: QueryResult }) {
if (policy.max_threads < 10 && policy.explanation.reason != null) {
reasonHeader.push(
policyName +
": " +
policy.explanation.reason +
". SnQL Query executed with " +
policy.max_threads +
" threads."
": " +
policy.explanation.reason +
". SnQL Query executed with " +
policy.max_threads +
" threads."
);
}
});
Expand All @@ -294,28 +277,13 @@ function QueryResultHistoryItem(props: { queryResult: QueryResult }) {
Execution Duration (ms): {props.queryResult.duration_ms}
{QueryResultQuotaAllowance({ queryResult: props.queryResult })}
</div>
<Button.Group>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
JSON.stringify(props.queryResult)
)
}
>
Copy to clipboard (JSON)
</Button>
<Button
variant="outline"
onClick={() =>
window.navigator.clipboard.writeText(
CSV.sheet([props.queryResult.columns, ...props.queryResult.rows])
)
}
>
Copy to clipboard (CSV)
</Button>
</Button.Group>
<QueryResultCopier
jsonInput={JSON.stringify(props.queryResult)}
csvInput={CSV.sheet([
props.queryResult.columns,
...props.queryResult.rows,
])}
/>
<Space h="md" />
<Table
headerData={props.queryResult.columns}
Expand Down
Loading

0 comments on commit 3062ccf

Please sign in to comment.