From 5776aa7fc9f4a873dbebda13627d613c4d47aa49 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 2 Nov 2023 09:50:05 +1000 Subject: [PATCH] FEATURE: Add JSON result type component (#260) If a column is payload or contains _payload it will be assumed it has JSON data in it, then we will show the truncated JSON in the result column with a button to show the full-screen formatted JSON using our full-screen code viewer. We also do the same if the column is the `json` postgres data type. --- .../discourse/components/query-result.js | 2 ++ .../components/result-types/json.hbs | 9 ++++++ .../discourse/components/result-types/json.js | 31 +++++++++++++++++++ assets/stylesheets/explorer.scss | 12 +++++++ config/locales/client.en.yml | 1 + lib/discourse_data_explorer/data_explorer.rb | 10 +++++- spec/data_explorer_spec.rb | 31 +++++++++++++++++++ spec/system/reports_spec.rb | 14 +++++++++ 8 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 assets/javascripts/discourse/components/result-types/json.hbs create mode 100644 assets/javascripts/discourse/components/result-types/json.js diff --git a/assets/javascripts/discourse/components/query-result.js b/assets/javascripts/discourse/components/query-result.js index e126396..9d25e16 100644 --- a/assets/javascripts/discourse/components/query-result.js +++ b/assets/javascripts/discourse/components/query-result.js @@ -17,6 +17,7 @@ import UrlViewComponent from "./result-types/url"; import UserViewComponent from "./result-types/user"; import GroupViewComponent from "./result-types/group"; import HtmlViewComponent from "./result-types/html"; +import JsonViewComponent from "./result-types/json"; import CategoryViewComponent from "./result-types/category"; const VIEW_COMPONENTS = { @@ -29,6 +30,7 @@ const VIEW_COMPONENTS = { user: UserViewComponent, group: GroupViewComponent, html: HtmlViewComponent, + json: JsonViewComponent, category: CategoryViewComponent, }; diff --git a/assets/javascripts/discourse/components/result-types/json.hbs b/assets/javascripts/discourse/components/result-types/json.hbs new file mode 100644 index 0000000..3fd4fe1 --- /dev/null +++ b/assets/javascripts/discourse/components/result-types/json.hbs @@ -0,0 +1,9 @@ +
+
{{@ctx.value}}
+ +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/result-types/json.js b/assets/javascripts/discourse/components/result-types/json.js new file mode 100644 index 0000000..59dcfc4 --- /dev/null +++ b/assets/javascripts/discourse/components/result-types/json.js @@ -0,0 +1,31 @@ +import Component from "@glimmer/component"; +import FullscreenCodeModal from "discourse/components/modal/fullscreen-code"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; +import { cached } from "@glimmer/tracking"; + +export default class Json extends Component { + @service dialog; + @service modal; + + @cached + get parsedJson() { + try { + return JSON.parse(this.args.ctx.value); + } catch { + return null; + } + } + + @action + viewJson() { + this.modal.show(FullscreenCodeModal, { + model: { + code: this.parsedJson + ? JSON.stringify(this.parsedJson, null, 2) + : this.args.ctx.value, + codeClasses: "", + }, + }); + } +} diff --git a/assets/stylesheets/explorer.scss b/assets/stylesheets/explorer.scss index 9637fc2..9170786 100644 --- a/assets/stylesheets/explorer.scss +++ b/assets/stylesheets/explorer.scss @@ -416,6 +416,18 @@ table.group-reports { display: block; color: inherit !important; } +.result-json { + display: flex; +} + +.result-json-value { + flex: 1; + margin-right: 0.5em; + max-width: 250px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} .explorer-pad-bottom { margin-bottom: 200px; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 51f83e6..0d93697 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -56,6 +56,7 @@ en: no: "No" null_: "Null" export: "Export" + view_json: "View JSON" save: "Save Changes" saverun: "Save Changes and Run" run: "Run" diff --git a/lib/discourse_data_explorer/data_explorer.rb b/lib/discourse_data_explorer/data_explorer.rb index d5f4c3e..1bd002d 100644 --- a/lib/discourse_data_explorer/data_explorer.rb +++ b/lib/discourse_data_explorer/data_explorer.rb @@ -5,6 +5,10 @@ module ::DiscourseDataExplorer end module DataExplorer + # Used for ftype calls, see https://www.rubydoc.info/gems/pg/0.17.1/PG%2FResult:ftype + # and /usr/include/postgresql/server/catalog/pg_type_d.h + PG_TYPE_OID_JSON = 114 + # Run a data explorer query on the currently connected database. # # @param [Query] query the Query object to run @@ -131,6 +135,9 @@ module ::DiscourseDataExplorer html: { ignore: true, }, + json: { + ignore: true, + }, } end @@ -145,7 +152,6 @@ module ::DiscourseDataExplorer needed_classes = {} ret = {} col_map = {} - pg_result.fields.each_with_index do |col, idx| rgx = column_regexes.find { |r| r.match col } if rgx @@ -158,6 +164,8 @@ module ::DiscourseDataExplorer needed_classes[cls] << idx elsif col =~ /^\w+_url$/ col_map[idx] = "url" + elsif col =~ /^\w+_payload$/ || col == "payload" || pg_result.ftype(idx) == PG_TYPE_OID_JSON + col_map[idx] = "json" end end diff --git a/spec/data_explorer_spec.rb b/spec/data_explorer_spec.rb index ba93454..8cb7a6a 100644 --- a/spec/data_explorer_spec.rb +++ b/spec/data_explorer_spec.rb @@ -57,5 +57,36 @@ describe DiscourseDataExplorer::DataExplorer do expect(result[:pg_result].to_a.size).to eq(1) expect(result[:pg_result][0]["id"]).to eq(topic2.id) end + + describe ".add_extra_data" do + it "treats any column with payload in the name as 'json'" do + Fabricate(:reviewable_queued_post) + sql = <<~SQL + SELECT id, payload FROM reviewables LIMIT 10 + SQL + query = DiscourseDataExplorer::Query.create!(name: "some query", sql: sql) + result = described_class.run_query(query) + _, colrender = DiscourseDataExplorer::DataExplorer.add_extra_data(result[:pg_result]) + expect(colrender).to eq({ 1 => "json" }) + end + + it "treats columns with the actual json data type as 'json'" do + ApiKeyScope.create( + resource: "topics", + action: "update", + api_key_id: Fabricate(:api_key).id, + allowed_parameters: { + "category_id" => ["#{topic.category_id}"], + }, + ) + sql = <<~SQL + SELECT id, allowed_parameters FROM api_key_scopes LIMIT 10 + SQL + query = DiscourseDataExplorer::Query.create!(name: "some query", sql: sql) + result = described_class.run_query(query) + _, colrender = DiscourseDataExplorer::DataExplorer.add_extra_data(result[:pg_result]) + expect(colrender).to eq({ 1 => "json" }) + end + end end end diff --git a/spec/system/reports_spec.rb b/spec/system/reports_spec.rb index dcedbaf..a0da064 100644 --- a/spec/system/reports_spec.rb +++ b/spec/system/reports_spec.rb @@ -42,4 +42,18 @@ RSpec.describe "Reports", type: :system, js: true do find(".query-run .btn-primary").click expect(page).to have_css(".query-results .result-header") end + + it "allows user to run a report with a JSON column and open a fullscreen code viewer" do + Fabricate(:reviewable_queued_post) + sql = <<~SQL + SELECT id, payload FROM reviewables LIMIT 10 + SQL + json_query = DiscourseDataExplorer::Query.create!(name: "some query", sql: sql) + sign_in(user) + visit("/g/group/reports/#{json_query.id}") + find(".query-run .btn-primary").click + expect(page).to have_css(".query-results .result-json") + first(".query-results .result-json .btn.result-json-button").click + expect(page).to have_css(".fullscreen-code-modal") + end end