Skip to content

Commit

Permalink
FEATURE: Add JSON result type component (#260)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
martin-brennan authored Nov 1, 2023
1 parent 3e5f679 commit 5776aa7
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 1 deletion.
2 changes: 2 additions & 0 deletions assets/javascripts/discourse/components/query-result.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -29,6 +30,7 @@ const VIEW_COMPONENTS = {
user: UserViewComponent,
group: GroupViewComponent,
html: HtmlViewComponent,
json: JsonViewComponent,
category: CategoryViewComponent,
};

Expand Down
9 changes: 9 additions & 0 deletions assets/javascripts/discourse/components/result-types/json.hbs
Original file line number Diff line number Diff line change
@@ -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>
31 changes: 31 additions & 0 deletions assets/javascripts/discourse/components/result-types/json.js
Original file line number Diff line number Diff line change
@@ -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: "",
},
});
}
}
12 changes: 12 additions & 0 deletions assets/stylesheets/explorer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 9 additions & 1 deletion lib/discourse_data_explorer/data_explorer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ class ValidationError < StandardError
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
Expand Down Expand Up @@ -131,6 +135,9 @@ def self.extra_data_pluck_fields
html: {
ignore: true,
},
json: {
ignore: true,
},
}
end

Expand All @@ -145,7 +152,6 @@ def self.add_extra_data(pg_result)
needed_classes = {}
ret = {}
col_map = {}

pg_result.fields.each_with_index do |col, idx|
rgx = column_regexes.find { |r| r.match col }
if rgx
Expand All @@ -158,6 +164,8 @@ def self.add_extra_data(pg_result)
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

Expand Down
31 changes: 31 additions & 0 deletions spec/data_explorer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,36 @@
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
14 changes: 14 additions & 0 deletions spec/system/reports_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,18 @@
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

0 comments on commit 5776aa7

Please sign in to comment.