DEV: Upgrade `admin-plugins-explorer` to Octane (#209)
- Drop `explorer-container` and move its logic to `admin-plugin-explorer` container - Convert resizing of the query edit pane from jquery -> draggable modifier
This commit is contained in:
parent
b1df914549
commit
4d26cf78f0
|
@ -0,0 +1 @@
|
||||||
|
<pre><code class={{@codeClass}}>{{@value}}</code></pre>
|
|
@ -1,96 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
import { observes } from "discourse-common/utils/decorators";
|
|
||||||
import { schedule, throttle } from "@ember/runloop";
|
|
||||||
|
|
||||||
export default Component.extend({
|
|
||||||
@observes("hideSchema")
|
|
||||||
_onHideSchema() {
|
|
||||||
this.appEvents.trigger("ace:resize");
|
|
||||||
},
|
|
||||||
|
|
||||||
@observes("everEditing")
|
|
||||||
_onInsertEditor() {
|
|
||||||
schedule("afterRender", this, () => this._bindControls());
|
|
||||||
},
|
|
||||||
|
|
||||||
_bindControls() {
|
|
||||||
if (this._state !== "inDOM") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const $editPane = $(".query-editor");
|
|
||||||
if (!$editPane.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldGrippie = this.grippie;
|
|
||||||
if (oldGrippie) {
|
|
||||||
oldGrippie.off("mousedown mousemove mouseup");
|
|
||||||
}
|
|
||||||
|
|
||||||
const $grippie = $editPane.find(".grippie");
|
|
||||||
const $target = $editPane.find(".panels-flex");
|
|
||||||
const $document = $(document);
|
|
||||||
|
|
||||||
const minWidth = $target.width();
|
|
||||||
const minHeight = $target.height();
|
|
||||||
|
|
||||||
this.set("grippie", $grippie);
|
|
||||||
|
|
||||||
const mousemove = (e) => {
|
|
||||||
const diffY = this.startY - e.screenY;
|
|
||||||
const diffX = this.startX - e.screenX;
|
|
||||||
|
|
||||||
const newHeight = Math.max(minHeight, this.startHeight - diffY);
|
|
||||||
const newWidth = Math.max(minWidth, this.startWidth - diffX);
|
|
||||||
|
|
||||||
$target.height(newHeight);
|
|
||||||
$target.width(newWidth);
|
|
||||||
$grippie.width(newWidth);
|
|
||||||
this.appEvents.trigger("ace:resize");
|
|
||||||
};
|
|
||||||
|
|
||||||
const throttledMousemove = ((event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
throttle(this, mousemove, event, 20);
|
|
||||||
}).bind(this);
|
|
||||||
|
|
||||||
const mouseup = (() => {
|
|
||||||
$document.off("mousemove", throttledMousemove);
|
|
||||||
$document.off("mouseup", mouseup);
|
|
||||||
this.setProperties({
|
|
||||||
startY: null,
|
|
||||||
startX: null,
|
|
||||||
startHeight: null,
|
|
||||||
startWidth: null,
|
|
||||||
});
|
|
||||||
}).bind(this);
|
|
||||||
|
|
||||||
$grippie.on("mousedown", (e) => {
|
|
||||||
this.setProperties({
|
|
||||||
startY: e.screenY,
|
|
||||||
startX: e.screenX,
|
|
||||||
startHeight: $target.height(),
|
|
||||||
startWidth: $target.width(),
|
|
||||||
});
|
|
||||||
|
|
||||||
$document.on("mousemove", throttledMousemove);
|
|
||||||
$document.on("mouseup", mouseup);
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this._bindControls();
|
|
||||||
},
|
|
||||||
|
|
||||||
willDestroyElement() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
if (this.everEditing) {
|
|
||||||
this.grippie && this.grippie.off("mousedown");
|
|
||||||
this.set("grippie", null);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -3,140 +3,126 @@ import showModal from "discourse/lib/show-modal";
|
||||||
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
|
import Query from "discourse/plugins/discourse-data-explorer/discourse/models/query";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import discourseComputed, {
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
bind,
|
|
||||||
observes,
|
|
||||||
} from "discourse-common/utils/decorators";
|
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { get } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { not, reads, sort } from "@ember/object/computed";
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] });
|
const NoQuery = Query.create({ name: "No queries", fake: true, group_ids: [] });
|
||||||
|
|
||||||
export default Controller.extend({
|
export default class PluginsExplorerController extends Controller {
|
||||||
dialog: service(),
|
@service dialog;
|
||||||
queryParams: { selectedQueryId: "id", params: "params" },
|
@service appEvents;
|
||||||
selectedQueryId: null,
|
|
||||||
editDisabled: false,
|
|
||||||
showResults: false,
|
|
||||||
hideSchema: false,
|
|
||||||
loading: false,
|
|
||||||
explain: false,
|
|
||||||
|
|
||||||
saveDisabled: not("selectedItem.dirty"),
|
@tracked sortByProperty = "last_run_at";
|
||||||
runDisabled: reads("selectedItem.dirty"),
|
@tracked sortDescending = true;
|
||||||
results: reads("selectedItem.results"),
|
@tracked params;
|
||||||
|
@tracked search;
|
||||||
|
@tracked newQueryName;
|
||||||
|
@tracked showCreate;
|
||||||
|
@tracked editingName = false;
|
||||||
|
@tracked editingQuery = false;
|
||||||
|
@tracked selectedQueryId;
|
||||||
|
@tracked loading = false;
|
||||||
|
@tracked showResults = false;
|
||||||
|
@tracked hideSchema = false;
|
||||||
|
@tracked results = this.selectedItem.results;
|
||||||
|
@tracked dirty = false;
|
||||||
|
|
||||||
asc: null,
|
queryParams = ["params", { selectedQueryId: "id" }];
|
||||||
order: null,
|
explain = false;
|
||||||
editing: false,
|
acceptedImportFileTypes = ["application/json"];
|
||||||
everEditing: false,
|
order = null;
|
||||||
showRecentQueries: true,
|
|
||||||
sortBy: ["last_run_at:desc"],
|
|
||||||
sortedQueries: sort("model", "sortBy"),
|
|
||||||
|
|
||||||
@discourseComputed("params")
|
get validQueryPresent() {
|
||||||
parsedParams(params) {
|
return !!this.selectedItem.id;
|
||||||
return params ? JSON.parse(params) : null;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed
|
get saveDisabled() {
|
||||||
acceptedImportFileTypes() {
|
return !this.dirty;
|
||||||
return ["application/json"];
|
}
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("search", "sortBy")
|
get runDisabled() {
|
||||||
filteredContent(search) {
|
return this.dirty;
|
||||||
const regexp = new RegExp(search, "i");
|
}
|
||||||
|
|
||||||
|
get sortedQueries() {
|
||||||
|
const sortedQueries = this.model.sortBy(this.sortByProperty);
|
||||||
|
return this.sortDescending ? sortedQueries.reverse() : sortedQueries;
|
||||||
|
}
|
||||||
|
|
||||||
|
get parsedParams() {
|
||||||
|
return this.params ? JSON.parse(this.params) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get filteredContent() {
|
||||||
|
const regexp = new RegExp(this.search, "i");
|
||||||
return this.sortedQueries.filter(
|
return this.sortedQueries.filter(
|
||||||
(result) => regexp.test(result.name) || regexp.test(result.description)
|
(result) => regexp.test(result.name) || regexp.test(result.description)
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
|
||||||
@discourseComputed("newQueryName")
|
get createDisabled() {
|
||||||
createDisabled(newQueryName) {
|
return (this.newQueryName || "").trim().length === 0;
|
||||||
return (newQueryName || "").trim().length === 0;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("selectedQueryId")
|
get selectedItem() {
|
||||||
selectedItem(selectedQueryId) {
|
const query = this.model.findBy("id", parseInt(this.selectedQueryId, 10));
|
||||||
const id = parseInt(selectedQueryId, 10);
|
return query || NoQuery;
|
||||||
const item = this.model.findBy("id", id);
|
}
|
||||||
|
|
||||||
!isNaN(id)
|
get editDisabled() {
|
||||||
? this.set("showRecentQueries", false)
|
return parseInt(this.selectedQueryId, 10) < 0 ? true : false;
|
||||||
: this.set("showRecentQueries", true);
|
}
|
||||||
|
|
||||||
if (id < 0) {
|
get groupOptions() {
|
||||||
this.set("editDisabled", true);
|
return this.groups
|
||||||
}
|
|
||||||
|
|
||||||
return item || NoQuery;
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("selectedItem", "editing")
|
|
||||||
selectedGroupNames() {
|
|
||||||
const groupIds = this.selectedItem.group_ids || [];
|
|
||||||
const groupNames = groupIds.map((id) => {
|
|
||||||
return this.groupOptions.find((groupOption) => groupOption.id === id)
|
|
||||||
.name;
|
|
||||||
});
|
|
||||||
return groupNames.join(", ");
|
|
||||||
},
|
|
||||||
|
|
||||||
@discourseComputed("groups")
|
|
||||||
groupOptions(groups) {
|
|
||||||
return groups
|
|
||||||
.filter((g) => g.id !== 0)
|
.filter((g) => g.id !== 0)
|
||||||
.map((g) => {
|
.map((g) => {
|
||||||
return { id: g.id, name: g.name };
|
return { id: g.id, name: g.name };
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
@discourseComputed("selectedItem", "selectedItem.dirty")
|
get othersDirty() {
|
||||||
othersDirty(selectedItem) {
|
return !!this.model.find((q) => q !== this.selectedItem && this.dirty);
|
||||||
return !!this.model.find((q) => q !== selectedItem && q.dirty);
|
}
|
||||||
},
|
|
||||||
|
|
||||||
@observes("editing")
|
|
||||||
setEverEditing() {
|
|
||||||
if (this.editing && !this.everEditing) {
|
|
||||||
this.set("everEditing", true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
addCreatedRecord(record) {
|
addCreatedRecord(record) {
|
||||||
this.model.pushObject(record);
|
this.model.pushObject(record);
|
||||||
this.set("selectedQueryId", get(record, "id"));
|
this.selectedQueryId = record.id;
|
||||||
this.selectedItem.set("dirty", false);
|
this.dirty = false;
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
showResults: false,
|
showResults: false,
|
||||||
results: null,
|
results: null,
|
||||||
editing: true,
|
editingName: true,
|
||||||
|
editingQuery: true,
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
save() {
|
save() {
|
||||||
this.set("loading", true);
|
this.loading = true;
|
||||||
if (this.get("selectedItem.description") === "") {
|
|
||||||
this.set("selectedItem.description", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.selectedItem
|
return this.selectedItem
|
||||||
.save()
|
.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const query = this.selectedItem;
|
this.dirty = false;
|
||||||
query.markNotDirty();
|
this.editingName = false;
|
||||||
this.set("editing", false);
|
this.editingQuery = false;
|
||||||
})
|
})
|
||||||
.catch((x) => {
|
.catch((x) => {
|
||||||
popupAjaxError(x);
|
popupAjaxError(x);
|
||||||
throw x;
|
throw x;
|
||||||
})
|
})
|
||||||
.finally(() => this.set("loading", false));
|
.finally(() => (this.loading = false));
|
||||||
},
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
saveAndRun() {
|
||||||
|
this.save().then(() => this.run());
|
||||||
|
}
|
||||||
|
|
||||||
async _importQuery(file) {
|
async _importQuery(file) {
|
||||||
const json = await this._readFileAsTextAsync(file);
|
const json = await this._readFileAsTextAsync(file);
|
||||||
|
@ -144,7 +130,7 @@ export default Controller.extend({
|
||||||
const record = this.store.createRecord("query", query);
|
const record = this.store.createRecord("query", query);
|
||||||
const response = await record.save();
|
const response = await record.save();
|
||||||
return response.target;
|
return response.target;
|
||||||
},
|
}
|
||||||
|
|
||||||
_parseQuery(json) {
|
_parseQuery(json) {
|
||||||
const parsed = JSON.parse(json);
|
const parsed = JSON.parse(json);
|
||||||
|
@ -154,7 +140,7 @@ export default Controller.extend({
|
||||||
}
|
}
|
||||||
query.id = 0; // 0 means no Id yet
|
query.id = 0; // 0 means no Id yet
|
||||||
return query;
|
return query;
|
||||||
},
|
}
|
||||||
|
|
||||||
_readFileAsTextAsync(file) {
|
_readFileAsTextAsync(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -166,200 +152,270 @@ export default Controller.extend({
|
||||||
|
|
||||||
reader.readAsText(file);
|
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() {}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
scrollTop() {
|
scrollTop() {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
this.setProperties({ editing: false, everEditing: false });
|
this.editingName = false;
|
||||||
},
|
this.editingQuery = false;
|
||||||
|
}
|
||||||
|
|
||||||
actions: {
|
@action
|
||||||
updateHideSchema(value) {
|
updateGroupIds(value) {
|
||||||
this.set("hideSchema", value);
|
this.dirty = true;
|
||||||
},
|
this.selectedItem.set("group_ids", value);
|
||||||
|
}
|
||||||
|
|
||||||
import(files) {
|
@action
|
||||||
this.set("loading", true);
|
updateHideSchema(value) {
|
||||||
const file = files[0];
|
this.hideSchema = value;
|
||||||
this._importQuery(file)
|
}
|
||||||
.then((record) => this.addCreatedRecord(record))
|
|
||||||
.catch((e) => {
|
|
||||||
if (e.jqXHR) {
|
|
||||||
popupAjaxError(e);
|
|
||||||
} else if (e instanceof SyntaxError) {
|
|
||||||
this.dialog.alert(I18n.t("explorer.import.unparseable_json"));
|
|
||||||
} else if (e instanceof TypeError) {
|
|
||||||
this.dialog.alert(I18n.t("explorer.import.wrong_json"));
|
|
||||||
} else {
|
|
||||||
this.dialog.alert(I18n.t("errors.desc.unknown"));
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.set("loading", false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
showCreate() {
|
@action
|
||||||
this.set("showCreate", true);
|
import(files) {
|
||||||
},
|
this.loading = true;
|
||||||
|
const file = files[0];
|
||||||
editName() {
|
this._importQuery(file)
|
||||||
this.set("editing", true);
|
.then((record) => this.addCreatedRecord(record))
|
||||||
},
|
.catch((e) => {
|
||||||
|
if (e.jqXHR) {
|
||||||
download() {
|
popupAjaxError(e);
|
||||||
window.open(this.get("selectedItem.downloadUrl"), "_blank");
|
} else if (e instanceof SyntaxError) {
|
||||||
},
|
this.dialog.alert(I18n.t("explorer.import.unparseable_json"));
|
||||||
|
} else if (e instanceof TypeError) {
|
||||||
goHome() {
|
this.dialog.alert(I18n.t("explorer.import.wrong_json"));
|
||||||
this.setProperties({
|
} else {
|
||||||
asc: null,
|
this.dialog.alert(I18n.t("errors.desc.unknown"));
|
||||||
order: null,
|
// eslint-disable-next-line no-console
|
||||||
showResults: false,
|
console.error(e);
|
||||||
editDisabled: false,
|
|
||||||
showRecentQueries: true,
|
|
||||||
selectedQueryId: null,
|
|
||||||
params: null,
|
|
||||||
sortBy: ["last_run_at:desc"],
|
|
||||||
});
|
|
||||||
this.transitionToRoute({ queryParams: { id: null, params: null } });
|
|
||||||
},
|
|
||||||
|
|
||||||
showHelpModal() {
|
|
||||||
showModal("query-help");
|
|
||||||
},
|
|
||||||
|
|
||||||
resetParams() {
|
|
||||||
this.selectedItem.resetParams();
|
|
||||||
},
|
|
||||||
|
|
||||||
saveDefaults() {
|
|
||||||
this.selectedItem.saveDefaults();
|
|
||||||
},
|
|
||||||
|
|
||||||
save() {
|
|
||||||
this.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
saverun() {
|
|
||||||
this.save().then(() => this.send("run"));
|
|
||||||
},
|
|
||||||
|
|
||||||
sortByProperty(property) {
|
|
||||||
if (this.sortBy[0] === `${property}:desc`) {
|
|
||||||
this.set("sortBy", [`${property}:asc`]);
|
|
||||||
} else {
|
|
||||||
this.set("sortBy", [`${property}:desc`]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
create() {
|
|
||||||
const name = this.newQueryName.trim();
|
|
||||||
this.setProperties({
|
|
||||||
loading: true,
|
|
||||||
showCreate: false,
|
|
||||||
showRecentQueries: false,
|
|
||||||
});
|
|
||||||
this.store
|
|
||||||
.createRecord("query", { name })
|
|
||||||
.save()
|
|
||||||
.then((result) => this.addCreatedRecord(result.target))
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => this.set("loading", false));
|
|
||||||
},
|
|
||||||
|
|
||||||
discard() {
|
|
||||||
this.set("loading", true);
|
|
||||||
this.store
|
|
||||||
.find("query", this.get("selectedItem.id"))
|
|
||||||
.then((result) => {
|
|
||||||
const query = this.get("selectedItem");
|
|
||||||
query.setProperties(result.getProperties(Query.updatePropertyNames));
|
|
||||||
if (!query.group_ids || !Array.isArray(query.group_ids)) {
|
|
||||||
query.set("group_ids", []);
|
|
||||||
}
|
|
||||||
query.markNotDirty();
|
|
||||||
this.set("editing", false);
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => this.set("loading", false));
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
const query = this.selectedItem;
|
|
||||||
this.setProperties({ loading: true, showResults: false });
|
|
||||||
this.store
|
|
||||||
.destroyRecord("query", query)
|
|
||||||
.then(() => query.set("destroyed", true))
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => this.set("loading", false));
|
|
||||||
},
|
|
||||||
|
|
||||||
recover() {
|
|
||||||
const query = this.selectedItem;
|
|
||||||
this.setProperties({ loading: true, showResults: true });
|
|
||||||
query
|
|
||||||
.save()
|
|
||||||
.then(() => query.set("destroyed", false))
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => {
|
|
||||||
this.set("loading", false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// This is necessary with glimmer's one way data stream to get the child's
|
|
||||||
// changes of 'params' to bubble up.
|
|
||||||
updateParams(identifier, value) {
|
|
||||||
this.selectedItem.set(`params.${identifier}`, value);
|
|
||||||
},
|
|
||||||
|
|
||||||
run() {
|
|
||||||
if (this.get("selectedItem.dirty")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.runDisabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setProperties({
|
|
||||||
loading: true,
|
|
||||||
showResults: false,
|
|
||||||
params: JSON.stringify(this.selectedItem.params),
|
|
||||||
});
|
|
||||||
|
|
||||||
ajax(
|
|
||||||
"/admin/plugins/explorer/queries/" +
|
|
||||||
this.get("selectedItem.id") +
|
|
||||||
"/run",
|
|
||||||
{
|
|
||||||
type: "POST",
|
|
||||||
data: {
|
|
||||||
params: JSON.stringify(this.get("selectedItem.params")),
|
|
||||||
explain: this.explain,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
.then((result) => {
|
.finally(() => {
|
||||||
this.set("results", result);
|
this.loading = false;
|
||||||
if (!result.success) {
|
this.dirty = true;
|
||||||
this.set("showResults", false);
|
});
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.set("showResults", true);
|
@action
|
||||||
})
|
displayCreate() {
|
||||||
.catch((err) => {
|
this.showCreate = true;
|
||||||
this.set("showResults", false);
|
}
|
||||||
if (err.jqXHR && err.jqXHR.status === 422 && err.jqXHR.responseJSON) {
|
|
||||||
this.set("results", err.jqXHR.responseJSON);
|
@action
|
||||||
} else {
|
editName() {
|
||||||
popupAjaxError(err);
|
this.editingName = true;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.finally(() => this.set("loading", false));
|
@action
|
||||||
},
|
editQuery() {
|
||||||
},
|
this.editingQuery = true;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
download() {
|
||||||
|
window.open(this.selectedItem.downloadUrl, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
goHome() {
|
||||||
|
this.setProperties({
|
||||||
|
order: null,
|
||||||
|
showResults: false,
|
||||||
|
selectedQueryId: null,
|
||||||
|
params: null,
|
||||||
|
sortByProperty: "last_run_at",
|
||||||
|
sortDescending: true,
|
||||||
|
});
|
||||||
|
this.transitionToRoute({ queryParams: { id: null, params: null } });
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
showHelpModal() {
|
||||||
|
showModal("query-help");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
resetParams() {
|
||||||
|
this.selectedItem.resetParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
saveDefaults() {
|
||||||
|
this.selectedItem.saveDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateSortProperty(property) {
|
||||||
|
if (this.sortByProperty === property) {
|
||||||
|
this.sortDescending = !this.sortDescending;
|
||||||
|
} else {
|
||||||
|
this.sortByProperty = property;
|
||||||
|
this.sortDescending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
create() {
|
||||||
|
const name = this.newQueryName.trim();
|
||||||
|
this.setProperties({
|
||||||
|
loading: true,
|
||||||
|
showCreate: false,
|
||||||
|
});
|
||||||
|
this.store
|
||||||
|
.createRecord("query", { name })
|
||||||
|
.save()
|
||||||
|
.then((result) => this.addCreatedRecord(result.target))
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
this.dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
discard() {
|
||||||
|
this.loading = true;
|
||||||
|
this.store
|
||||||
|
.find("query", this.selectedItem.id)
|
||||||
|
.then((result) => {
|
||||||
|
this.selectedItem.setProperties(
|
||||||
|
result.getProperties(Query.updatePropertyNames)
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!this.selectedItem.group_ids ||
|
||||||
|
!Array.isArray(this.selectedItem.group_ids)
|
||||||
|
) {
|
||||||
|
this.selectedItem.set("group_ids", []);
|
||||||
|
}
|
||||||
|
this.dirty = false;
|
||||||
|
this.editingName = false;
|
||||||
|
this.editingQuery = false;
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => (this.loading = false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
destroyQuery() {
|
||||||
|
this.loading = true;
|
||||||
|
this.showResults = false;
|
||||||
|
this.store
|
||||||
|
.destroyRecord("query", this.selectedItem)
|
||||||
|
.then(() => this.selectedItem.set("destroyed", true))
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => (this.loading = false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
recover() {
|
||||||
|
this.loading = true;
|
||||||
|
this.showResults = true;
|
||||||
|
this.selectedItem
|
||||||
|
.save()
|
||||||
|
.then(() => this.selectedItem.set("destroyed", false))
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => (this.loading = false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateParams(identifier, value) {
|
||||||
|
this.selectedItem.set(`params.${identifier}`, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateSearch(value) {
|
||||||
|
this.search = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateNewQueryName(value) {
|
||||||
|
this.newQueryName = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setDirty() {
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
exitEdit() {
|
||||||
|
this.editingName = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
run() {
|
||||||
|
if (this.dirty || this.runDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
loading: true,
|
||||||
|
showResults: false,
|
||||||
|
params: JSON.stringify(this.selectedItem.params),
|
||||||
|
});
|
||||||
|
|
||||||
|
ajax("/admin/plugins/explorer/queries/" + this.selectedItem.id + "/run", {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
params: JSON.stringify(this.selectedItem.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,16 +7,9 @@ import RestModel from "discourse/models/rest";
|
||||||
import { reads } from "@ember/object/computed";
|
import { reads } from "@ember/object/computed";
|
||||||
|
|
||||||
const Query = RestModel.extend({
|
const Query = RestModel.extend({
|
||||||
dirty: false,
|
|
||||||
params: {},
|
params: {},
|
||||||
results: null,
|
results: null,
|
||||||
|
hasParams: reads("param_info.length"),
|
||||||
@on("init")
|
|
||||||
_init() {
|
|
||||||
this._super(...arguments);
|
|
||||||
|
|
||||||
this.set("dirty", false);
|
|
||||||
},
|
|
||||||
|
|
||||||
@on("init")
|
@on("init")
|
||||||
@observes("param_info")
|
@observes("param_info")
|
||||||
|
@ -24,17 +17,6 @@ const Query = RestModel.extend({
|
||||||
this.resetParams();
|
this.resetParams();
|
||||||
},
|
},
|
||||||
|
|
||||||
@observes("name", "description", "sql", "group_ids")
|
|
||||||
markDirty() {
|
|
||||||
this.set("dirty", true);
|
|
||||||
},
|
|
||||||
|
|
||||||
markNotDirty() {
|
|
||||||
this.set("dirty", false);
|
|
||||||
},
|
|
||||||
|
|
||||||
hasParams: reads("param_info.length"),
|
|
||||||
|
|
||||||
resetParams() {
|
resetParams() {
|
||||||
const newParams = {};
|
const newParams = {};
|
||||||
const oldParams = this.params;
|
const oldParams = this.params;
|
||||||
|
|
|
@ -25,7 +25,6 @@ export default DiscourseRoute.extend({
|
||||||
return schemaPromise.then((schema) => {
|
return schemaPromise.then((schema) => {
|
||||||
return queryPromise.then((model) => {
|
return queryPromise.then((model) => {
|
||||||
model.forEach((query) => {
|
model.forEach((query) => {
|
||||||
query.markNotDirty();
|
|
||||||
query.set(
|
query.set(
|
||||||
"group_names",
|
"group_names",
|
||||||
(query.group_ids || []).map((id) => groupNames[id])
|
(query.group_ids || []).map((id) => groupNames[id])
|
||||||
|
|
|
@ -1,368 +1,389 @@
|
||||||
{{#explorer-container hideSchema=hideSchema everEditing=everEditing}}
|
{{#if this.disallow}}
|
||||||
{{#if disallow}}
|
<h1>{{i18n "explorer.admins_only"}}</h1>
|
||||||
<h1>{{i18n "explorer.admins_only"}}</h1>
|
{{else}}
|
||||||
{{else}}
|
{{#unless this.validQueryPresent}}
|
||||||
{{#unless selectedQueryId}}
|
<div class="query-list">
|
||||||
<div class="query-list">
|
<TextField
|
||||||
{{text-field value=search placeholderKey="explorer.search_placeholder"}}
|
@value={{this.search}}
|
||||||
{{d-button
|
@placeholderKey="explorer.search_placeholder"
|
||||||
action=(action "showCreate")
|
@onChange={{this.updateSearch}}
|
||||||
icon="plus"
|
/>
|
||||||
class="no-text btn-right"
|
<DButton
|
||||||
}}
|
@action={{this.displayCreate}}
|
||||||
{{pick-files-button
|
@icon="plus"
|
||||||
class="import-btn"
|
@class="no-text btn-right"
|
||||||
label="explorer.import.label"
|
/>
|
||||||
icon="upload"
|
<PickFilesButton
|
||||||
acceptedFormatsOverride=acceptedImportFileTypes
|
@class="import-btn"
|
||||||
showButton=true
|
@label="explorer.import.label"
|
||||||
onFilesPicked=(action "import")
|
@icon="upload"
|
||||||
}}
|
@acceptedFormatsOverride={{this.acceptedImportFileTypes}}
|
||||||
|
@showButton="true"
|
||||||
|
@onFilesPicked={{this.import}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.showCreate}}
|
||||||
|
<div class="query-create">
|
||||||
|
<TextField
|
||||||
|
@value={{this.newQueryName}}
|
||||||
|
@placeholderKey="explorer.create_placeholder"
|
||||||
|
@onChange={{this.updateNewQueryName}}
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@action={{this.create}}
|
||||||
|
@disabled={{this.createDisabled}}
|
||||||
|
@label="explorer.create"
|
||||||
|
@icon="plus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.othersDirty}}
|
||||||
|
<div class="warning">
|
||||||
|
{{d-icon "exclamation-triangle"}}
|
||||||
|
{{i18n "explorer.others_dirty"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
{{#if this.model.length}}
|
||||||
|
{{#unless this.selectedItem.fake}}
|
||||||
|
<div class="query-edit {{if this.editName 'editing'}}">
|
||||||
|
{{#if this.selectedItem}}
|
||||||
|
{{#if this.editingName}}
|
||||||
|
<div class="name">
|
||||||
|
<DButton
|
||||||
|
@action={{this.goHome}}
|
||||||
|
@icon="chevron-left"
|
||||||
|
@class="previous"
|
||||||
|
/>
|
||||||
|
<DButton
|
||||||
|
@action={{this.exitEdit}}
|
||||||
|
@icon="times"
|
||||||
|
@class="previous"
|
||||||
|
/>
|
||||||
|
<div class="name-text-field">
|
||||||
|
<TextField
|
||||||
|
@value={{this.selectedItem.name}}
|
||||||
|
@onChange={{this.setDirty}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="desc">
|
||||||
|
<DTextarea
|
||||||
|
@value={{this.selectedItem.description}}
|
||||||
|
@placeholder={{(i18n "explorer.description_placeholder")}}
|
||||||
|
@input={{this.setDirty}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="name">
|
||||||
|
<DButton
|
||||||
|
@action={{this.goHome}}
|
||||||
|
@icon="chevron-left"
|
||||||
|
@class="previous"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h1>
|
||||||
|
{{this.selectedItem.name}}
|
||||||
|
{{#unless this.editDisabled}}
|
||||||
|
<a href {{action "editName"}} class="edit-query-name">
|
||||||
|
{{d-icon "pencil-alt"}}
|
||||||
|
</a>
|
||||||
|
{{/unless}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="desc">
|
||||||
|
{{this.selectedItem.description}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#unless this.selectedItem.destroyed}}
|
||||||
|
<div class="pull-left">
|
||||||
|
<div class="groups">
|
||||||
|
<span class="label">{{i18n "explorer.allow_groups"}}</span>
|
||||||
|
<span>
|
||||||
|
<MultiSelect
|
||||||
|
@value={{this.selectedItem.group_ids}}
|
||||||
|
@content={{this.groupOptions}}
|
||||||
|
@options={{hash allowAny=false}}
|
||||||
|
@onChange={{this.updateGroupIds}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
|
||||||
|
{{#if this.editingQuery}}
|
||||||
|
<div class="query-editor {{if this.hideSchema 'no-schema'}}">
|
||||||
|
<div class="panels-flex">
|
||||||
|
<div class="editor-panel">
|
||||||
|
<AceEditor
|
||||||
|
@content={{this.selectedItem.sql}}
|
||||||
|
@mode="sql"
|
||||||
|
@disabled={{this.selectedItem.destroyed}}
|
||||||
|
{{on "click" this.setDirty}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel">
|
||||||
|
<ExplorerSchema
|
||||||
|
@schema={{this.schema}}
|
||||||
|
@hideSchema={{this.hideSchema}}
|
||||||
|
@updateHideSchema={{this.updateHideSchema}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grippie"
|
||||||
|
{{draggable
|
||||||
|
didStartDrag=this.didStartDrag
|
||||||
|
didEndDrag=this.didEndDrag
|
||||||
|
dragMove=this.dragMove
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{d-icon "discourse-expand"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="sql">
|
||||||
|
<CodeView
|
||||||
|
@value={{selectedItem.sql}}
|
||||||
|
@codeClass="sql"
|
||||||
|
@setDirty={{this.setDirty}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
|
||||||
|
<div class="pull-left left-buttons">
|
||||||
|
{{#if this.editingQuery}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.save}}
|
||||||
|
@label="explorer.save"
|
||||||
|
@disabled={{this.saveDisabled}}
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
{{#unless this.editDisabled}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.editQuery}}
|
||||||
|
@label="explorer.edit"
|
||||||
|
@icon="pencil-alt"
|
||||||
|
/>
|
||||||
|
{{/unless}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@action={{this.download}}
|
||||||
|
@label="explorer.export"
|
||||||
|
@disabled={{this.runDisabled}}
|
||||||
|
@icon="download"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{#if this.editingQuery}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.showHelpModal}}
|
||||||
|
@label="explorer.help.label"
|
||||||
|
@icon="question-circle"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pull-right right-buttons">
|
||||||
|
{{#if this.selectedItem.destroyed}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.recover}}
|
||||||
|
@icon="undo"
|
||||||
|
@label="explorer.recover"
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
{{#if this.editingQuery}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.discard}}
|
||||||
|
@icon="undo"
|
||||||
|
@label="explorer.undo"
|
||||||
|
@disabled={{this.saveDisabled}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@action={{this.destroyQuery}}
|
||||||
|
@class="btn-danger"
|
||||||
|
@icon="trash-alt"
|
||||||
|
@label="explorer.delete"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clear"></div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if showCreate}}
|
<form class="query-run" {{on "submit" this.run}}>
|
||||||
<div class="query-create">
|
<ParamInputsWrapper
|
||||||
{{text-field
|
@hasParams={{this.selectedItem.hasParams}}
|
||||||
value=newQueryName
|
@params={{this.selectedItem.params}}
|
||||||
placeholderKey="explorer.create_placeholder"
|
@initialValues={{this.parsedParams}}
|
||||||
}}
|
@paramInfo={{this.selectedItem.param_info}}
|
||||||
{{d-button
|
@updateParams={{this.updateParams}}
|
||||||
action=(action "create")
|
/>
|
||||||
disabled=createDisabled
|
|
||||||
label="explorer.create"
|
|
||||||
icon="plus"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if othersDirty}}
|
{{#if this.runDisabled}}
|
||||||
<div class="warning">
|
{{#if this.saveDisabled}}
|
||||||
{{d-icon "exclamation-triangle"}}
|
<DButton
|
||||||
{{i18n "explorer.others_dirty"}}
|
@label="explorer.run"
|
||||||
</div>
|
@disabled="true"
|
||||||
{{/if}}
|
@class="btn-primary"
|
||||||
|
/>
|
||||||
|
{{else}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.saveAndRun}}
|
||||||
|
@icon="play"
|
||||||
|
@label="explorer.saverun"
|
||||||
|
@class="btn-primary"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.run}}
|
||||||
|
@icon="play"
|
||||||
|
@label="explorer.run"
|
||||||
|
@disabled={{this.runDisabled}}
|
||||||
|
@class="btn-primary"
|
||||||
|
@type="submit"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<label class="query-plan">
|
||||||
|
<Input @type="checkbox" @checked={{this.explain}} name="explain" />
|
||||||
|
{{i18n "explorer.explain_label"}}
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
<hr />
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
{{#if model.length}}
|
<ConditionalLoadingSpinner @condition={{this.loading}} />
|
||||||
{{#unless selectedItem.fake}}
|
|
||||||
<div class="query-edit {{if editName 'editing'}}">
|
|
||||||
{{#if selectedItem}}
|
|
||||||
{{#if editing}}
|
|
||||||
<div class="name">
|
|
||||||
{{d-button
|
|
||||||
action=(action "goHome")
|
|
||||||
icon="chevron-left"
|
|
||||||
class="previous"
|
|
||||||
}}
|
|
||||||
<div class="name-text-field">
|
|
||||||
{{text-field value=selectedItem.name}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="desc">
|
{{#unless this.selectedItem.fake}}
|
||||||
{{textarea
|
<QueryResultsWrapper
|
||||||
value=selectedItem.description
|
@results={{results}}
|
||||||
placeholder=(i18n "explorer.description_placeholder")
|
@showResults={{showResults}}
|
||||||
}}
|
@query={{selectedItem}}
|
||||||
</div>
|
@content={{results}}
|
||||||
{{else}}
|
/>
|
||||||
<div class="name">
|
{{/unless}}
|
||||||
{{d-button
|
|
||||||
action=(action "goHome")
|
|
||||||
icon="chevron-left"
|
|
||||||
class="previous"
|
|
||||||
}}
|
|
||||||
|
|
||||||
<h1>
|
{{#unless this.validQueryPresent}}
|
||||||
{{selectedItem.name}}
|
<div class="container">
|
||||||
{{#unless editDisabled}}
|
<table class="recent-queries">
|
||||||
<a href {{action "editName" class="edit-query-name"}}>
|
<thead class="heading-container">
|
||||||
{{d-icon "pencil-alt"}}
|
<th class="col heading name">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
class="heading-toggle"
|
||||||
|
{{on "click" (fn this.updateSortProperty "name")}}
|
||||||
|
>
|
||||||
|
<TableHeaderToggle
|
||||||
|
@field="name"
|
||||||
|
@labelKey="explorer.query_user"
|
||||||
|
@order={{this.order}}
|
||||||
|
@asc={{(not this.sortDescending)}}
|
||||||
|
@automatic="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="col heading created-by">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
class="heading-toggle"
|
||||||
|
{{on "click" (fn this.updateSortProperty "username")}}
|
||||||
|
>
|
||||||
|
<TableHeaderToggle
|
||||||
|
@field="username"
|
||||||
|
@labelKey="explorer.query_user"
|
||||||
|
@order={{this.order}}
|
||||||
|
@asc={{(not this.sortDescending)}}
|
||||||
|
@automatic="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="col heading group-names">
|
||||||
|
<div class="group-names-header">
|
||||||
|
{{i18n "explorer.query_groups"}}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th class="col heading created-at">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
class="heading-toggle"
|
||||||
|
{{on "click" (fn this.updateSortProperty "last_run_at")}}
|
||||||
|
>
|
||||||
|
<TableHeaderToggle
|
||||||
|
@field="last_run_at"
|
||||||
|
@labelKey="explorer.query_time"
|
||||||
|
@order={{this.order}}
|
||||||
|
@asc={{(not this.sortDescending)}}
|
||||||
|
@automatic="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{#each this.filteredContent as |query|}}
|
||||||
|
<tr class="query-row">
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
{{on "click" this.scrollTop}}
|
||||||
|
href="/admin/plugins/explorer/?id={{query.id}}"
|
||||||
|
>
|
||||||
|
<b class="query-name">{{query.name}}</b>
|
||||||
|
<medium class="query-desc">{{query.description}}</medium>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="query-created-by">
|
||||||
|
{{#if query.username}}
|
||||||
|
<a href="/u/{{query.username}}/activity">
|
||||||
|
<medium>{{query.username}}</medium>
|
||||||
</a>
|
</a>
|
||||||
{{/unless}}
|
{{/if}}
|
||||||
</h1>
|
</td>
|
||||||
</div>
|
<td class="query-group-names">
|
||||||
|
{{#each query.group_names as |group|}}
|
||||||
<div class="desc">
|
<ShareReport @group={{group}} @query={{query}} />
|
||||||
{{selectedItem.description}}
|
{{/each}}
|
||||||
</div>
|
</td>
|
||||||
{{/if}}
|
<td class="query-created-at">
|
||||||
|
{{#if query.last_run_at}}
|
||||||
{{#unless selectedItem.destroyed}}
|
<medium>
|
||||||
<div class="pull-left">
|
{{bound-date query.last_run_at}}
|
||||||
<div class="groups">
|
</medium>
|
||||||
<span class="label">{{i18n "explorer.allow_groups"}}</span>
|
{{else if query.created_at}}
|
||||||
<span>
|
<medium>
|
||||||
{{multi-select
|
{{bound-date query.created_at}}
|
||||||
value=selectedItem.group_ids
|
</medium>
|
||||||
content=groupOptions
|
{{/if}}
|
||||||
allowAny=false
|
</td>
|
||||||
onSelect=(action (mut selectedItem.group_ids))
|
</tr>
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
|
|
||||||
{{! the SQL editor will show the first time you }}
|
|
||||||
{{#if everEditing}}
|
|
||||||
<div class="query-editor {{if hideSchema 'no-schema'}}">
|
|
||||||
<div class="panels-flex">
|
|
||||||
<div class="editor-panel">
|
|
||||||
{{ace-editor
|
|
||||||
content=selectedItem.sql
|
|
||||||
mode="sql"
|
|
||||||
disabled=selectedItem.destroyed
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-panel">
|
|
||||||
<ExplorerSchema
|
|
||||||
@schema={{schema}}
|
|
||||||
@hideSchema={{hideSchema}}
|
|
||||||
@updateHideSchema={{action "updateHideSchema"}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grippie">
|
|
||||||
{{d-icon "discourse-expand"}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="sql">
|
<br />
|
||||||
{{hljs-code-view value=selectedItem.sql codeClass="sql"}}
|
<em class="no-search-results">
|
||||||
</div>
|
{{i18n "explorer.no_search_results"}}
|
||||||
{{/if}}
|
</em>
|
||||||
|
{{/each}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
<div class="clear"></div>
|
<div class="explorer-pad-bottom"></div>
|
||||||
|
|
||||||
<div class="pull-left left-buttons">
|
|
||||||
{{#if everEditing}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "save")
|
|
||||||
label="explorer.save"
|
|
||||||
disabled=saveDisable
|
|
||||||
}}
|
|
||||||
{{else}}
|
|
||||||
{{#unless editDisabled}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "editName")
|
|
||||||
label="explorer.edit"
|
|
||||||
icon="pencil-alt"
|
|
||||||
}}
|
|
||||||
{{/unless}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{d-button
|
|
||||||
action=(action "download")
|
|
||||||
label="explorer.export"
|
|
||||||
disabled=runDisabled
|
|
||||||
icon="download"
|
|
||||||
}}
|
|
||||||
|
|
||||||
{{#if everEditing}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "showHelpModal")
|
|
||||||
label="explorer.help.label"
|
|
||||||
icon="question-circle"
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pull-right right-buttons">
|
|
||||||
{{#if selectedItem.destroyed}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "recover")
|
|
||||||
class=""
|
|
||||||
icon="undo"
|
|
||||||
label="explorer.recover"
|
|
||||||
}}
|
|
||||||
{{else}}
|
|
||||||
{{#if everEditing}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "discard")
|
|
||||||
icon="undo"
|
|
||||||
label="explorer.undo"
|
|
||||||
disabled=saveDisabled
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{d-button
|
|
||||||
action=(action "destroy")
|
|
||||||
class="btn-danger"
|
|
||||||
icon="trash-alt"
|
|
||||||
label="explorer.delete"
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="clear"></div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="query-run" {{action "run" on="submit"}}>
|
|
||||||
<ParamInputsWrapper
|
|
||||||
@hasParams={{selectedItem.hasParams}}
|
|
||||||
@params={{selectedItem.params}}
|
|
||||||
@initialValues={{parsedParams}}
|
|
||||||
@paramInfo={{selectedItem.param_info}}
|
|
||||||
@updateParams={{action "updateParams"}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{{#if runDisabled}}
|
|
||||||
{{#if saveDisabled}}
|
|
||||||
{{d-button
|
|
||||||
label="explorer.run"
|
|
||||||
disabled="true"
|
|
||||||
class="btn-primary"
|
|
||||||
}}
|
|
||||||
{{else}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "saverun")
|
|
||||||
icon="play"
|
|
||||||
label="explorer.saverun"
|
|
||||||
class="btn-primary"
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
{{else}}
|
|
||||||
{{d-button
|
|
||||||
action=(action "run")
|
|
||||||
icon="play"
|
|
||||||
label="explorer.run"
|
|
||||||
disabled=runDisabled
|
|
||||||
class="btn-primary"
|
|
||||||
type="submit"
|
|
||||||
}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<label class="query-plan">
|
|
||||||
{{input type="checkbox" checked=explain name="explain"}}
|
|
||||||
{{i18n "explorer.explain_label"}}
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
{{conditional-loading-spinner condition=loading}}
|
|
||||||
|
|
||||||
{{#unless selectedItem.fake}}
|
|
||||||
<QueryResultsWrapper
|
|
||||||
@results={{results}}
|
|
||||||
@showResults={{showResults}}
|
|
||||||
@query={{selectedItem}}
|
|
||||||
@content={{results}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
|
||||||
|
|
||||||
{{#if showRecentQueries}}
|
|
||||||
<div class="container">
|
|
||||||
<table class="recent-queries">
|
|
||||||
<thead class="heading-container">
|
|
||||||
<th class="col heading name">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
class="heading-toggle"
|
|
||||||
{{action "sortByProperty" "name"}}
|
|
||||||
>
|
|
||||||
{{table-header-toggle
|
|
||||||
field="name"
|
|
||||||
labelKey="explorer.query_name"
|
|
||||||
order=order
|
|
||||||
asc=asc
|
|
||||||
automatic=true
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="col heading created-by">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
class="heading-toggle"
|
|
||||||
{{action "sortByProperty" "username"}}
|
|
||||||
>
|
|
||||||
{{table-header-toggle
|
|
||||||
field="username"
|
|
||||||
labelKey="explorer.query_user"
|
|
||||||
order=order
|
|
||||||
asc=asc
|
|
||||||
automatic=true
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="col heading group-names">
|
|
||||||
<div class="group-names-header">
|
|
||||||
{{i18n "explorer.query_groups"}}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th class="col heading created-at">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
class="heading-toggle"
|
|
||||||
{{action "sortByProperty" "last_run_at"}}
|
|
||||||
>
|
|
||||||
{{table-header-toggle
|
|
||||||
field="last_run_at"
|
|
||||||
labelKey="explorer.query_time"
|
|
||||||
order=order
|
|
||||||
asc=asc
|
|
||||||
automatic=true
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{#each filteredContent as |query|}}
|
|
||||||
<tr class="query-row">
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
{{on "click" this.scrollTop}}
|
|
||||||
href="/admin/plugins/explorer/?id={{query.id}}"
|
|
||||||
>
|
|
||||||
<b class="query-name">{{query.name}}</b>
|
|
||||||
<medium class="query-desc">{{query.description}}</medium>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td class="query-created-by">
|
|
||||||
{{#if query.username}}
|
|
||||||
<a href="/u/{{query.username}}/activity">
|
|
||||||
<medium>{{query.username}}</medium>
|
|
||||||
</a>
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
<td class="query-group-names">
|
|
||||||
{{#each query.group_names as |group|}}
|
|
||||||
<ShareReport @group={{group}} @query={{query}} />
|
|
||||||
{{/each}}
|
|
||||||
</td>
|
|
||||||
<td class="query-created-at">
|
|
||||||
{{#if query.last_run_at}}
|
|
||||||
<medium>
|
|
||||||
{{bound-date query.last_run_at}}
|
|
||||||
</medium>
|
|
||||||
{{else if query.created_at}}
|
|
||||||
<medium>
|
|
||||||
{{bound-date query.created_at}}
|
|
||||||
</medium>
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{else}}
|
|
||||||
<br />
|
|
||||||
<em class="no-search-results">
|
|
||||||
{{i18n "explorer.no_search_results"}}
|
|
||||||
</em>
|
|
||||||
{{/each}}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div class="explorer-pad-bottom"></div>
|
|
||||||
{{/if}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/explorer-container}}
|
{{/if}}
|
|
@ -1 +0,0 @@
|
||||||
<pre><code class={{codeClass}}>{{value}}</code></pre>
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{#d-modal-body title="explorer.help.modal_title"}}
|
<DModalBody @title="explorer.help.modal_title">
|
||||||
{{html-safe (i18n "explorer.help.auto_resolution")}}
|
{{html-safe (i18n "explorer.help.auto_resolution")}}
|
||||||
{{html-safe (i18n "explorer.help.custom_params")}}
|
{{html-safe (i18n "explorer.help.custom_params")}}
|
||||||
{{html-safe (i18n "explorer.help.default_values")}}
|
{{html-safe (i18n "explorer.help.default_values")}}
|
||||||
{{html-safe (i18n "explorer.help.data_types")}}
|
{{html-safe (i18n "explorer.help.data_types")}}
|
||||||
{{/d-modal-body}}
|
</DModalBody>
|
|
@ -22,7 +22,7 @@ ar:
|
||||||
label: "استيراد"
|
label: "استيراد"
|
||||||
modal: "استيراد استعلام"
|
modal: "استيراد استعلام"
|
||||||
unparseable_json: "ملف JSON غير قابل للتحليل"
|
unparseable_json: "ملف JSON غير قابل للتحليل"
|
||||||
wrong_json: "ملف JSON خاطئ. يجب أن يحتوي ملف JSON على كائن \"استعلام\"، والذي يجب أن يحتوي على الأقل على خاصية \"sql\"."
|
wrong_json: 'ملف JSON خاطئ. يجب أن يحتوي ملف JSON على كائن "استعلام"، والذي يجب أن يحتوي على الأقل على خاصية "sql".'
|
||||||
help:
|
help:
|
||||||
label: "المساعدة"
|
label: "المساعدة"
|
||||||
modal_title: "مساعدة مستكشف البيانات"
|
modal_title: "مساعدة مستكشف البيانات"
|
||||||
|
|
|
@ -22,7 +22,7 @@ it:
|
||||||
label: "Importa"
|
label: "Importa"
|
||||||
modal: "Importa una query"
|
modal: "Importa una query"
|
||||||
unparseable_json: "File JSON non analizzabile."
|
unparseable_json: "File JSON non analizzabile."
|
||||||
wrong_json: "File JSON errato. Un file JSON dovrebbe contenere un oggetto \"query\" contenente almeno una proprietà \"sql\"."
|
wrong_json: 'File JSON errato. Un file JSON dovrebbe contenere un oggetto "query" contenente almeno una proprietà "sql".'
|
||||||
help:
|
help:
|
||||||
label: "Guida"
|
label: "Guida"
|
||||||
modal_title: "Guida di Data Explorer"
|
modal_title: "Guida di Data Explorer"
|
||||||
|
|
|
@ -22,7 +22,7 @@ pt_BR:
|
||||||
label: "Importar"
|
label: "Importar"
|
||||||
modal: "Importar Uma Consulta"
|
modal: "Importar Uma Consulta"
|
||||||
unparseable_json: "Arquivo JSON não analisável"
|
unparseable_json: "Arquivo JSON não analisável"
|
||||||
wrong_json: "Arquivo JSON incorreto. Um arquivo JSON deve conter um objeto \"consulta\", que deve ter pelo menos a propriedade \"sql\""
|
wrong_json: 'Arquivo JSON incorreto. Um arquivo JSON deve conter um objeto "consulta", que deve ter pelo menos a propriedade "sql"'
|
||||||
help:
|
help:
|
||||||
label: "Ajuda"
|
label: "Ajuda"
|
||||||
modal_title: "Ajuda do Explorador de Dados"
|
modal_title: "Ajuda do Explorador de Dados"
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
|
|
||||||
hr:
|
hr:
|
||||||
site_settings:
|
site_settings:
|
||||||
data_explorer_enabled: "Uključi \"Data Explorer\" na /admin/plugins/explorer"
|
data_explorer_enabled: 'Uključi "Data Explorer" na /admin/plugins/explorer'
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
|
|
||||||
pt:
|
pt:
|
||||||
site_settings:
|
site_settings:
|
||||||
data_explorer_enabled: "Ativar o «Explorador de Dados» em \"/admin/plugins/explorer\""
|
data_explorer_enabled: 'Ativar o «Explorador de Dados» em "/admin/plugins/explorer"'
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
|
|
||||||
sk:
|
sk:
|
||||||
site_settings:
|
site_settings:
|
||||||
data_explorer_enabled: "Povoľ \"Data explorer\" v /admin/plugins/explorer"
|
data_explorer_enabled: 'Povoľ "Data explorer" v /admin/plugins/explorer'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Configuration file for discourse-translator-bot
|
# Configuration file for discourse-translator-bot
|
||||||
|
|
||||||
files:
|
files:
|
||||||
- source_path: config/locales/client.en.yml
|
- source_path: config/locales/client.en.yml
|
||||||
destination_path: client.yml
|
destination_path: client.yml
|
||||||
- source_path: config/locales/server.en.yml
|
- source_path: config/locales/server.en.yml
|
||||||
destination_path: server.yml
|
destination_path: server.yml
|
||||||
|
|
Loading…
Reference in New Issue