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.
This commit is contained in:
Martin Brennan 2023-11-02 09:50:05 +10:00 committed by GitHub
parent 3e5f679fee
commit 5776aa7fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 1 deletions

View File

@ -17,6 +17,7 @@ import UrlViewComponent from "./result-types/url";
import UserViewComponent from "./result-types/user"; import UserViewComponent from "./result-types/user";
import GroupViewComponent from "./result-types/group"; import GroupViewComponent from "./result-types/group";
import HtmlViewComponent from "./result-types/html"; import HtmlViewComponent from "./result-types/html";
import JsonViewComponent from "./result-types/json";
import CategoryViewComponent from "./result-types/category"; import CategoryViewComponent from "./result-types/category";
const VIEW_COMPONENTS = { const VIEW_COMPONENTS = {
@ -29,6 +30,7 @@ const VIEW_COMPONENTS = {
user: UserViewComponent, user: UserViewComponent,
group: GroupViewComponent, group: GroupViewComponent,
html: HtmlViewComponent, html: HtmlViewComponent,
json: JsonViewComponent,
category: CategoryViewComponent, category: CategoryViewComponent,
}; };

View File

@ -0,0 +1,9 @@
<div class="result-json">
<div class="result-json-value">{{@ctx.value}}</div>
<DButton
class="result-json-button"
@action={{action "viewJson"}}
@icon="ellipsis-h"
@title="explorer.view_json"
/>
</div>

View File

@ -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: "",
},
});
}
}

View File

@ -416,6 +416,18 @@ table.group-reports {
display: block; display: block;
color: inherit !important; 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 { .explorer-pad-bottom {
margin-bottom: 200px; margin-bottom: 200px;

View File

@ -56,6 +56,7 @@ en:
no: "No" no: "No"
null_: "Null" null_: "Null"
export: "Export" export: "Export"
view_json: "View JSON"
save: "Save Changes" save: "Save Changes"
saverun: "Save Changes and Run" saverun: "Save Changes and Run"
run: "Run" run: "Run"

View File

@ -5,6 +5,10 @@ module ::DiscourseDataExplorer
end end
module DataExplorer 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. # Run a data explorer query on the currently connected database.
# #
# @param [Query] query the Query object to run # @param [Query] query the Query object to run
@ -131,6 +135,9 @@ module ::DiscourseDataExplorer
html: { html: {
ignore: true, ignore: true,
}, },
json: {
ignore: true,
},
} }
end end
@ -145,7 +152,6 @@ module ::DiscourseDataExplorer
needed_classes = {} needed_classes = {}
ret = {} ret = {}
col_map = {} col_map = {}
pg_result.fields.each_with_index do |col, idx| pg_result.fields.each_with_index do |col, idx|
rgx = column_regexes.find { |r| r.match col } rgx = column_regexes.find { |r| r.match col }
if rgx if rgx
@ -158,6 +164,8 @@ module ::DiscourseDataExplorer
needed_classes[cls] << idx needed_classes[cls] << idx
elsif col =~ /^\w+_url$/ elsif col =~ /^\w+_url$/
col_map[idx] = "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
end end

View File

@ -57,5 +57,36 @@ describe DiscourseDataExplorer::DataExplorer do
expect(result[:pg_result].to_a.size).to eq(1) expect(result[:pg_result].to_a.size).to eq(1)
expect(result[:pg_result][0]["id"]).to eq(topic2.id) expect(result[:pg_result][0]["id"]).to eq(topic2.id)
end 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
end end

View File

@ -42,4 +42,18 @@ RSpec.describe "Reports", type: :system, js: true do
find(".query-run .btn-primary").click find(".query-run .btn-primary").click
expect(page).to have_css(".query-results .result-header") expect(page).to have_css(".query-results .result-header")
end 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 end