From 4e3fe065bf9a4205123c4ef5cfe75dc7e241242b Mon Sep 17 00:00:00 2001 From: Igor Gaponenko Date: Thu, 12 Sep 2024 17:40:53 -0700 Subject: [PATCH] Web Dashboard: restricted query rendering, full query text download option --- src/www/qserv/css/QservCzarMySQLQueries.css | 2 +- src/www/qserv/css/QservWorkerMySQLQueries.css | 2 +- src/www/qserv/css/QservWorkerQueries.css | 2 +- src/www/qserv/css/StatusQueryInspector.css | 4 ++ src/www/qserv/js/Common.js | 14 +++++- src/www/qserv/js/QservCzarMySQLQueries.js | 10 ++++ src/www/qserv/js/QservWorkerMySQLQueries.js | 49 ++++++++++++------- src/www/qserv/js/QservWorkerQueries.js | 14 +++++- src/www/qserv/js/StatusActiveQueries.js | 10 ++++ src/www/qserv/js/StatusPastQueries.js | 10 ++++ src/www/qserv/js/StatusQueryInspector.js | 26 +++++++++- 11 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/www/qserv/css/QservCzarMySQLQueries.css b/src/www/qserv/css/QservCzarMySQLQueries.css index fe1f6806d0..fec01f3e8c 100644 --- a/src/www/qserv/css/QservCzarMySQLQueries.css +++ b/src/www/qserv/css/QservCzarMySQLQueries.css @@ -13,7 +13,7 @@ table#fwk-czar-mysql-queries > thead > tr > th.sticky { } table#fwk-czar-mysql-queries tbody th, table#fwk-czar-mysql-queries tbody td { - vertical-align:middle; + vertical-align:top; } table#fwk-czar-mysql-queries pre { padding: 0; diff --git a/src/www/qserv/css/QservWorkerMySQLQueries.css b/src/www/qserv/css/QservWorkerMySQLQueries.css index 3e681745bb..0bb3afa1d9 100644 --- a/src/www/qserv/css/QservWorkerMySQLQueries.css +++ b/src/www/qserv/css/QservWorkerMySQLQueries.css @@ -13,7 +13,7 @@ table#fwk-worker-mysql-queries > thead > tr > th.sticky { } table#fwk-worker-mysql-queries tbody th, table#fwk-worker-mysql-queries tbody td { - vertical-align:middle; + vertical-align:top; } table#fwk-worker-mysql-queries pre { padding: 0; diff --git a/src/www/qserv/css/QservWorkerQueries.css b/src/www/qserv/css/QservWorkerQueries.css index 593009abce..05fff85e48 100644 --- a/src/www/qserv/css/QservWorkerQueries.css +++ b/src/www/qserv/css/QservWorkerQueries.css @@ -8,7 +8,7 @@ table#fwk-qserv-queries caption { } table#fwk-qserv-queries tbody th, table#fwk-qserv-queries tbody td { - vertical-align:middle; + vertical-align:top; } table#fwk-qserv-queries pre { padding: 0; diff --git a/src/www/qserv/css/StatusQueryInspector.css b/src/www/qserv/css/StatusQueryInspector.css index 7e2ce078aa..d352c47400 100644 --- a/src/www/qserv/css/StatusQueryInspector.css +++ b/src/www/qserv/css/StatusQueryInspector.css @@ -28,6 +28,10 @@ height:20px; margin-top: 7px; } +#fwk-status-query-info tbody > tr > td > a.download-query { + height:20px; + margin-top: 7px; +} #fwk-status-query-info tbody > tr > td > pre.query_toggler { color:#4d4dff; } diff --git a/src/www/qserv/js/Common.js b/src/www/qserv/js/Common.js index d83bdbd182..f6b9b4012f 100644 --- a/src/www/qserv/js/Common.js +++ b/src/www/qserv/js/Common.js @@ -9,7 +9,18 @@ function(sqlFormatter, static RestAPIVersion = 35; static query2text(query, expanded) { if (expanded) { - return sqlFormatter.format(query, Common._sqlFormatterConfig); + if (query.length > Common._max_expanded_length) { + return sqlFormatter.format( + "************* ATTENTION **************;" + + "*Query has been truncated at " + Common._max_expanded_length + " bytes since it is too long*;" + + "*Click the download button to see the full text of the query*;" + + "********************************;" + + ";" + + query.substring(0, Common._max_expanded_length) + "...", + Common._sqlFormatterConfig); + } else { + return sqlFormatter.format(query, Common._sqlFormatterConfig); + } } else if (query.length > Common._max_compact_length) { return query.substring(0, Common._max_compact_length) + "..."; } else { @@ -18,6 +29,7 @@ function(sqlFormatter, } static _sqlFormatterConfig = {"language":"mysql", "uppercase:":true, "indent":" "}; static _max_compact_length = 120; + static _max_expanded_length = 4096; static _ivals = [ {value: 2, name: '2 sec'}, {value: 5, name: '5 sec'}, diff --git a/src/www/qserv/js/QservCzarMySQLQueries.js b/src/www/qserv/js/QservCzarMySQLQueries.js index a0e06bf2c1..44b7c90298 100644 --- a/src/www/qserv/js/QservCzarMySQLQueries.js +++ b/src/www/qserv/js/QservCzarMySQLQueries.js @@ -21,6 +21,7 @@ function(CSSLoader, // queries between updates. this._id2query = {}; // Store query text for each identifier. The dictionary gets // updated at each refresh of the page. + this._id2url = {}; // Store URL to the query blob for each identifier } fwk_app_on_show() { console.log('show: ' + this.fwk_app_name); @@ -70,6 +71,7 @@ function(CSSLoader, Time State + Query @@ -132,6 +134,7 @@ function(CSSLoader, } _display(queries) { const queryCopyTitle = "Click to copy the query text to the clipboard."; + const queryDownloadTitle = "Click to download the query text to your computer."; const COL_Id = 0, COL_Command = 4, COL_Time = 5, COL_State = 6, COL_Info = 7; let tbody = this._table().children('tbody'); if (_.isEmpty(queries.columns)) { @@ -139,6 +142,9 @@ function(CSSLoader, return; } this._id2query = {}; + for (let id in this._id2url) { + URL.revokeObjectURL(this._id2url[id]); + } let html = ''; for (let i in queries.rows) { let row = queries.rows[i]; @@ -146,6 +152,7 @@ function(CSSLoader, let queryId = row[COL_Id]; let query = row[COL_Info]; this._id2query[queryId] = query; + this._id2url[queryId] = URL.createObjectURL(new Blob([query], {type: "text/plain"})); const expanded = (queryId in this._queryId2Expanded) && this._queryId2Expanded[queryId]; const queryToggleTitle = "Click to toggle query formatting."; const queryStyle = "color:#4d4dff;"; @@ -157,6 +164,9 @@ function(CSSLoader, + + +
` + this._query2text(queryId, expanded) + `

 `;
             }
diff --git a/src/www/qserv/js/QservWorkerMySQLQueries.js b/src/www/qserv/js/QservWorkerMySQLQueries.js
index 2c89cb4f4d..3d2f09a8fc 100644
--- a/src/www/qserv/js/QservWorkerMySQLQueries.js
+++ b/src/www/qserv/js/QservWorkerMySQLQueries.js
@@ -17,10 +17,11 @@ function(CSSLoader,
 
         constructor(name) {
             super(name);
-            this._mySqlThreadId2Expanded = {};  // Store 'true' to allow persistent state for the expanded
+            this._mysqlThreadId2Expanded = {};  // Store 'true' to allow persistent state for the expanded
                                                 // queries between updates.
-            this._mySqlThreaId2query = {};      // Store query text for each identifier. The dictionary gets
+            this._mysqlThreadId2query = {};     // Store query text for each identifier. The dictionary gets
                                                 // updated at each refresh of the page.
+            this._mysqlThreadId2url = {};       // Store URL to the query blob for each identifier
         }
         fwk_app_on_show() {
             console.log('show: ' + this.fwk_app_name);
@@ -101,6 +102,7 @@ function(CSSLoader,
            
            
            
+           
         
         
           QID
@@ -115,6 +117,7 @@ function(CSSLoader,
           Command
           State
           
+          
           Query
         
       
@@ -217,6 +220,7 @@ function(CSSLoader,
         _display(status) {
             const queryInspectTitle = "Click to see detailed info (progress, messages, etc.) on the query.";
             const queryCopyTitle = "Click to copy the query text to the clipboard.";
+            const queryDownloadTitle = "Click to download the query text to your computer.";
             const COL_Id = 0, COL_Command = 4, COL_Time = 5, COL_State = 6, COL_Info = 7;
             const desiredQueryCommand = this._query_command();
             let tbody = this._table().children('tbody');
@@ -224,7 +228,10 @@ function(CSSLoader,
                 tbody.html('');
                 return;
             }
-            this._mySqlThreaId2query = {};
+            this._mysqlThreadId2query = {};
+            for (let id in this._mysqlThreadId2url) {
+                URL.revokeObjectURL(this._mysqlThreadId2url[id]);
+            }
             let numQueriesTotal = 0;
             let numQueriesDisplayed = 0;
             let html = '';
@@ -234,10 +241,11 @@ function(CSSLoader,
                 let row = status.queries.rows[i];
                 const thisQueryCommand = row[COL_Command];
                 if ((desiredQueryCommand !== '') && (thisQueryCommand !== desiredQueryCommand)) continue;
-                let mySqlThreadId = row[COL_Id];
+                let mysqlThreadId = row[COL_Id];
                 let query = row[COL_Info];
-                this._mySqlThreaId2query[mySqlThreadId] = query;
-                const expanded = (mySqlThreadId in this._mySqlThreadId2Expanded) && this._mySqlThreadId2Expanded[mySqlThreadId];
+                this._mysqlThreadId2query[mysqlThreadId] = query;
+                this._mysqlThreadId2url[mysqlThreadId] = URL.createObjectURL(new Blob([query], {type: "text/plain"}));
+                const expanded = (mysqlThreadId in this._mysqlThreadId2Expanded) && this._mysqlThreadId2Expanded[mysqlThreadId];
                 const queryToggleTitle = "Click to toggle query formatting.";
                 const queryStyle = "color:#4d4dff;";
                 // Task context (if any)
@@ -247,8 +255,8 @@ function(CSSLoader,
                 let subChunkId = '';
                 let templateId = '';
                 let state = '';
-                if (_.has(status.mysql_thread_to_task, mySqlThreadId)) {
-                    let task = status.mysql_thread_to_task[mySqlThreadId];
+                if (_.has(status.mysql_thread_to_task, mysqlThreadId)) {
+                    let task = status.mysql_thread_to_task[mysqlThreadId];
                     queryId    = task['query_id'];
                     jobId      = task['job_id'];
                     chunkId    = task['chunk_id'];
@@ -258,7 +266,7 @@ function(CSSLoader,
                 }
                 const rowClass = QservWorkerMySQLQueries._state2css(state);
                 html += `
-
+
   
${queryId}
`; if (queryId === '') { html += ` @@ -275,7 +283,7 @@ function(CSSLoader,
${subChunkId}
${templateId}
${state}
-
${mySqlThreadId}
+
${mysqlThreadId}
${row[COL_Time]}
${row[COL_Command]}
${row[COL_State]}
`; @@ -289,7 +297,10 @@ function(CSSLoader, -
` + this._query2text(mySqlThreadId, expanded) + `
`;
+  
+    
+  
+  
` + this._query2text(mysqlThreadId, expanded) + `
`;
                 }
                 html += `
 `;
@@ -305,8 +316,8 @@ function(CSSLoader,
             };
             let copyQueryToClipboard = function(e) {
                 let button = $(e.currentTarget);
-                let mySqlThreadId = button.parent().parent().attr("mysql_thread_id");
-                let query = that._mySqlThreaId2query[mySqlThreadId];
+                let mysqlThreadId = button.parent().parent().attr("mysql_thread_id");
+                let query = that._mysqlThreadId2query[mysqlThreadId];
                 navigator.clipboard.writeText(query,
                     () => {},
                     () => { alert("Failed to write the query to the clipboard. Please copy the text manually: " + query); }
@@ -315,18 +326,18 @@ function(CSSLoader,
             let toggleQueryDisplay = function(e) {
                 let td = $(e.currentTarget);
                 let pre = td.find("pre.query");
-                const mySqlThreadId = td.parent().attr("mysql_thread_id");
-                const expanded = !((mySqlThreadId in that._mySqlThreadId2Expanded) && that._mySqlThreadId2Expanded[mySqlThreadId]);
-                pre.text(that._query2text(mySqlThreadId, expanded));
-                that._mySqlThreadId2Expanded[mySqlThreadId] = expanded;
+                const mysqlThreadId = td.parent().attr("mysql_thread_id");
+                const expanded = !((mysqlThreadId in that._mysqlThreadId2Expanded) && that._mysqlThreadId2Expanded[mysqlThreadId]);
+                pre.text(that._query2text(mysqlThreadId, expanded));
+                that._mysqlThreadId2Expanded[mysqlThreadId] = expanded;
             };
             tbody.find("button.inspect-query").click(displayQuery);
             tbody.find("button.copy-query").click(copyQueryToClipboard);
             tbody.find("td.query_toggler").click(toggleQueryDisplay);
             this._set_num_queries(numQueriesTotal, numQueriesDisplayed);
         }
-        _query2text(mySqlThreadId, expanded) {
-            return Common.query2text(this._mySqlThreaId2query[mySqlThreadId], expanded);
+        _query2text(mysqlThreadId, expanded) {
+            return Common.query2text(this._mysqlThreadId2query[mysqlThreadId], expanded);
         }
         static _state2css(state) {
             switch (state) {
diff --git a/src/www/qserv/js/QservWorkerQueries.js b/src/www/qserv/js/QservWorkerQueries.js
index 49b02d28b0..54d70ba9d7 100644
--- a/src/www/qserv/js/QservWorkerQueries.js
+++ b/src/www/qserv/js/QservWorkerQueries.js
@@ -21,6 +21,7 @@ function(CSSLoader,
                                             // queries between updates.
             this._id2query = {};            // Store query text for each identifier. The dictionary gets
                                             // updated at each refresh of the page.
+            this._id2url = {};              // Store URL to the query blob for each identifier
         }
         fwk_app_on_show() {
             console.log('show: ' + this.fwk_app_name);
@@ -71,6 +72,7 @@ function(CSSLoader,
           #tasks
           qid
           
+          
           
           query
         
@@ -142,11 +144,15 @@ function(CSSLoader,
          */
         _display(data) {
             const queryCopyTitle = "Click to copy the query text to the clipboard.";
+            const queryDownloadTitle = "Click to download the query text to your computer.";
             const queryInspectTitle = "Click to see detailed info (progress, messages, etc.) on the query.";
             const queryToggleTitle = "Click to toggle query formatting.";
             const queryStyle = "color:#4d4dff;";
             let html = '';
             this._id2query = {};
+            for (let id in this._id2url) {
+                URL.revokeObjectURL(this._id2url[id]);
+            }
             for (let worker in data) {
                 if (!data[worker].success) {
                     html += `
@@ -173,15 +179,19 @@ function(CSSLoader,
                             const queryId  = scheduler.query_id_to_count[j][0];
                             const numTasks = scheduler.query_id_to_count[j][1];
                             this._id2query[queryId] = queries[queryId].query;
+                            this._id2url[queryId] = URL.createObjectURL(new Blob([queries[queryId].query], {type: "text/plain"}));
                             const expanded = (queryId in this._queryId2Expanded) && this._queryId2Expanded[queryId];
                             htmlSchedulerQueries += `
 
   
${numTasks}
-
${queryId}
+
${queryId}
- + + + +
` + this._query2text(queryId, expanded) + `

diff --git a/src/www/qserv/js/StatusActiveQueries.js b/src/www/qserv/js/StatusActiveQueries.js
index e13cb6e829..a1049bb437 100644
--- a/src/www/qserv/js/StatusActiveQueries.js
+++ b/src/www/qserv/js/StatusActiveQueries.js
@@ -23,6 +23,7 @@ function(CSSLoader,
             this._queryId2Expanded = {};  // Store 'true' to allow persistent state for the expanded
                                           // queries between updates.
             this._id2query = {};          // Store query text for each identifier
+            this._id2url = {};            // Store URL to the query blob for each identifier
         }
 
         /**
@@ -108,6 +109,7 @@ function(CSSLoader,
           Czar
           QID
           
+          
           
           
           Query
@@ -184,8 +186,12 @@ function(CSSLoader,
         }
         _display(data) {
             this._id2query = {};
+            for (let id in this._id2url) {
+                URL.revokeObjectURL(this._id2url[id]);
+            }
             const queryToggleTitle = "Click to toggle query formatting.";
             const queryCopyTitle = "Click to copy the query text to the clipboard.";
+            const queryDownloadTitle = "Click to download the query text to your computer.";
             const queryInspectTitle = "Click to see detailed info (progress, messages, etc.) on the query.";
             const queryProgressTitle = "Click to see query progression plot.";
             const queryStyle = "color:#4d4dff;";
@@ -193,6 +199,7 @@ function(CSSLoader,
             for (let i in data.queries) {
                 let query = data.queries[i];
                 this._id2query[query.queryId] = query.query;
+                this._id2url[query.queryId] = URL.createObjectURL(new Blob([query.query], {type: "text/plain"}));
                 const progress = Math.floor(100. * query.completedChunks  / query.totalChunks);
                 const scheduler = _.isUndefined(query.scheduler) ? 'Loading...' : query.scheduler.substring('Sched'.length);
                 const scheduler_color = _.has(this._scheduler2color, scheduler) ?
@@ -231,6 +238,9 @@ function(CSSLoader,
   
     
   
+  
+    
+  
   
     
   
diff --git a/src/www/qserv/js/StatusPastQueries.js b/src/www/qserv/js/StatusPastQueries.js
index 9fb92782da..1d2a5b5a47 100644
--- a/src/www/qserv/js/StatusPastQueries.js
+++ b/src/www/qserv/js/StatusPastQueries.js
@@ -23,6 +23,7 @@ function(CSSLoader,
             this._queryId2Expanded = {};  // Store 'true' to allow persistent state for the expanded
                                           // queries between updates.
             this._id2query = {};          // Store query text for each identifier
+            this._id2url = {};            // Store URL to the query blob for each identifier
         }
 
         /**
@@ -164,6 +165,7 @@ function(CSSLoader,
           Czar
           QID
           
+          
           
           Query
         
@@ -267,8 +269,12 @@ function(CSSLoader,
         }
         _display(data) {
             this._id2query = {};
+            for (let id in this._id2url) {
+                URL.revokeObjectURL(this._id2url[id]);
+            }
             const queryToggleTitle = "Click to toggle query formatting.";
             const queryCopyTitle = "Click to copy the query text to the clipboard.";
+            const queryDownloadTitle = "Click to download the query text to your computer.";
             const queryInspectTitle = "Click to see detailed info (progress, messages, etc.) on the query.";
             const queryStyle = "color:#4d4dff;";
             let html = '';
@@ -299,6 +305,7 @@ function(CSSLoader,
             for (let i in data.queries_past) {
                 let query = data.queries_past[i];
                 this._id2query[query.queryId] = query.query;
+                this._id2url[query.queryId] = URL.createObjectURL(new Blob([query.query], {type: "text/plain"}));
                 let elapsed = this._elapsed(query.completed_sec - query.submitted_sec);
                 let failed_query_class = query.status !== "COMPLETED" ? "table-danger" : "";
                 let performance = this._performance(query.chunkCount, query.completed_sec - query.submitted_sec);
@@ -319,6 +326,9 @@ function(CSSLoader,
   
     
   
+  
+    
+  
   
     
   
diff --git a/src/www/qserv/js/StatusQueryInspector.js b/src/www/qserv/js/StatusQueryInspector.js
index 5d640fca98..1d5860e729 100644
--- a/src/www/qserv/js/StatusQueryInspector.js
+++ b/src/www/qserv/js/StatusQueryInspector.js
@@ -23,6 +23,7 @@ function(CSSLoader,
             // Store 'true' to allow persistent state of the query display for
             // the expanded queries between updates.
             this._expanded = {'query': false, 'qTemplate': false, 'qMerge': false, 'resultQuery': false};
+            this._download_url = {};
         }
 
         /**
@@ -77,6 +78,7 @@ function(CSSLoader,
             if (this._initialized) return;
             this._initialized = true;
             const queryCopyTitle = "Click to copy the query text to the clipboard.";
+            const queryDownloadTitle = "Click to download the query text to your computer.";
             const queryToggleTitle = "Click to toggle query formatting.";
             let html = `
 
@@ -139,7 +141,8 @@ function(CSSLoader,   - + +   @@ -149,6 +152,9 @@ function(CSSLoader, + + +

         
         
@@ -156,6 +162,9 @@ function(CSSLoader,
           
             
           
+          
+            
+          
           

         
         
@@ -163,6 +172,9 @@ function(CSSLoader,
           
             
           
+          
+            
+          
           

         
         
@@ -170,6 +182,9 @@ function(CSSLoader,
           
             
           
+          
+            
+          
           

         
       
@@ -311,6 +326,11 @@ function(CSSLoader,
             this._status().removeClass('updating');
             this._loading = false;
         }
+        _set_query_download_url(target, query) {
+            if (!_.isUndefined(this._download_url[target])) URL.revokeObjectURL(this._download_url[target]);
+            this._download_url[target] = URL.createObjectURL(new Blob([query]));
+            this._query_status().find("a.download-query[target='" + target + "']").attr("href", this._download_url[target], {type: "text/plain"});
+        }
         _display(info) {
             this._info = info;
             this._set_query_info_state("status", `
${info.status}
`); @@ -323,9 +343,13 @@ function(CSSLoader, this._set_query_info("completed", info.completed ? (new Date(info.completed)).toLocalTimeString('iso') : ""); this._set_query_info("returned", info.returned ? (new Date(info.returned)).toLocalTimeString('iso') : ""); this._set_query_info("query", Common.query2text(info.query, this._expanded["query"])); + this._set_query_download_url("query", info.query); this._set_query_info("qTemplate", Common.query2text(info.qTemplate, this._expanded["qTemplate"])); + this._set_query_download_url("qTemplate", info.qTemplate); this._set_query_info("qMerge", Common.query2text(info.qMerge, this._expanded["qMerge"])); + this._set_query_download_url("qMerge", info.qMerge); this._set_query_info("resultQuery", Common.query2text(info.resultQuery, this._expanded["resultQuery"])); + this._set_query_download_url("resultQuery", info.resultQuery); let html = ''; for (let i in info.messages) { let msg = info.messages[i];