From 1b9d99381c2919dde5fbfa38832490e0c41785ca Mon Sep 17 00:00:00 2001 From: Travis Briggs Date: Thu, 13 Aug 2026 06:17:07 -0700 Subject: [PATCH 1/3] Add a "Copy link" button to resultset headers The result output URLs (/run//output//) are already unauthenticated and serve Access-Control-Allow-Origin: *, so they can be consumed from outside Quarry - a spreadsheet's import-from-URL, a bot, a notebook. Until now the only way to get one was to right-click a "Download data" entry and copy the link address. Add a "Copy link" dropdown beside "Download data", offering the same six formats. It copies the absolute URL to the clipboard and shows a transient "Copied to clipboard" confirmation in the resultset header. The download links are unchanged. The confirmation appears immediately and fades over 1.5s via a CSS transition; jQuery's .fadeOut() is disallowed by eslint (no-jquery/no-fade). compiled.js is regenerated with `npm run build`. This patch was written with AI assistance (Claude Code). It has been reviewed and tested by hand: eslint passes, and the button was verified in a browser against a local docker-compose instance, including a query with multiple resultsets. Co-Authored-By: Claude Opus 5 Bug: T434787 --- quarry/web/static/css/query/view.css | 16 +++++ quarry/web/static/js/query/view.js | 16 +++++ quarry/web/static/templates/compiled.js | 70 +++++++++++++++++-- .../web/static/templates/query-resultset.html | 52 +++++++++----- 4 files changed, 132 insertions(+), 22 deletions(-) diff --git a/quarry/web/static/css/query/view.css b/quarry/web/static/css/query/view.css index c47876dd..15890ff9 100644 --- a/quarry/web/static/css/query/view.css +++ b/quarry/web/static/css/query/view.css @@ -95,6 +95,22 @@ table#query-results-sampled { margin-bottom: 12px; } +.copy-link-status { + float: left; + margin-right: 8px; + line-height: 22px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + transition: opacity 1.5s ease-out; +} + +.copy-link-status.copy-link-status-shown { + opacity: 1; + /* Appear at once, fade away slowly. */ + transition: none; +} + div.dataTables_info { margin: 0 0 10.5px; } diff --git a/quarry/web/static/js/query/view.js b/quarry/web/static/js/query/view.js index 29800a9f..c540049b 100644 --- a/quarry/web/static/js/query/view.js +++ b/quarry/web/static/js/query/view.js @@ -151,6 +151,22 @@ $( () => { } ); } + /* Copy an absolute result URL to the clipboard instead of downloading it. */ + $( '#query-result' ).on( 'click', '.copy-link-menu a', function ( event ) { + event.preventDefault(); + const $status = $( this ).closest( '.btn-toolbar' ).find( '.copy-link-status' ); + navigator.clipboard.writeText( this.href ).then( () => { + $status.text( ' ✅️ Copied to clipboard' ).addClass( 'copy-link-status-shown' ); + setTimeout( () => { + // Empty it once faded, so it stops taking up room in the header. + $status.one( 'transitionend', () => $status.text( '' ) ) + .removeClass( 'copy-link-status-shown' ); + }, 3000 ); + }, ( err ) => { + console.log( 'Could not copy result link to clipboard', err ); + } ); + } ); + $( '#togglehl' ).on( 'click', () => { if ( editor === null ) { editor = makeEditor(); diff --git a/quarry/web/static/templates/compiled.js b/quarry/web/static/templates/compiled.js index 15db4197..35e351d9 100644 --- a/quarry/web/static/templates/compiled.js +++ b/quarry/web/static/templates/compiled.js @@ -18,7 +18,7 @@ output += "\n "; } output += "\n ("; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "rowcount"), env.opts.autoescape); -output += " rows)\n \n
\n
\n \n
    \n "; +output += " rows)\n
\n
\n
\n \n
\n \n
    \n "; frame = frame.push(); var t_3 = {"tsv": "TSV","json": "JSON","json-lines": "JSON Lines","csv": "CSV","wikitable": "Wikitable","html": "HTML"}; if(t_3) {var t_1; @@ -36,7 +36,7 @@ frame.set("loop.revindex0", t_2 - t_1 - 1); frame.set("loop.first", t_1 === 0); frame.set("loop.last", t_1 === t_2 - 1); frame.set("loop.length", t_2); -output += "\n
  • "; output += runtime.suppressValue(t_5, env.opts.autoescape); -output += "
  • \n "; +output += "\n "; ; } } else { @@ -70,7 +70,7 @@ frame.set("loop.revindex0", t_2 - t_1 - 1); frame.set("loop.first", t_1 === 0); frame.set("loop.last", t_1 === t_2 - 1); frame.set("loop.length", t_2); -output += "\n
  • "; output += runtime.suppressValue(t_7, env.opts.autoescape); -output += "
  • \n "; +output += "\n "; ; } } } frame = frame.pop(); -output += "\n
\n
\n
\n
\n
\n
\n"; +output += "\n \n \n
\n \n
    \n "; +frame = frame.push(); +var t_10 = {"tsv": "TSV","json": "JSON","json-lines": "JSON Lines","csv": "CSV","wikitable": "Wikitable","html": "HTML"}; +if(t_10) {var t_8; +if(runtime.isArray(t_10)) { +var t_9 = t_10.length; +for(t_8=0; t_8 < t_10.length; t_8++) { +var t_11 = t_10[t_8][0] +frame.set("format", t_10[t_8][0]); +var t_12 = t_10[t_8][1] +frame.set("formatname", t_10[t_8][1]); +frame.set("loop.index", t_8 + 1); +frame.set("loop.index0", t_8); +frame.set("loop.revindex", t_9 - t_8); +frame.set("loop.revindex0", t_9 - t_8 - 1); +frame.set("loop.first", t_8 === 0); +frame.set("loop.last", t_8 === t_9 - 1); +frame.set("loop.length", t_9); +output += "\n
  • "; +output += runtime.suppressValue(t_12, env.opts.autoescape); +output += "
  • \n "; +; +} +} else { +t_8 = -1; +var t_9 = runtime.keys(t_10).length; +for(var t_13 in t_10) { +t_8++; +var t_14 = t_10[t_13]; +frame.set("format", t_13); +frame.set("formatname", t_14); +frame.set("loop.index", t_8 + 1); +frame.set("loop.index0", t_8); +frame.set("loop.revindex", t_9 - t_8); +frame.set("loop.revindex0", t_9 - t_8 - 1); +frame.set("loop.first", t_8 === 0); +frame.set("loop.last", t_8 === t_9 - 1); +frame.set("loop.length", t_9); +output += "\n
  • "; +output += runtime.suppressValue(t_14, env.opts.autoescape); +output += "
  • \n "; +; +} +} +} +frame = frame.pop(); +output += "\n
\n
\n \n \n \n
\n\n"; if(parentTemplate) { parentTemplate.rootRenderFunc(env, context, frame, runtime, cb); } else { diff --git a/quarry/web/static/templates/query-resultset.html b/quarry/web/static/templates/query-resultset.html index cc18caf2..9f28c120 100644 --- a/quarry/web/static/templates/query-resultset.html +++ b/quarry/web/static/templates/query-resultset.html @@ -9,22 +9,42 @@ ({{rowcount}} rows)
-
- - +
+ +
+ + +
+
+ + +
From b6bc40ec26e9c3a36d77f6caedc416c53e85ce25 Mon Sep 17 00:00:00 2001 From: Travis Briggs Date: Thu, 13 Aug 2026 06:21:17 -0700 Subject: [PATCH 2/3] Put "Copy link" left of "Download data" and style it as secondary Downloading is the primary action for most people looking at a resultset, so it keeps btn-info and the rightmost position. "Copy link" is the secondary action and moves to its left as btn-default. The right-alignment on the dropdown menu moves with it, so it is the rightmost menu that stays inside the column. Co-Authored-By: Claude Opus 5 Bug: T434787 --- quarry/web/static/templates/compiled.js | 36 +++++++++---------- .../web/static/templates/query-resultset.html | 14 ++++---- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/quarry/web/static/templates/compiled.js b/quarry/web/static/templates/compiled.js index 35e351d9..c693a9f0 100644 --- a/quarry/web/static/templates/compiled.js +++ b/quarry/web/static/templates/compiled.js @@ -18,7 +18,7 @@ output += "\n "; } output += "\n ("; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "rowcount"), env.opts.autoescape); -output += " rows)\n \n
\n
\n \n
\n \n
    \n "; +output += " rows)\n
\n
\n
\n \n
\n \n
    \n "; frame = frame.push(); var t_3 = {"tsv": "TSV","json": "JSON","json-lines": "JSON Lines","csv": "CSV","wikitable": "Wikitable","html": "HTML"}; if(t_3) {var t_1; @@ -42,14 +42,6 @@ output += "/output/"; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "resultset_id"), env.opts.autoescape); output += "/"; output += runtime.suppressValue(t_4, env.opts.autoescape); -output += "\" download=\"quarry-"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "query_id"), env.opts.autoescape); -output += "-"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "slugify_title"), env.opts.autoescape); -output += "-run"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "run_id"), env.opts.autoescape); -output += "."; -output += runtime.suppressValue(t_4, env.opts.autoescape); output += "\">"; output += runtime.suppressValue(t_5, env.opts.autoescape); output += "\n "; @@ -76,14 +68,6 @@ output += "/output/"; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "resultset_id"), env.opts.autoescape); output += "/"; output += runtime.suppressValue(t_6, env.opts.autoescape); -output += "\" download=\"quarry-"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "query_id"), env.opts.autoescape); -output += "-"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "slugify_title"), env.opts.autoescape); -output += "-run"; -output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "run_id"), env.opts.autoescape); -output += "."; -output += runtime.suppressValue(t_6, env.opts.autoescape); output += "\">"; output += runtime.suppressValue(t_7, env.opts.autoescape); output += "\n "; @@ -92,7 +76,7 @@ output += "\n "; } } frame = frame.pop(); -output += "\n
\n
\n
\n \n
    \n "; +output += "\n
\n
\n
\n \n
    \n "; frame = frame.push(); var t_10 = {"tsv": "TSV","json": "JSON","json-lines": "JSON Lines","csv": "CSV","wikitable": "Wikitable","html": "HTML"}; if(t_10) {var t_8; @@ -116,6 +100,14 @@ output += "/output/"; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "resultset_id"), env.opts.autoescape); output += "/"; output += runtime.suppressValue(t_11, env.opts.autoescape); +output += "\" download=\"quarry-"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "query_id"), env.opts.autoescape); +output += "-"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "slugify_title"), env.opts.autoescape); +output += "-run"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "run_id"), env.opts.autoescape); +output += "."; +output += runtime.suppressValue(t_11, env.opts.autoescape); output += "\">"; output += runtime.suppressValue(t_12, env.opts.autoescape); output += "\n "; @@ -142,6 +134,14 @@ output += "/output/"; output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "resultset_id"), env.opts.autoescape); output += "/"; output += runtime.suppressValue(t_13, env.opts.autoescape); +output += "\" download=\"quarry-"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "query_id"), env.opts.autoescape); +output += "-"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "slugify_title"), env.opts.autoescape); +output += "-run"; +output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "run_id"), env.opts.autoescape); +output += "."; +output += runtime.suppressValue(t_13, env.opts.autoescape); output += "\">"; output += runtime.suppressValue(t_14, env.opts.autoescape); output += "\n "; diff --git a/quarry/web/static/templates/query-resultset.html b/quarry/web/static/templates/query-resultset.html index 9f28c120..554ea209 100644 --- a/quarry/web/static/templates/query-resultset.html +++ b/quarry/web/static/templates/query-resultset.html @@ -12,10 +12,10 @@
    - -
    -
    From 32f64df924347c408706de596e724d2f13f8c36f Mon Sep 17 00:00:00 2001 From: Github Action Date: Thu, 13 Aug 2026 13:50:58 +0000 Subject: [PATCH 3/3] auto update of tag --- helm-quarry/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm-quarry/values.yaml b/helm-quarry/values.yaml index 3cbae97e..c01f42a7 100644 --- a/helm-quarry/values.yaml +++ b/helm-quarry/values.yaml @@ -1,6 +1,6 @@ web: repository: 'quay.io/wikimedia-quarry/quarry' - tag: pr-96 # web tag managed by github actions + tag: pr-99 # web tag managed by github actions resources: requests: memory: "300Mi" @@ -11,7 +11,7 @@ web: worker: repository: 'quay.io/wikimedia-quarry/quarry' - tag: pr-96 # worker tag managed by github actions + tag: pr-99 # worker tag managed by github actions resources: requests: memory: "400Mi"