diff --git a/grafana/dashboards/maintainer-reviews.json b/grafana/dashboards/maintainer-reviews.json index a8892e0b0..8049fb689 100644 --- a/grafana/dashboards/maintainer-reviews.json +++ b/grafana/dashboards/maintainer-reviews.json @@ -45,7 +45,7 @@ "gridPos": { "h": 4, "w": 4, "x": 0, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS prs FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -55,7 +55,7 @@ "gridPos": { "h": 4, "w": 4, "x": 4, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "green" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS merged FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='merged' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -65,7 +65,7 @@ "gridPos": { "h": 4, "w": 4, "x": 8, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "red" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -76,7 +76,7 @@ "gridPos": { "h": 4, "w": 4, "x": 12, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "orange" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS manual FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (status='manual' OR verdict='manual') AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -87,7 +87,7 @@ "gridPos": { "h": 4, "w": 4, "x": 16, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS commented FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='commented' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -98,7 +98,7 @@ "gridPos": { "h": 4, "w": 4, "x": 20, "y": 1 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "purple" }, "unit": "short" }, "overrides": [] }, "options": { "colorMode": "background", "graphMode": "none", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false }, "textMode": "value" }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS ignored FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND status='manual' AND verdict IS NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "row", @@ -116,7 +116,7 @@ "gridPos": { "h": 4, "w": 8, "x": 0, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "blue" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS opened FROM issues WHERE ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS opened FROM issues WHERE ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS opened FROM issues WHERE (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(created_at) >= ${__from:date:seconds} AND unixepoch(created_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -127,7 +127,7 @@ "gridPos": { "h": 4, "w": 8, "x": 8, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "red" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}", "rawQueryText": "SELECT count(*) AS closed FROM issues WHERE state='closed' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds}" }] }, { "type": "stat", @@ -138,7 +138,7 @@ "gridPos": { "h": 4, "w": 8, "x": 16, "y": 6 }, "fieldConfig": { "defaults": { "color": { "mode": "fixed", "fixedColor": "orange" } } }, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "center", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND ('$repo' = '$__all' OR repo = '$repo')", "rawQueryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND ('$repo' = '$__all' OR repo = '$repo')" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})", "rawQueryText": "SELECT count(*) AS open FROM issues WHERE state='open' AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})" }] }, { "type": "table", @@ -172,7 +172,7 @@ ] }, "options": { "showHeader": true, "cellHeight": "sm", "footer": { "show": false }, "sortBy": [{ "displayName": "updated_at", "desc": true }] }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000", "rawQueryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000", "rawQueryText": "SELECT repo, number, submitter AS author, status, verdict, title, updated_at FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} ORDER BY updated_at DESC LIMIT 1000" }] }, { "type": "timeseries", @@ -182,7 +182,7 @@ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 26 }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "drawStyle": "bars", "fillOpacity": 60, "lineWidth": 1, "showPoints": "never", "stacking": { "mode": "none" } }, "unit": "short" }, "overrides": [] }, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": false }, "tooltip": { "mode": "single", "sort": "none" } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "time series", "timeColumns": ["time"], "queryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time", "rawQueryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "time series", "timeColumns": ["time"], "queryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time", "rawQueryText": "SELECT date(updated_at) AS time, count(*) AS reviews FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY date(updated_at) ORDER BY time" }] }, { "type": "piechart", @@ -192,7 +192,7 @@ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 26 }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "unit": "short" }, "overrides": [] }, "options": { "legend": { "displayMode": "list", "placement": "right", "showLegend": true, "values": ["value", "percent"] }, "pieType": "donut", "reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": true }, "tooltip": { "mode": "single", "sort": "none" } }, - "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC", "rawQueryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND ('$repo' = '$__all' OR repo = '$repo') AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC" }] + "targets": [{ "datasource": { "type": "frser-sqlite-datasource", "uid": "gittensory-db" }, "refId": "A", "queryType": "table", "queryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC", "rawQueryText": "SELECT verdict, count(*) AS c FROM review_targets WHERE (submitter NOT LIKE '%[bot]%' OR submitter IS NULL) AND verdict IS NOT NULL AND (${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring}) AND unixepoch(updated_at) >= ${__from:date:seconds} AND unixepoch(updated_at) < ${__to:date:seconds} GROUP BY verdict ORDER BY c DESC" }] } ] } diff --git a/test/unit/selfhost-grafana-dashboard.test.ts b/test/unit/selfhost-grafana-dashboard.test.ts index 5c8ac2dda..ab02377d6 100644 --- a/test/unit/selfhost-grafana-dashboard.test.ts +++ b/test/unit/selfhost-grafana-dashboard.test.ts @@ -61,14 +61,17 @@ function targetForPanel(panelId: number): DashboardTarget { return target; } -function expandGrafanaRange(query: string): string { +function grafanaSqlString(value: string): string { + return `'${value.replaceAll("'", "''")}'`; +} + +function expandGrafanaRange(query: string, repo = "$__all"): string { const from = Math.floor(Date.parse("2026-06-29T20:00:00Z") / 1000); const to = Math.floor(Date.parse("2026-06-29T22:00:00Z") / 1000); - // Every panel's $repo variable also needs expanding for a real sqlite3 CLI run, same as the time + // Every panel's repo variable also needs expanding for a real sqlite3 CLI run, same as the time // placeholders above -- Grafana's own templating engine does this substitution normally, so a raw - // file-read + direct sqlite3 execution (what these tests do) has to simulate it. Default to "All - // repos" ('$__all' both sides of the OR) unless a caller substitutes a specific repo value first. - return query.replaceAll(timeFrom, String(from)).replaceAll(timeTo, String(to)).replaceAll("'$repo'", "'$__all'"); + // file-read + direct sqlite3 execution (what these tests do) has to simulate its SQL string format. + return query.replaceAll(timeFrom, String(from)).replaceAll(timeTo, String(to)).replaceAll("${repo:sqlstring}", grafanaSqlString(repo)); } function tmpRoot(): string { @@ -337,7 +340,7 @@ describe("maintainer Reviews & PRs Grafana dashboard", () => { expect(target?.queryType).toBe("table"); expect(target?.rawQueryText).toBe(target?.queryText); expect(target?.queryText).toContain("FROM issues"); - expect(target?.queryText).toContain("('$repo' = '$__all' OR repo = '$repo')"); + expect(target?.queryText).toContain("(${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})"); expect(panel?.description?.length ?? 0).toBeGreaterThan(0); } @@ -355,7 +358,7 @@ describe("maintainer Reviews & PRs Grafana dashboard", () => { it("scopes the issue-activity panels to the selected $repo, same as the PR panels", () => { for (const id of [12, 13, 14] as const) { - expect(targetForPanel(id).queryText).toContain("('$repo' = '$__all' OR repo = '$repo')"); + expect(targetForPanel(id).queryText).toContain("(${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})"); } }); @@ -373,12 +376,13 @@ describe("maintainer Reviews & PRs Grafana dashboard", () => { expect(vars[0]!.query?.rawQueryText).toBe("SELECT DISTINCT repo FROM review_targets ORDER BY repo"); }); - it("scopes every review_targets panel query to the selected $repo", () => { + it("scopes every review_targets panel query to the selected repo with Grafana SQL-string escaping", () => { const targets = reviewTargets(); expect(targets.length).toBeGreaterThan(0); for (const target of targets) { - expect(target.queryText).toContain("('$repo' = '$__all' OR repo = '$repo')"); + expect(target.queryText).toContain("(${repo:sqlstring} = '$__all' OR repo = ${repo:sqlstring})"); + expect(target.queryText).not.toContain("'$repo'"); } }); @@ -466,17 +470,43 @@ describe("maintainer Reviews & PRs Grafana dashboard", () => { const trackedQuery = targetForPanel(2).queryText!; const allRepos = sqlite(db, expandGrafanaRange(trackedQuery)); - // A specific repo selection substitutes BOTH $repo occurrences with the same real repo value (never - // the literal "$__all" sentinel, which only appears when "All" is selected) -- simulate that directly - // on the raw query rather than going through expandGrafanaRange's own "All" default. - const repoAOnly = sqlite(db, expandGrafanaRange(trackedQuery.replaceAll("'$repo'", "'owner/repo-a'"))); - const repoBOnly = sqlite(db, expandGrafanaRange(trackedQuery.replaceAll("'$repo'", "'owner/repo-b'"))); + // A specific repo selection substitutes BOTH repo occurrences with the same SQL-escaped repo value (never + // the literal "$__all" sentinel, which only appears when "All" is selected). + const repoAOnly = sqlite(db, expandGrafanaRange(trackedQuery, "owner/repo-a")); + const repoBOnly = sqlite(db, expandGrafanaRange(trackedQuery, "owner/repo-b")); expect(allRepos).toBe("3"); expect(repoAOnly).toBe("1"); expect(repoBOnly).toBe("2"); }); + + (sqliteCliAvailable ? it : it.skip)("SQL-escapes repo variable values instead of interpolating raw dashboard input", () => { + const root = tmpRoot(); + const db = join(root, "reporting.sqlite"); + sqlite(db, ` + CREATE TABLE review_targets ( + repo TEXT NOT NULL, + number INTEGER NOT NULL, + submitter TEXT, + status TEXT NOT NULL, + verdict TEXT, + title TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + INSERT INTO review_targets (repo, number, submitter, status, verdict, title, created_at, updated_at) + VALUES + ('owner/repo-a', 1, 'alice', 'merged', 'merge', 'in repo a', '2026-06-29T20:30:00Z', '2026-06-29T20:30:00Z'), + ('owner/repo-b', 2, 'bob', 'merged', 'merge', 'in repo b', '2026-06-29T20:30:00Z', '2026-06-29T20:30:00Z'); + `); + + const trackedQuery = targetForPanel(2).queryText!; + const injectedRepo = "x') OR 1=1 --"; + + expect(sqlite(db, expandGrafanaRange(trackedQuery, injectedRepo))).toBe("0"); + }); + it("excludes bot-authored PRs from every review_targets panel query, not just the table", () => { const targets = reviewTargets();