The code for listing all of the defined queries is mixed together with the code for editing a single query. Notably, this results in large amounts of unnecessary data being loaded for the list view, which causes substantial rendering slowdowns. To address this issue, we now only load the necessary data for the list view, and load the full data when it's actually needed (any endpoint that returns a single query). The primary changes that achieve this are: - Create a new `QueryDetailsSerializer` serialiser, which includes all of the query info, and change the existing `QuerySerializer` serialiser to only include the necessary attributes of each query for generating a list of them all. - Split the monolith `/plugins/explorer` route into `/plugins/explorer` for showing just the list of queries, and `/plugins/explorer/queries/:query_id`, for showing/editing/running a specific query.
293 lines
6.4 KiB
JavaScript
293 lines
6.4 KiB
JavaScript
import { tracked } from "@glimmer/tracking";
|
|
import Controller from "@ember/controller";
|
|
import { action } from "@ember/object";
|
|
import { service } from "@ember/service";
|
|
import { Promise } from "rsvp";
|
|
import { ajax } from "discourse/lib/ajax";
|
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
import { bind } from "discourse/lib/decorators";
|
|
import QueryHelp from "discourse/plugins/discourse-data-explorer/discourse/components/modal/query-help";
|
|
import { ParamValidationError } from "discourse/plugins/discourse-data-explorer/discourse/components/param-input-form";
|
|
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
|
|
|
|
export default class PluginsExplorerController extends Controller {
|
|
@service modal;
|
|
@service appEvents;
|
|
@service router;
|
|
|
|
@tracked params;
|
|
@tracked editingName = false;
|
|
@tracked editingQuery = false;
|
|
@tracked loading = false;
|
|
@tracked showResults = false;
|
|
@tracked hideSchema = false;
|
|
@tracked results = this.model.results;
|
|
@tracked dirty = false;
|
|
|
|
queryParams = ["params"];
|
|
explain = false;
|
|
order = null;
|
|
form = null;
|
|
|
|
get saveDisabled() {
|
|
return !this.dirty;
|
|
}
|
|
|
|
get runDisabled() {
|
|
return this.dirty;
|
|
}
|
|
|
|
get parsedParams() {
|
|
return this.params ? JSON.parse(this.params) : null;
|
|
}
|
|
|
|
get editDisabled() {
|
|
return parseInt(this.model.id, 10) < 0 ? true : false;
|
|
}
|
|
|
|
get groupOptions() {
|
|
return this.groups
|
|
.filter((g) => g.id !== 0)
|
|
.map((g) => {
|
|
return { id: g.id, name: g.name };
|
|
});
|
|
}
|
|
|
|
@action
|
|
async save() {
|
|
try {
|
|
this.loading = true;
|
|
await this.model.save();
|
|
|
|
this.dirty = false;
|
|
this.editingName = false;
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
throw error;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
saveAndRun() {
|
|
this.save().then(() => this.run());
|
|
}
|
|
|
|
async _importQuery(file) {
|
|
const json = await this._readFileAsTextAsync(file);
|
|
const query = this._parseQuery(json);
|
|
const record = this.store.createRecord("query", query);
|
|
const response = await record.save();
|
|
return response.target;
|
|
}
|
|
|
|
_parseQuery(json) {
|
|
const parsed = JSON.parse(json);
|
|
const query = parsed.query;
|
|
if (!query || !query.sql) {
|
|
throw new TypeError();
|
|
}
|
|
query.id = 0; // 0 means no Id yet
|
|
return query;
|
|
}
|
|
|
|
_readFileAsTextAsync(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
resolve(reader.result);
|
|
};
|
|
reader.onerror = reject;
|
|
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
@bind
|
|
dragMove(e) {
|
|
if (!e.movementY && !e.movementX) {
|
|
return;
|
|
}
|
|
|
|
const editPane = document.querySelector(".query-editor");
|
|
const target = editPane.querySelector(".panels-flex");
|
|
const grippie = editPane.querySelector(".grippie");
|
|
|
|
// we need to get the initial height / width of edit pane
|
|
// before we manipulate the size
|
|
if (!this.initialPaneWidth && !this.originalPaneHeight) {
|
|
this.originalPaneWidth = target.clientWidth;
|
|
this.originalPaneHeight = target.clientHeight;
|
|
}
|
|
|
|
const newHeight = Math.max(
|
|
this.originalPaneHeight,
|
|
target.clientHeight + e.movementY
|
|
);
|
|
const newWidth = Math.max(
|
|
this.originalPaneWidth,
|
|
target.clientWidth + e.movementX
|
|
);
|
|
|
|
target.style.height = newHeight + "px";
|
|
target.style.width = newWidth + "px";
|
|
grippie.style.width = newWidth + "px";
|
|
this.appEvents.trigger("ace:resize");
|
|
}
|
|
|
|
@bind
|
|
didStartDrag() {}
|
|
|
|
@bind
|
|
didEndDrag() {}
|
|
|
|
@action
|
|
updateGroupIds(value) {
|
|
this.dirty = true;
|
|
this.model.set("group_ids", value);
|
|
}
|
|
|
|
@action
|
|
updateHideSchema(value) {
|
|
this.hideSchema = value;
|
|
}
|
|
|
|
@action
|
|
editName() {
|
|
this.editingName = true;
|
|
}
|
|
|
|
@action
|
|
editQuery() {
|
|
this.editingQuery = true;
|
|
}
|
|
|
|
@action
|
|
download() {
|
|
window.open(this.model.downloadUrl, "_blank");
|
|
}
|
|
|
|
@action
|
|
goHome() {
|
|
this.router.transitionTo("adminPlugins.explorer");
|
|
}
|
|
|
|
@action
|
|
showHelpModal() {
|
|
this.modal.show(QueryHelp);
|
|
}
|
|
|
|
@action
|
|
resetParams() {
|
|
this.model.resetParams();
|
|
}
|
|
|
|
@action
|
|
async discard() {
|
|
try {
|
|
this.loading = true;
|
|
const result = await this.store.find("query", this.model.id);
|
|
this.model.setProperties(result.getProperties(Query.updatePropertyNames));
|
|
if (!this.model.group_ids || !Array.isArray(this.model.group_ids)) {
|
|
this.model.set("group_ids", []);
|
|
}
|
|
this.dirty = false;
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
async destroyQuery() {
|
|
try {
|
|
this.loading = true;
|
|
this.showResults = false;
|
|
await this.store.destroyRecord("query", this.model);
|
|
this.model.set("destroyed", true);
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
async recover() {
|
|
try {
|
|
this.loading = true;
|
|
this.showResults = true;
|
|
await this.model.save();
|
|
this.model.set("destroyed", false);
|
|
} catch (error) {
|
|
popupAjaxError(error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
onRegisterApi(form) {
|
|
this.form = form;
|
|
}
|
|
|
|
@action
|
|
setDirty() {
|
|
this.dirty = true;
|
|
}
|
|
|
|
@action
|
|
exitEdit() {
|
|
this.editingName = false;
|
|
}
|
|
|
|
@action
|
|
async run() {
|
|
let params = null;
|
|
if (this.model.hasParams) {
|
|
try {
|
|
params = await this.form?.submit();
|
|
} catch (err) {
|
|
if (err instanceof ParamValidationError) {
|
|
return;
|
|
}
|
|
}
|
|
if (params == null) {
|
|
return;
|
|
}
|
|
}
|
|
this.setProperties({
|
|
loading: true,
|
|
showResults: false,
|
|
params: JSON.stringify(params),
|
|
});
|
|
|
|
ajax("/admin/plugins/explorer/queries/" + this.model.id + "/run", {
|
|
type: "POST",
|
|
data: {
|
|
params: JSON.stringify(params),
|
|
explain: this.explain,
|
|
},
|
|
})
|
|
.then((result) => {
|
|
this.results = result;
|
|
if (!result.success) {
|
|
this.showResults = false;
|
|
return;
|
|
}
|
|
this.showResults = true;
|
|
})
|
|
.catch((err) => {
|
|
this.showResults = false;
|
|
if (err.jqXHR && err.jqXHR.status === 422 && err.jqXHR.responseJSON) {
|
|
this.results = err.jqXHR.responseJSON;
|
|
} else {
|
|
popupAjaxError(err);
|
|
}
|
|
})
|
|
.finally(() => (this.loading = false));
|
|
}
|
|
}
|