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}) ); }