diff --git a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 index e3eeb32bca..c741c977bd 100644 --- a/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 +++ b/test/data/ui-screenshots/debug_tracks.test.ts/debug-tracks/debug-track-added.png.sha256 @@ -1 +1 @@ -8b7d70ad4f8739e1da2c08a01e986300e8a54802605242fac43e256cd077a637 \ No newline at end of file +4b25c85d37c04af0f123ed63eab1c5227bae0786edee6111a09e22088375183d \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png index d4456261f8..b104d7cf27 100644 Binary files a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png and b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png differ diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 index 34de71f38c..ee67cf6def 100644 --- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/query-mode.png.sha256 @@ -1 +1 @@ -b9a96bb2efd20db364d16d7e7fe1e060248f5bf27557badcd123c3c330195d61 \ No newline at end of file +3a0423ca165ba88d7635c53932c7bb5d66fbebb7247833c0bb32ce465d3bfefd \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 index 6d20986aa3..3c2d0f5d20 100644 --- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-1-clicked.png.sha256 @@ -1 +1 @@ -00ab906efd78e5e2ec0465f9ac25cc792af71643d77112df36e68e85fb323c49 \ No newline at end of file +32fb8853ad2e317ce9d37a318167ca68d2826e37ddbc28b5f35ba22520f57b03 \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 index 9652c50b10..a78f963aec 100644 --- a/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/omnibox-query/row-2-clicked.png.sha256 @@ -1 +1 @@ -ce20e472156ea2c2ecb7aab6c76d3242e73eea039bf0624e1f2e194057dcb842 \ No newline at end of file +e39cccdbc390289b3c10d2743c597d7e360f6b8c1b40647d51e86ea4b483be49 \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256 index b0bb016ea6..b2a709da78 100644 --- a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-1.png.sha256 @@ -1 +1 @@ -c636755a7c74805ac5c8dbafd31110fbb666070df179ffddda7d9f2fe3404fa9 \ No newline at end of file +019681faa293a20dd826bb58abf2e21a468887dccdc3e16ce60c73eb3838fb33 \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256 index 7d205e50b2..e80e278320 100644 --- a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-2.png.sha256 @@ -1 +1 @@ -7a37afddcfef9a096999af8c6dcfc7b89df088519e9359c1050ca2a937229290 \ No newline at end of file +62084cb38d8bc53e27ed85ed028c0291e763436da4116d3b6bb664bc1e1bb0b7 \ No newline at end of file diff --git a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256 b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256 index c9afab843e..16afbeb654 100644 --- a/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256 +++ b/test/data/ui-screenshots/queries.test.ts/query-page/query-limit-3.png.sha256 @@ -1 +1 @@ -177c2c44763d97dc161c133c3ec3c8ac9af76ae54e7e6bb660902ec0233512ab \ No newline at end of file +6d2f7e20729c73b152a9d13890eed7ff882e20a37fb87cc828cc863ba39ae2ad \ No newline at end of file diff --git a/ui/src/components/query_table/query_table.ts b/ui/src/components/query_table/query_table.ts index 2d33e0784e..0d70523d00 100644 --- a/ui/src/components/query_table/query_table.ts +++ b/ui/src/components/query_table/query_table.ts @@ -24,6 +24,8 @@ import {downloadData} from '../../base/download_utils'; import {Router} from '../../core/router'; import {AppImpl} from '../../core/app_impl'; import {Trace} from '../../public/trace'; +import {MenuItem, PopupMenu} from '../../widgets/menu'; +import {Icons} from '../../base/semantic_icons'; interface QueryTableRowAttrs { trace: Trace; @@ -228,20 +230,30 @@ export class QueryTable implements m.ClassComponent { ) { return [ contextButtons, - m(Button, { - label: 'Copy query', - onclick: () => { - copyToClipboard(query); + m( + PopupMenu, + { + trigger: m(Button, { + label: 'Copy', + rightIcon: Icons.ContextMenu, + }), }, - }), - resp && - resp.error === undefined && - m(Button, { - label: 'Copy result (.tsv)', - onclick: () => { - queryResponseToClipboard(resp); - }, + m(MenuItem, { + label: 'Query', + onclick: () => copyToClipboard(query), }), + resp && + resp.error === undefined && [ + m(MenuItem, { + label: 'Result (.tsv)', + onclick: () => queryResponseAsTsvToClipboard(resp), + }), + m(MenuItem, { + label: 'Result (.md)', + onclick: () => queryResponseAsMarkdownToClipboard(resp), + }), + ], + ), ]; } @@ -264,7 +276,9 @@ export class QueryTable implements m.ClassComponent { } } -async function queryResponseToClipboard(resp: QueryResponse): Promise { +async function queryResponseAsTsvToClipboard( + resp: QueryResponse, +): Promise { const lines: string[][] = []; lines.push(resp.columns); for (const row of resp.rows) { @@ -275,5 +289,48 @@ async function queryResponseToClipboard(resp: QueryResponse): Promise { } lines.push(line); } - copyToClipboard(lines.map((line) => line.join('\t')).join('\n')); + await copyToClipboard(lines.map((line) => line.join('\t')).join('\n')); +} + +async function queryResponseAsMarkdownToClipboard( + resp: QueryResponse, +): Promise { + // Convert all values to strings. + // rows = [header, separators, ...body] + const rows: string[][] = []; + rows.push(resp.columns); + rows.push(resp.columns.map((_) => '---')); + for (const responseRow of resp.rows) { + rows.push( + resp.columns.map((responseCol) => { + const value = responseRow[responseCol]; + return value === null ? 'NULL' : `${value}`; + }), + ); + } + + // Find the maximum width of each column. + const maxWidths: number[] = Array(resp.columns.length).fill(0); + for (const row of rows) { + for (let i = 0; i < resp.columns.length; i++) { + if (row[i].length > maxWidths[i]) { + maxWidths[i] = row[i].length; + } + } + } + + const text = rows + .map((row, rowIndex) => { + // Pad each column to the maximum width with hyphens (separator row) or + // spaces (all other rows). + const expansionChar = rowIndex === 1 ? '-' : ' '; + const line: string[] = row.map( + (str, colIndex) => + str + expansionChar.repeat(maxWidths[colIndex] - str.length), + ); + return `| ${line.join(' | ')} |`; + }) + .join('\n'); + + await copyToClipboard(text); }