diff --git a/src/www/qserv/css/QservWorkerMySQLQueries.css b/src/www/qserv/css/QservWorkerMySQLQueries.css
new file mode 100644
index 000000000..3e681745b
--- /dev/null
+++ b/src/www/qserv/css/QservWorkerMySQLQueries.css
@@ -0,0 +1,27 @@
+#fwk-worker-mysql-queries-controls label {
+ font-weight: bold;
+}
+table#fwk-worker-mysql-queries caption {
+ caption-side: top;
+ text-align: right;
+ padding-top: 0;
+}
+table#fwk-worker-mysql-queries > thead > tr > th.sticky {
+ position:sticky;
+ top:80px;
+ z-index:2;
+}
+table#fwk-worker-mysql-queries tbody th,
+table#fwk-worker-mysql-queries tbody td {
+ vertical-align:middle;
+}
+table#fwk-worker-mysql-queries pre {
+ padding: 0;
+ margin: 0;
+}
+table#fwk-worker-mysql-queries tbody > tr > td.query_toggler:hover {
+ cursor:pointer;
+}
+table#fwk-worker-mysql-queries caption.updating {
+ background-color: #ffeeba;
+}
diff --git a/src/www/qserv/js/Common.js b/src/www/qserv/js/Common.js
index d74d31fb7..6fd085e40 100644
--- a/src/www/qserv/js/Common.js
+++ b/src/www/qserv/js/Common.js
@@ -3,7 +3,7 @@ define([
function(sqlFormatter) {
class Common {
- static RestAPIVersion = 23;
+ static RestAPIVersion = 24;
static query2text(query, expanded) {
if (expanded) {
return sqlFormatter.format(query, Common._sqlFormatterConfig);
diff --git a/src/www/qserv/js/QservMonitoringDashboard.js b/src/www/qserv/js/QservMonitoringDashboard.js
index 3e94a2cdc..674b62f8e 100644
--- a/src/www/qserv/js/QservMonitoringDashboard.js
+++ b/src/www/qserv/js/QservMonitoringDashboard.js
@@ -43,6 +43,7 @@ require([
'qserv/StatusUserQueries',
'qserv/QservCss',
'qserv/QservMySQLConnections',
+ 'qserv/QservWorkerMySQLQueries',
'qserv/QservWorkerQueries',
'qserv/QservWorkerSchedulers',
'qserv/QservWorkerSchedulerHist',
@@ -81,6 +82,7 @@ function(CSSLoader,
StatusUserQueries,
QservCss,
QservMySQLConnections,
+ QservWorkerMySQLQueries,
QservWorkerQueries,
QservWorkerSchedulers,
QservWorkerSchedulerHist,
@@ -169,6 +171,7 @@ function(CSSLoader,
{ name: 'Workers',
apps: [
new QservMySQLConnections('MySQL Connections'),
+ new QservWorkerMySQLQueries('MySQL Queries'),
new QservWorkerQueries('Queries in Worker Queues'),
new QservWorkerSchedulers('Schedulers'),
new QservWorkerSchedulerHist('Scheduler Histograms'),
diff --git a/src/www/qserv/js/QservWorkerMySQLQueries.js b/src/www/qserv/js/QservWorkerMySQLQueries.js
new file mode 100644
index 000000000..a5d033351
--- /dev/null
+++ b/src/www/qserv/js/QservWorkerMySQLQueries.js
@@ -0,0 +1,238 @@
+define([
+ 'webfwk/CSSLoader',
+ 'webfwk/Fwk',
+ 'webfwk/FwkApplication',
+ 'qserv/Common',
+ 'underscore'],
+
+function(CSSLoader,
+ Fwk,
+ FwkApplication,
+ Common,
+ _) {
+
+ CSSLoader.load('qserv/css/QservWorkerMySQLQueries.css');
+
+ class QservWorkerMySQLQueries extends FwkApplication {
+
+ constructor(name) {
+ super(name);
+ this._queryId2Expanded = {}; // Store 'true' to allow persistent state for the expanded
+ // queries between updates.
+ this._id2query = {}; // Store query text for each identifier. The dictionary gets
+ // updated at each refresh of the page.
+ }
+ fwk_app_on_show() {
+ console.log('show: ' + this.fwk_app_name);
+ this.fwk_app_on_update();
+ }
+ fwk_app_on_hide() {
+ console.log('hide: ' + this.fwk_app_name);
+ }
+ fwk_app_on_update() {
+ if (this.fwk_app_visible) {
+ this._init();
+ if (this._prev_update_sec === undefined) {
+ this._prev_update_sec = 0;
+ }
+ let now_sec = Fwk.now().sec;
+ if (now_sec - this._prev_update_sec > this._update_interval_sec()) {
+ this._prev_update_sec = now_sec;
+ this._init();
+ this._load();
+ }
+ }
+ }
+ _init() {
+ if (this._initialized === undefined) this._initialized = false;
+ if (this._initialized) return;
+ this._initialized = true;
+ let html = `
+
+
+
+
+
+
+ Id |
+ Time |
+ State |
+ |
+ Query |
+
+
+ Loading...
+
+
+
+
`;
+ let cont = this.fwk_app_container.html(html);
+ cont.find(".form-control-selector").change(() => {
+ this._load();
+ });
+ cont.find("button#reset-controls-form").click(() => {
+ this._set_update_interval_sec(10);
+ this._load();
+ });
+ }
+ _form_control(elem_type, id) {
+ if (this._form_control_obj === undefined) this._form_control_obj = {};
+ if (!_.has(this._form_control_obj, id)) {
+ this._form_control_obj[id] = this.fwk_app_container.find(elem_type + '#' + id);
+ }
+ return this._form_control_obj[id];
+ }
+ _update_interval_sec() { return this._form_control('select', 'update-interval').val(); }
+ _set_update_interval_sec(val) { this._form_control('select', 'update-interval').val(val); }
+ _worker() { return this._form_control('select', 'worker').val(); }
+ _set_worker(val) { this._form_control('select', 'worker').val(val); }
+ _set_workers(workers) {
+ const prev_worker = this._worker();
+ let html = '';
+ for (let i in workers) {
+ const worker = workers[i];
+ const selected = (_.isEmpty(prev_worker) && (i === 0)) ||
+ (!_.isEmpty(prev_worker) && (prev_worker === worker));
+ html += `
+ `;
+ }
+ this._form_control('select', 'worker').html(html);
+ }
+ _table() {
+ if (this._table_obj === undefined) {
+ this._table_obj = this.fwk_app_container.find('table#fwk-worker-mysql-queries');
+ }
+ return this._table_obj;
+ }
+ _load() {
+ if (this._loading === undefined) this._loading = false;
+ if (this._loading) return;
+ this._loading = true;
+ this._table().children('caption').addClass('updating');
+ Fwk.web_service_GET(
+ "/replication/config",
+ {version: Common.RestAPIVersion},
+ (data) => {
+ let workers = [];
+ for (let i in data.config.workers) {
+ workers.push(data.config.workers[i].name);
+ }
+ this._set_workers(workers);
+ this._load_queries();
+ },
+ (msg) => {
+ console.log('request failed', this.fwk_app_name, msg);
+ this._table().children('caption').html('No Response');
+ this._table().children('caption').removeClass('updating');
+ this._loading = false;
+ }
+ );
+ }
+ _load_queries() {
+ Fwk.web_service_GET(
+ "/replication/qserv/worker/db/" + this._worker(),
+ { timeout_sec: 2, version: Common.RestAPIVersion
+ },
+ (data) => {
+ if (data.success) {
+ this._display(data.status.queries);
+ Fwk.setLastUpdate(this._table().children('caption'));
+ } else {
+ console.log('request failed', this.fwk_app_name, data.error);
+ this._table().children('caption').html('' + data.error + '');
+ }
+ this._table().children('caption').removeClass('updating');
+ this._loading = false;
+ },
+ (msg) => {
+ console.log('request failed', this.fwk_app_name, msg);
+ this._table().children('caption').html('No Response');
+ this._table().children('caption').removeClass('updating');
+ this._loading = false;
+ }
+ );
+ }
+ _display(queries) {
+ const queryCopyTitle = "Click to copy the query text to the clipboard.";
+ 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)) {
+ tbody.html('');
+ return;
+ }
+ this._id2query = {};
+ let html = '';
+ for (let i in queries.rows) {
+ let row = queries.rows[i];
+ if (row[COL_Command] !== 'Query') continue;
+ let queryId = row[COL_Id];
+ let query = row[COL_Info];
+ this._id2query[queryId] = query;
+ const expanded = (queryId in this._queryId2Expanded) && this._queryId2Expanded[queryId];
+ const queryToggleTitle = "Click to toggle query formatting.";
+ const queryStyle = "color:#4d4dff;";
+ html += `
+
+ ${queryId} |
+ ${row[COL_Time]} |
+ ${row[COL_State]} |
+
+
+ |
+ ` + this._query2text(queryId, expanded) + ` |
+
`;
+ }
+ tbody.html(html);
+ let that = this;
+ let copyQueryToClipboard = function(e) {
+ let button = $(e.currentTarget);
+ let queryId = button.parent().parent().attr("id");
+ let query = that._id2query[queryId];
+ navigator.clipboard.writeText(query,
+ () => {},
+ () => { alert("Failed to write the query to the clipboard. Please copy the text manually: " + query); }
+ );
+ };
+ let toggleQueryDisplay = function(e) {
+ let td = $(e.currentTarget);
+ let pre = td.find("pre.query");
+ const queryId = td.parent().attr("id");
+ const expanded = !((queryId in that._queryId2Expanded) && that._queryId2Expanded[queryId]);
+ pre.text(that._query2text(queryId, expanded));
+ that._queryId2Expanded[queryId] = expanded;
+ };
+ tbody.find("button.copy-query").click(copyQueryToClipboard);
+ tbody.find("td.query_toggler").click(toggleQueryDisplay);
+ }
+ _query2text(queryId, expanded) {
+ return Common.query2text(this._id2query[queryId], expanded);
+ }
+ }
+ return QservWorkerMySQLQueries;
+});