diff --git a/addon/apex-runner.css b/addon/apex-runner.css
index 19511334..234e863a 100644
--- a/addon/apex-runner.css
+++ b/addon/apex-runner.css
@@ -7,3 +7,8 @@
-webkit-mask-image: url('chrome-extension://__MSG_@@extension_id__/images/textarea.svg');
background-color: #04844B;
}
+.scrolltable-wrapper {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0;
+}
\ No newline at end of file
diff --git a/addon/apex-runner.html b/addon/apex-runner.html
index 3ed02602..d60da0df 100644
--- a/addon/apex-runner.html
+++ b/addon/apex-runner.html
@@ -3,6 +3,7 @@
Apex Runner
+
diff --git a/addon/apex-runner.js b/addon/apex-runner.js
index 05997f74..23149baf 100644
--- a/addon/apex-runner.js
+++ b/addon/apex-runner.js
@@ -69,6 +69,8 @@ class Model {
this.sfHost = sfHost;
this.tableModel = new TableModel(sfHost, this.didUpdate.bind(this));
this.resultTableCallback = (d) => this.tableModel.dataChange(d);
+ this.tableJobModel = new TableModel(sfHost, this.didUpdate.bind(this));
+ this.resultJobTableCallback = (d) => this.tableJobModel.dataChange(d);
this.editor = null;
this.initialScript = "";
this.describeInfo = new DescribeInfo(this.spinFor.bind(this), () => {
@@ -90,6 +92,7 @@ class Model {
this.executeStatus = "Ready";
this.executeError = null;
this.logs = null;
+ this.jobs = null;
this.scriptHistory = new ScriptHistory("insextScriptHistory", 100);
this.selectedHistoryEntry = null;
this.savedHistory = new ScriptHistory("insextSavedScriptHistory", 50);
@@ -151,6 +154,9 @@ class Model {
updatedLogs() {
this.resultTableCallback(this.logs);
}
+ updatedJobs() {
+ this.resultJobTableCallback(this.jobs);
+ }
setscriptName(value) {
this.scriptName = value;
}
@@ -646,19 +652,16 @@ class Model {
if (!data.done) {
let pr = vm.batchHandler(sfConn.rest(data.nextRecordsUrl, {}), vm, logs, onData);
vm.executeError = null;
- vm.logs = logs;
onData(false);
vm.didUpdate();
return pr;
}
if (logs.records.length == 0) {
vm.executeError = null;
- vm.logs = logs;
onData(true);
return null;
}
vm.executeError = null;
- vm.logs = logs;
onData(true);
return null;
}, err => {
@@ -668,13 +671,11 @@ class Model {
if (logs.totalSize != -1) {
// We already got some data. Show it, and indicate that not all data was executed
vm.executeError = null;
- vm.logs = logs;
onData(true);
return null;
}
vm.executeStatus = "Error";
vm.executeError = err.message;
- vm.logs = null;
onData(true);
return null;
});
@@ -831,6 +832,9 @@ class Model {
let logs = new RecordTable();
logs.describeInfo = vm.describeInfo;
logs.sfHost = vm.sfHost;
+ let jobs = new RecordTable();
+ jobs.describeInfo = vm.describeInfo;
+ jobs.sfHost = vm.sfHost;
let pollId = 1;
let handshake = await sfConn.rest("/cometd/" + apiVersion, {
method: "POST",
@@ -910,12 +914,17 @@ class Model {
advice = arsp.advice;
}
if (response.find(rsp => rsp != null && rsp.data != null && rsp.channel == "/systemTopic/Logging")) {
- let queryLogs = "SELECT Id, Application, Status, Operation, StartTime, LogLength, LogUser.Name FROM ApexLog ORDER BY StartTime DESC";
+ let queryLogs = "SELECT Id, Application, Status, Operation, StartTime, LogLength, LogUser.Name FROM ApexLog ORDER BY StartTime DESC LIMIT 100";
+ let queryJobs = "SELECT Id, JobType, ApexClass.Name, CompletedDate, CreatedBy.Name, CreatedDate, ExtendedStatus, TotalJobItems , JobItemsProcessed, NumberOfErrors, Status FROM AsyncApexJob WHERE JobType in ('BatchApex', 'Queueable') ORDER BY CreatedDate desc LIMIT 100";
//logs.resetTable();
logs = new RecordTable();
logs.describeInfo = vm.describeInfo;
logs.sfHost = vm.sfHost;
+ jobs = new RecordTable();
+ jobs.describeInfo = vm.describeInfo;
+ jobs.sfHost = vm.sfHost;
await vm.batchHandler(sfConn.rest("/services/data/v" + apiVersion + "/query/?q=" + encodeURIComponent(queryLogs), {}), vm, logs, () => {
+ vm.logs = logs;
vm.updatedLogs();
})
.catch(error => {
@@ -925,6 +934,17 @@ class Model {
vm.executeError = "UNEXPECTED EXCEPTION:" + error;
vm.logs = null;
});
+ await vm.batchHandler(sfConn.rest("/services/data/v" + apiVersion + "/query/?q=" + encodeURIComponent(queryJobs), {}), vm, jobs, () => {
+ vm.jobs = jobs;
+ vm.updatedJobs();
+ })
+ .catch(error => {
+ console.error(error);
+ vm.isWorking = false;
+ vm.executeStatus = "Error";
+ vm.executeError = "UNEXPECTED EXCEPTION:" + error;
+ vm.logs = null;
+ });
}
//TODO table to query job in run
@@ -935,6 +955,7 @@ class Model {
recalculateSize() {
// Investigate if we can use the IntersectionObserver API here instead, once it is available.
this.tableModel.viewportChange();
+ this.tableJobModel.viewportChange();
}
}
@@ -1052,12 +1073,24 @@ class App extends React.Component {
this.onStopExecute = this.onStopExecute.bind(this);
this.onClick = this.onClick.bind(this);
this.openEmptyLog = this.openEmptyLog.bind(this);
+ this.onTabSelect = this.onTabSelect.bind(this);
+
+ this.state = {
+ selectedTabId: 1
+ };
+ }
+ onTabSelect(e) {
+ e.preventDefault();
+ this.setState({selectedTabId: e.target.tabIndex});
}
onClick(){
let {model} = this.props;
if (model && model.tableModel) {
model.tableModel.onClick();
}
+ if (model && model.tableJobModel) {
+ model.tableJobModel.onClick();
+ }
}
onSelectHistoryEntry(e) {
let {model} = this.props;
@@ -1298,14 +1331,29 @@ class App extends React.Component {
h("div", {className: "area", id: "result-area"},
h("div", {className: "result-bar"},
h("h1", {}, "Execute Result"),
+ h("div", {className: "slds-tabs_default flex-area", style: {height: "inherit"}},
+ h("ul", {className: "options-tab-container slds-tabs_default__nav", role: "tablist"},
+ h("li", {className: "options-tab slds-text-align_center slds-tabs_default__item" + (this.state.selectedTabId === 1 ? " slds-is-active" : ""), title: "Logs", tabIndex: 1, role: "presentation", onClick: this.onTabSelect},
+ h("a", {className: "slds-tabs_default__link", href: "#", role: "tab", tabIndex: 1, id: "tab-default-1__item"}, "Logs")
+ ),
+ h("li", {className: "options-tab slds-text-align_center slds-tabs_default__item" + (this.state.selectedTabId === 2 ? " slds-is-active" : ""), title: "Jobs", tabIndex: 2, role: "presentation", onClick: this.onTabSelect},
+ h("a", {className: "slds-tabs_default__link", href: "#", role: "tab", tabIndex: 2, id: "tab-default-2__item"}, "Jobs")
+ )
+ ),
+ ),
h("span", {className: "result-status flex-right"},
h("span", {}, model.executeStatus),
h("button", {className: "cancel-btn", disabled: !model.isWorking, onClick: this.onStopExecute}, "Stop polling logs"),
h("button", {onClick: this.openEmptyLog}, "Open empty logs"),
- )
+ ),
),
- h("textarea", {id: "result-text", readOnly: true, value: model.executeError || "", hidden: model.executeError == null}),
- h(ScrollTable, {model: model.tableModel, hidden: model.executeError != null})
+ h("textarea", {className: "result-text", readOnly: true, value: model.executeError || "", hidden: model.executeError == null}),
+ h("div", {className: "scrolltable-wrapper", hidden: (model.executeError != null || this.state.selectedTabId != 1)},
+ h(ScrollTable, {model: model.tableModel})
+ ),
+ h("div", {className: "scrolltable-wrapper", hidden: (model.executeError != null || this.state.selectedTabId != 2)},
+ h(ScrollTable, {model: model.tableJobModel})
+ )
)
);
}
diff --git a/addon/data-export.css b/addon/data-export.css
index 74de81a7..05bd33c0 100644
--- a/addon/data-export.css
+++ b/addon/data-export.css
@@ -120,7 +120,7 @@ textarea {
border: 1px solid #DDDBDA;
}
-textarea[hidden], .autocomplete-results[hidden] {
+textarea[hidden], div[hidden] {
display: none !important;
}
@@ -159,7 +159,7 @@ textarea[hidden], .autocomplete-results[hidden] {
color: #8c8c8c;
}
-#result-text {
+.result-text {
flex: 1 1 0;
resize: none;
white-space: pre;
@@ -804,4 +804,11 @@ button.toggle .button-icon {
display: block;
color: #506882;
font-size: .8125rem;
+}
+.result-status {
+ white-space: nowrap;
+}
+.result-status span,
+.result-status button{
+ margin-left: 1em;
}
\ No newline at end of file
diff --git a/addon/data-export.js b/addon/data-export.js
index 311607f3..5f975450 100644
--- a/addon/data-export.js
+++ b/addon/data-export.js
@@ -2121,7 +2121,7 @@ class App extends React.Component {
h("button", {className: "cancel-btn", disabled: !model.isWorking, onClick: this.onStopExport}, "Stop"),
),
),
- h("textarea", {id: "result-text", readOnly: true, value: model.exportError || "", hidden: model.exportError == null}),
+ h("textarea", {className: "result-text", readOnly: true, value: model.exportError || "", hidden: model.exportError == null}),
h(ScrollTable, {model: model.tableModel, hidden: model.exportError != null})
)
);
diff --git a/addon/data-load.css b/addon/data-load.css
index 581d353d..dc3c4372 100644
--- a/addon/data-load.css
+++ b/addon/data-load.css
@@ -174,6 +174,10 @@ td.scrolltable-cell-diff {
-webkit-mask-position: center;
}
+.pop-menu a.abord-job .icon {
+ background-color: #706E6B;
+}
+
.pop-menu a.download-salesforce .icon {
background-color: #61fab8;
}
diff --git a/addon/data-load.js b/addon/data-load.js
index b339c4f0..0614642e 100644
--- a/addon/data-load.js
+++ b/addon/data-load.js
@@ -374,6 +374,11 @@ export class TableModel {
if (!force && this.firstRowTop <= this.scrollTop && (this.lastRowTop >= this.scrollTop + this.offsetHeight || this.lastRowIdx == this.rowCount)
&& this.firstColLeft <= this.scrollLeft && (this.lastColLeft >= this.scrollLeft + this.offsetWidth || this.lastColIdx == this.colCount)) {
+ if (this.scrolledHeight != this.totalHeight || this.scrolledWidth != this.totalWidth){
+ this.scrolledHeight = this.totalHeight;
+ this.scrolledWidth = this.totalWidth;
+ this.didUpdate();
+ }
return;
}
@@ -572,6 +577,9 @@ export class TableModel {
queryLogArgs.set("recordId", cell.recordId);
cell.links.push({withIcon: true, href: "log.html?" + queryLogArgs, label: "View Log", className: "view-inspector", action: ""});
}
+ if (cell.objectType == "AsyncApexJob") {
+ cell.links.push({withIcon: true, href: cell.recordId, label: "Abord Job", className: "abord-job", action: "abord"});
+ }
// If the recordId ends with 0000000000AAA it is a dummy ID such as the ID for the master record type 012000000000000AAA
if (cell.recordId && self.isRecordId(cell.recordId) && !cell.recordId.endsWith("0000000000AAA")) {
@@ -637,6 +645,11 @@ class ScrollTableCell extends React.Component {
}
+ abordJob(e){
+ let script = "System.abortJob('" + e.target.href + "')";
+ sfConn.rest("/services/data/v" + apiVersion + "/tooling/executeAnonymous/?anonymousBody=" + encodeURIComponent(script), {})
+ .catch(error => { console.error(error); });
+ }
downloadFile(e){
sfConn.rest(e.target.href, {responseType: "text/csv"}).then(data => {
let downloadLink = document.createElement("a");
@@ -696,6 +709,8 @@ class ScrollTableCell extends React.Component {
attributes.onClick = this.copyToClipboard;
} else if (l.action == "download") {
attributes.onClick = this.downloadFile;
+ } else if (l.action == "abord") {
+ attributes.onClick = this.abordJob;
}
return h("a", attributes, arr);
})) : ""
diff --git a/addon/streaming.js b/addon/streaming.js
index ee873423..9261bbe4 100644
--- a/addon/streaming.js
+++ b/addon/streaming.js
@@ -383,7 +383,7 @@ class Monitor extends React.Component {
render() {
let {model} = this.props;
return h("div", {className: "area", id: "result-area"},
- h("textarea", {id: "result-text", readOnly: true, value: model.executeError || "", hidden: model.executeError == null}),
+ h("textarea", {className: "result-text", readOnly: true, value: model.executeError || "", hidden: model.executeError == null}),
h(ScrollTable, {model: model.tableModel})
);
}