[ct] Add chromium-builds-sk element.
Change-Id: I84e7c4f77e855535046fca958a19ff30e1ab575b
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/304036
Reviewed-by: Ravi Mistry <rmistry@google.com>
diff --git a/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.html b/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.html
new file mode 100644
index 0000000..d5b0bfc
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Chromium Perf Demo</title>
+ <meta charset="utf-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ </head>
+ <body>
+ <h2>chromium-builds-sk demo</h2><theme-chooser-sk></theme-chooser-sk>
+ <div id=container></div>
+ <error-toast-sk></error-toast-sk>
+ </body>
+</html>
diff --git a/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.js b/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.js
new file mode 100644
index 0000000..ca3cd39
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/chromium-builds-sk-demo.js
@@ -0,0 +1,20 @@
+import './index';
+import '../../../infra-sk/modules/theme-chooser-sk';
+import { $$ } from 'common-sk/modules/dom';
+import { fetchMock } from 'fetch-mock';
+import { chromiumRevResult, skiaRevResult } from './test_data';
+import 'elements-sk/error-toast-sk';
+
+fetchMock.config.overwriteRoutes = false;
+fetchMock.post('begin:/_/chromium_rev_data', chromiumRevResult, { delay: 1000 });
+fetchMock.post('begin:/_/skia_rev_data', skiaRevResult, { delay: 1000 });
+// For determining running tasks for the user we just say 2.
+fetchMock.postOnce('begin:/_/get', {
+ data: [], ids: [], pagination: { offset: 0, size: 1, total: 2 }, permissions: [],
+});
+fetchMock.post('begin:/_/get', {
+ data: [], ids: [], pagination: { offset: 0, size: 1, total: 0 }, permissions: [],
+});
+
+const chromiumPerf = document.createElement('chromium-builds-sk');
+$$('#container').appendChild(chromiumPerf);
diff --git a/ct/modules/chromium-builds-sk/chromium-builds-sk.js b/ct/modules/chromium-builds-sk/chromium-builds-sk.js
new file mode 100644
index 0000000..986be7a
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/chromium-builds-sk.js
@@ -0,0 +1,236 @@
+/**
+ * @fileoverview The bulk of the Chromium Builds page of CT.
+ */
+
+import 'elements-sk/icon/delete-icon-sk';
+import 'elements-sk/icon/cancel-icon-sk';
+import 'elements-sk/icon/check-circle-icon-sk';
+import 'elements-sk/icon/help-icon-sk';
+import 'elements-sk/spinner-sk';
+import 'elements-sk/toast-sk';
+import '../../../infra-sk/modules/confirm-dialog-sk';
+import '../chromium-build-selector-sk';
+import '../suggest-input-sk';
+import '../input-sk';
+import '../pageset-selector-sk';
+import '../task-repeater-sk';
+
+import { $$ } from 'common-sk/modules/dom';
+import { fromObject } from 'common-sk/modules/query';
+import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow';
+import { define } from 'elements-sk/define';
+import 'elements-sk/select-sk';
+import { errorMessage } from 'elements-sk/errorMessage';
+import { html } from 'lit-html';
+
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+
+import {
+ moreThanThreeActiveTasksChecker,
+} from '../ctfe_utils';
+
+const template = (el) => html`
+<confirm-dialog-sk id=confirm_dialog></confirm-dialog-sk>
+
+<table class=options>
+ <tr>
+ <td>Chromium Commit Hash</td>
+ <td>
+ <input-sk id=chromium_rev value=LKGR
+ @input=${el._chromiumRevChanged} class=hash-field>
+ </input-sk>
+ </td>
+ <td>
+ <div class="rev-detail-container">
+ <div class="loading-rev-spinner">
+ <spinner-sk id=chromium_spinner alt="Loading Chromium commit details"></spinner-sk>
+ </div>
+ <div class="rev-detail">${el._formatRevData(el._chromiumRevData)}</div>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>Skia Commit Hash</td>
+ <td>
+ <input-sk id=skia_rev value=LKGR @input=${el._skiaRevChanged} class=hash-field></input-sk>
+ </td>
+ <td>
+ <div class="rev-detail-container">
+ <div class="loading-rev-spinner">
+ <spinner-sk id=skia_spinner alt="Loading Skia commit details"></spinner-sk>
+ </div>
+ <div class="rev-detail">${el._formatRevData(el._skiaRevData)}</div>
+ </div>
+ </td>
+ </tr>
+ <tr>
+ <td>Repeat this task</td>
+ <td>
+ <task-repeater-sk id=repeat_after_days></task-repeater-sk>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" class="center">
+ <div class="triggering-spinner">
+ <spinner-sk .active=${el._triggeringTask} alt="Trigger task"></spinner-sk>
+ </div>
+ <button id=submit ?disabled=${el._triggeringTask} @click=${el._validateTask}>Queue Task
+ </button>
+ </td>
+ </tr>
+ <tr>
+ <td colspan=2 class=center>
+ <button id=view_history @click=${el._gotoRunsHistory}>View runs history</button>
+ </td>
+ </tr>
+</table>
+`;
+
+define('chromium-builds-sk', class extends ElementSk {
+ constructor() {
+ super(template);
+ this._triggeringTask = false;
+ this._moreThanThreeActiveTasks = moreThanThreeActiveTasksChecker();
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this._render();
+ // Load LKGR data.
+ this._chromiumRevChanged();
+ this._skiaRevChanged();
+ }
+
+
+ _chromiumRevChanged() {
+ const spinner = $$('#chromium_spinner', this);
+ const newValue = $$('#chromium_rev', this).value;
+ this._chromiumRev = newValue;
+ if (!newValue) {
+ this._chromiumRevData = null;
+ spinner.active = false;
+ return;
+ }
+ spinner.active = true;
+ const params = { rev: newValue };
+ const url = `/_/chromium_rev_data?${fromObject(params)}`;
+
+ fetch(url, { method: 'POST' })
+ .then(jsonOrThrow)
+ .then((json) => {
+ if (this._chromiumRev === newValue) {
+ if (json.commit) {
+ this._chromiumRevData = json;
+ } else {
+ this._chromiumRevData = null;
+ }
+ }
+ })
+ .catch((err) => {
+ if (this._chromiumRev === newValue) {
+ this._chromiumRevData = { error: err };
+ }
+ })
+ .finally(() => {
+ if (this._chromiumRev === newValue) {
+ spinner.active = false;
+ }
+ this._render();
+ });
+ }
+
+ _skiaRevChanged() {
+ const spinner = $$('#skia_spinner', this);
+ const newValue = $$('#skia_rev', this).value;
+ this._skiaRev = newValue;
+ if (!newValue) {
+ this._skiaRevData = null;
+ spinner.active = false;
+ return;
+ }
+ spinner.active = true;
+ const params = { rev: newValue };
+ const url = `/_/skia_rev_data?${fromObject(params)}`;
+
+ fetch(url, { method: 'POST' })
+ .then(jsonOrThrow)
+ .then((json) => {
+ if (this._skiaRev === newValue) {
+ if (json.commit) {
+ this._skiaRevData = json;
+ } else {
+ this._skiaRevData = null;
+ }
+ }
+ })
+ .catch((err) => {
+ if (this._skiaRev === newValue) {
+ this._skiaRevData = { error: err };
+ }
+ })
+ .finally(() => {
+ if (this._skiaRev === newValue) {
+ spinner.active = false;
+ }
+ this._render();
+ });
+ }
+
+ _formatRevData(revData) {
+ if (revData) {
+ if (!revData.error) {
+ return `${revData.commit} by ${revData.author.name} submitted ${
+ revData.committer.time}`;
+ }
+ return revData.error;
+ }
+ return '';
+ }
+
+ _validateTask() {
+ if (!this._chromiumRevData || !this._chromiumRevData.commit) {
+ errorMessage('Please enter a valid Chromium commit hash.');
+ $$('#chromium_rev', this).focus();
+ return;
+ }
+ if (!this._skiaRevData || !this._skiaRevData.commit) {
+ errorMessage('Please enter a valid Skia commit hash.');
+ $$('#skia_rev', this).focus();
+ return;
+ }
+ if (this._moreThanThreeActiveTasks()) {
+ return;
+ }
+ $$('#confirm_dialog', this).open('Proceed with queueing task?')
+ .then(() => this._queueTask())
+ .catch(() => {
+ errorMessage('Unable to queue task');
+ });
+ }
+
+ _queueTask() {
+ this._triggeringTask = true;
+ const params = {};
+ params.chromium_rev = this._chromiumRevData.commit;
+ params.chromium_rev_ts = this._chromiumRevData.committer.time;
+ params.skia_rev = this._skiaRevData.commit;
+ params.repeat_after_days = $$('#repeat_after_days', this).frequency;
+
+ fetch('/_/add_chromium_build_task', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(params),
+ })
+ .then(() => this._gotoRunsHistory())
+ .catch((e) => {
+ this._triggeringTask = false;
+ errorMessage(e);
+ });
+ }
+
+ _gotoRunsHistory() {
+ window.location.href = '/chromium_builds_runs/';
+ }
+});
diff --git a/ct/modules/chromium-builds-sk/chromium-builds-sk.scss b/ct/modules/chromium-builds-sk/chromium-builds-sk.scss
new file mode 100644
index 0000000..1a103eb
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/chromium-builds-sk.scss
@@ -0,0 +1,47 @@
+@import '../colors.css';
+
+chromium-builds-sk {
+ background-color: var(--surface);
+ color: var(--on-surface);
+ table.options {
+ padding-left: 2em;
+ margin: 1em;
+ border-style: solid;
+ }
+
+ select-sk {
+ * {
+ padding: 0;
+ }
+ [selected] {
+ background-color: var(--primary-variant);
+ }
+ }
+
+ .triggering-spinner {
+ margin: auto;
+ vertical-align: middle;
+ }
+
+
+ .long-field {
+ width: 40em;
+ }
+
+ .smaller-font {
+ font-size: 80%;
+ }
+
+ table.options>tbody>tr>td {
+ padding: 1em 2em;
+ }
+
+ td.center {
+ text-align:center;
+ padding-top:2em;
+ }
+}
+
+.darkmode chromium-builds-sk table.options {
+ background-color: var(--surface-2dp);
+}
diff --git a/ct/modules/chromium-builds-sk/chromium-builds-sk_test.js b/ct/modules/chromium-builds-sk/chromium-builds-sk_test.js
new file mode 100644
index 0000000..c5205e1
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/chromium-builds-sk_test.js
@@ -0,0 +1,174 @@
+import './index';
+
+import { $, $$ } from 'common-sk/modules/dom';
+import { fetchMock } from 'fetch-mock';
+import { chromiumRevResult, skiaRevResult } from './test_data';
+import { buildsJson } from '../chromium-build-selector-sk/test_data';
+import {
+ eventPromise,
+ setUpElementUnderTest,
+} from '../../../infra-sk/modules/test_util';
+
+describe('chromium-builds-sk', () => {
+ fetchMock.config.overwriteRoutes = false;
+ const factory = setUpElementUnderTest('chromium-builds-sk');
+ // Returns a new element with the pagesets, task priorirites, and
+ // active tasks fetches complete, and benchmarks and platforms set.
+ const newInstance = async (activeTasks) => {
+ mockActiveTasks(activeTasks);
+ // Create a unique dummy result to distinguish from when we load
+ // non-LKGR revisions.
+ const lkgrDummy = Object.assign({}, chromiumRevResult);
+ lkgrDummy.commit = 'aaaaaa';
+ fetchMock.postOnce('begin:/_/chromium_rev_data?rev=LKGR', lkgrDummy);
+ fetchMock.postOnce('begin:/_/skia_rev_data?rev=LKGR', lkgrDummy);
+ const ele = factory();
+ await fetchMock.flush(true);
+ return ele;
+ };
+
+ // Make our test object global to make helper functions convenient.
+ let chromiumBuilds;
+
+ afterEach(() => {
+ // Check all mock fetches called at least once and reset.
+ expect(fetchMock.done()).to.be.true;
+ fetchMock.reset();
+ });
+
+ const mockActiveTasks = (n) => {
+ n = n || 0;
+ // For running tasks for the user we put a nonzero total in one of the
+ // responses, and 0 in the remaining 6.
+ fetchMock.postOnce('begin:/_/get', {
+ data: [],
+ ids: [],
+ pagination: { offset: 0, size: 1, total: n },
+ permissions: [],
+ });
+ fetchMock.post('begin:/_/get', {
+ data: [],
+ ids: [],
+ pagination: { offset: 0, size: 1, total: 0 },
+ permissions: [],
+ }, { repeat: 6 });
+ };
+
+ const clickSubmit = () => {
+ $$('#submit', chromiumBuilds).click();
+ };
+
+ const setChromiumRev = async (value, result) => {
+ if (result) {
+ fetchMock.postOnce('begin:/_/chromium_rev_data', result);
+ }
+ const input = $$('#chromium_rev');
+ input.value = value;
+ input.dispatchEvent(new Event('input', {
+ bubbles: true,
+ }));
+ await fetchMock.flush(true);
+ };
+
+ const setSkiaRev = async (value, result) => {
+ if (result) {
+ fetchMock.postOnce('begin:/_/skia_rev_data', result);
+ }
+ const input = $$('#skia_rev');
+ input.value = value;
+ input.dispatchEvent(new Event('input', {
+ bubbles: true,
+ }));
+ await fetchMock.flush(true);
+ };
+
+ it('loads, has defaults set', async () => {
+ chromiumBuilds = await newInstance();
+ expect($$('#repeat_after_days', this)).to.have.property('frequency', '0');
+ expect($$('#chromium_rev', this)).to.have.property('value', 'LKGR');
+ expect($$('#skia_rev', this)).to.have.property('value', 'LKGR');
+ });
+
+ it('requires Chromium hash', async () => {
+ chromiumBuilds = await newInstance();
+ // Empty input is invalid.
+ await setChromiumRev('');
+ let event = eventPromise('error-sk');
+ clickSubmit();
+ let err = await event;
+ expect(err.detail.message).to.contain('Please enter a valid Chromium commit hash.');
+ // Backend doesn't give a valid revision.
+ await setChromiumRev('abc', 503);
+ event = eventPromise('error-sk');
+ clickSubmit();
+ err = await event;
+ expect(err.detail.message).to.contain('Please enter a valid Chromium commit hash.');
+ });
+
+ it('requires Skia hash', async () => {
+ chromiumBuilds = await newInstance();
+ // Empty input is invalid.
+ await setSkiaRev('');
+ let event = eventPromise('error-sk');
+ clickSubmit();
+ let err = await event;
+ expect(err.detail.message).to.contain('Please enter a valid Skia commit hash.');
+ // Backend doesn't give a valid revision.
+ await setSkiaRev('abc', 503);
+ event = eventPromise('error-sk');
+ clickSubmit();
+ err = await event;
+ expect(err.detail.message).to.contain('Please enter a valid Skia commit hash.');
+ });
+
+ it('triggers a new task, LKGR', async () => {
+ chromiumBuilds = await newInstance();
+ // Karma can't handlje page reloads, so disable it.
+ chromiumBuilds._gotoRunsHistory = () => { };
+ clickSubmit();
+ fetchMock.postOnce('begin:/_/add_chromium_build_task', {});
+ $('#confirm_dialog button')[1].click(); // brittle way to press 'ok'
+ await fetchMock.flush(true);
+ const taskJson = JSON.parse(fetchMock.lastOptions().body);
+ const expectation = {
+ chromium_rev: 'aaaaaa',
+ chromium_rev_ts: 'Mon May 08 21:08:33 2017',
+ skia_rev: 'aaaaaa',
+ repeat_after_days: '0',
+ };
+
+ expect(taskJson).to.deep.equal(expectation);
+ });
+
+ it('triggers a new task, explicit', async () => {
+ chromiumBuilds = await newInstance();
+ // Karma can't handlje page reloads, so disable it.
+ chromiumBuilds._gotoRunsHistory = () => { };
+ await setChromiumRev('abc', chromiumRevResult);
+ await setSkiaRev('abc', skiaRevResult);
+ clickSubmit();
+ fetchMock.postOnce('begin:/_/add_chromium_build_task', {});
+ $('#confirm_dialog button')[1].click(); // brittle way to press 'ok'
+ await fetchMock.flush(true);
+ const taskJson = JSON.parse(fetchMock.lastOptions().body);
+ const expectation = {
+ chromium_rev: 'deadbeefdeadbeef',
+ chromium_rev_ts: 'Mon May 08 21:08:33 2017',
+ skia_rev: '123456789abcdef',
+ repeat_after_days: '0',
+ };
+
+ console.log(taskJson);
+ console.log(expectation);
+ expect(taskJson).to.deep.equal(expectation);
+ });
+
+ it('rejects if too many active tasks', async () => {
+ // Report user as having 4 active tasks.
+ chromiumBuilds = await newInstance(4);
+ const event = eventPromise('error-sk');
+ clickSubmit();
+ const err = await event;
+ expect(err.detail.message).to.contain('You have 4 currently running tasks');
+ });
+});
diff --git a/ct/modules/chromium-builds-sk/index.js b/ct/modules/chromium-builds-sk/index.js
new file mode 100644
index 0000000..9378d73
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/index.js
@@ -0,0 +1,2 @@
+import './chromium-builds-sk';
+import './chromium-builds-sk.scss';
diff --git a/ct/modules/chromium-builds-sk/test_data.js b/ct/modules/chromium-builds-sk/test_data.js
new file mode 100644
index 0000000..6e4db65
--- /dev/null
+++ b/ct/modules/chromium-builds-sk/test_data.js
@@ -0,0 +1,59 @@
+export const chromiumRevResult = {
+ commit: 'deadbeefdeadbeef',
+ tree: 'badbeef',
+ parents: [
+ '123456789',
+ ],
+ author: {
+ name: 'alice',
+ email: 'alice@bob.org',
+ time: 'Mon May 08 21:08:33 2017',
+ },
+ committer: {
+ name: 'Commit bot',
+ email: 'commit-bot@chromium.org',
+ time: 'Mon May 08 21:08:33 2017',
+ },
+ message: 'Do a thing',
+ tree_diff: [
+ {
+ type: 'modify',
+ old_id: '123456',
+ old_mode: 33188,
+ old_path: 'some/path',
+ new_id: '789123',
+ new_mode: 33188,
+ new_path: 'some/path',
+ },
+ ],
+};
+
+export const skiaRevResult = {
+ commit: '123456789abcdef',
+ tree: 'abcdef123456789',
+ parents: [
+ 'deadbeef',
+ ],
+ author: {
+ name: 'bob',
+ email: 'bob@alice.org',
+ time: 'Mon May 08 21:08:33 2017',
+ },
+ committer: {
+ name: 'Commit bot',
+ email: 'commit-bot@chromium.org',
+ time: 'Mon May 08 21:08:33 2017',
+ },
+ message: 'Do a thing',
+ tree_diff: [
+ {
+ type: 'modify',
+ old_id: '123456',
+ old_mode: 33188,
+ old_path: 'some/path',
+ new_id: '789123',
+ new_mode: 33188,
+ new_path: 'some/path',
+ },
+ ],
+};
diff --git a/ct/package-lock.json b/ct/package-lock.json
index cb2b410..f4e70df 100644
--- a/ct/package-lock.json
+++ b/ct/package-lock.json
@@ -3158,14 +3158,15 @@
}
},
"fetch-mock": {
- "version": "7.3.9",
- "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-7.3.9.tgz",
- "integrity": "sha512-PgsTbiQBNapFz2P2UwDl3gowK3nZqfV4HdyDZ1dI4eTGGH9MLAeBglIPbyDbbNQoGYBOfla6/9uaiq7az2z4Aw==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-7.7.3.tgz",
+ "integrity": "sha512-I4OkK90JFQnjH8/n3HDtWxH/I6D1wrxoAM2ri+nb444jpuH3RTcgvXx2el+G20KO873W727/66T7QhOvFxNHPg==",
"dev": true,
"requires": {
"babel-polyfill": "^6.26.0",
"core-js": "^2.6.9",
"glob-to-regexp": "^0.4.0",
+ "lodash.isequal": "^4.5.0",
"path-to-regexp": "^2.2.1",
"whatwg-url": "^6.5.0"
},
@@ -6166,6 +6167,12 @@
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+ "dev": true
+ },
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
diff --git a/ct/package.json b/ct/package.json
index b3ab530..8aca22e 100644
--- a/ct/package.json
+++ b/ct/package.json
@@ -21,7 +21,7 @@
"chai": "~4.2.0",
"css-loader": "^3.5.3",
"cssmin": "~0.4.2",
- "fetch-mock": "~7.3.9",
+ "fetch-mock": "~7.7.0",
"html-loader": "^0.5.5",
"html-minifier": "~3.0.0",
"karma": "~4.3.0",