[task scheduler] Add task-scheduler-scaffold-sk element
Change-Id: I1791ccf17bf252837504430c7a9af8c89d122d23
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/329218
Reviewed-by: Weston Tracey <westont@google.com>
Commit-Queue: Eric Boren <borenet@google.com>
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/index.ts b/task_scheduler/modules/task-scheduler-scaffold-sk/index.ts
new file mode 100644
index 0000000..8790bcc
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/index.ts
@@ -0,0 +1,2 @@
+import './task-scheduler-scaffold-sk';
+import './task-scheduler-scaffold-sk.scss';
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.html b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.html
new file mode 100644
index 0000000..66c42bf
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>task-scheduler-scaffold-sk demo</title>
+ </head>
+ <body>
+ <task-scheduler-scaffold-sk title="task-scheduler-scaffold-sk demo">
+ <main>Content goes here.</main>
+ </task-scheduler-scaffold-sk>
+ </body>
+</html>
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.ts b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.ts
new file mode 100644
index 0000000..ad9456f
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk-demo.ts
@@ -0,0 +1,15 @@
+import fetchMock from 'fetch-mock';
+
+const loginURL = 'https://' + window.location.host + '/loginstatus/';
+fetchMock.get(loginURL, {
+ Email: 'user@google.com',
+ LoginURL: 'https://accounts.google.com/',
+ IsAGoogler: true,
+});
+fetchMock.get('https://skia.org/loginstatus/', {
+ Email: 'user@google.com',
+ LoginURL: 'https://accounts.google.com/',
+ IsAGoogler: true,
+});
+
+import './index';
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.scss b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.scss
new file mode 100644
index 0000000..cb2ace5
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.scss
@@ -0,0 +1,64 @@
+@import '~elements-sk/colors';
+@import '../colors';
+
+task-scheduler-scaffold-sk {
+ * {
+ font-family: Roboto, Helvetica, Arial, 'Bitstream Vera Sans', sans-serif;
+ }
+
+ header {
+ h1 {
+ font-weight: 400;
+ font-size: 1.5em;
+ padding: 0 0.25em;
+ }
+
+ .spinner-spacer {
+ min-width: 40px;
+ }
+
+ spinner-sk {
+ width: 24px;
+ height: 24px;
+ margin: 0;
+ border: 8px solid var(--on-primary);
+ border-left: 8px solid var(--primary-variant);
+ }
+ }
+
+ nav {
+ display: flex;
+ flex-direction: column;
+ a {
+ padding: 1em 1em;
+ color: var(--on-surface); /* Override User Agent SS*/
+ text-decoration: none;
+ }
+ span {
+ padding: 0 0.5em;
+ vertical-align: middle;
+ }
+ }
+
+ login-sk {
+ margin-right: 1em;
+ }
+
+ /* Overrides to elements-sk elements since they set color internally. */
+ svg.icon-sk-svg {
+ fill: var(--on-surface);
+ }
+
+ app-sk aside {
+ /* Soften the sidebar border a bit. */
+ border-right: 1px solid var(--surface-2dp, #ddd);
+ }
+}
+
+// Make sure the app doesn't leave empty space on the bottom. e.g. blank
+// white in darkmode.
+html,
+body,
+app-sk {
+ height: 100%;
+}
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.ts b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.ts
new file mode 100644
index 0000000..efc827a
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk.ts
@@ -0,0 +1,224 @@
+/**
+ * @module task_scheduler/modules/task-scheduler-scaffold-sk
+ * @description <h2><code>task-scheduler-scaffold-sk</code></h2>
+ *
+ * Contains the title bar, side bar, and error-toast for all the Task Scheduler
+ * pages. The rest of every Task Scheduler page should be a child of this
+ * element.
+ *
+ * Has a spinner-sk that can be activated when it hears "begin-task" events and
+ * keeps spinning until it hears an equal number of "end-task" events.
+ *
+ * The error-toast element responds to fetch-error events and normal error-sk
+ * events.
+ *
+ * @attr {string} app_title - The title to show in the page banner.
+ *
+ * @attr {boolean} testing_offline - If we should operate entirely in offline
+ * mode.
+ */
+import { define } from 'elements-sk/define';
+import { errorMessage } from 'elements-sk/errorMessage';
+import { html } from 'lit-html';
+import { ElementSk } from '../../../infra-sk/modules/ElementSk';
+
+import '../../../infra-sk/modules/app-sk';
+import '../../../infra-sk/modules/login-sk';
+import '../../../infra-sk/modules/theme-chooser-sk';
+
+import 'elements-sk/error-toast-sk';
+import 'elements-sk/icon/block-icon-sk';
+import 'elements-sk/icon/help-icon-sk';
+import 'elements-sk/icon/home-icon-sk';
+import 'elements-sk/icon/search-icon-sk';
+import 'elements-sk/icon/send-icon-sk';
+import 'elements-sk/spinner-sk';
+import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk';
+
+/**
+ * Moves the elements from one NodeList to the given HTMLElement.
+ */
+function move(from: HTMLCollection | NodeList, to: HTMLElement) {
+ Array.prototype.slice.call(from).forEach((ele) => to.appendChild(ele));
+}
+
+export class TaskSchedulerScaffoldSk extends ElementSk {
+ private main: HTMLElement | null = null;
+ private busyTaskCount: number = 0;
+ private spinner: SpinnerSk | null = null;
+ private static template = (ele: TaskSchedulerScaffoldSk) => html`
+ <app-sk>
+ <header class="primary-container-themes-sk">
+ <h1>${ele.title}</h1>
+ <div class="spinner-spacer">
+ <spinner-sk></spinner-sk>
+ </div>
+ <div class="spacer"></div>
+ <login-sk
+ ?testing_offline=${ele.testingOffline}
+ login_host="${ele.loginHost}"
+ ></login-sk>
+ <theme-chooser-sk></theme-chooser-sk>
+ </header>
+
+ <aside class="surface-themes-sk">
+ <nav>
+ <a href="/" tab-index="0">
+ <home-icon-sk></home-icon-sk><span>Home</span>
+ </a>
+ <a href="/trigger">
+ <send-icon-sk></send-icon-sk><span>Trigger Tasks</span>
+ </a>
+ <a href="/skip_tasks">
+ <block-icon-sk></block-icon-sk><span>Skip Tasks</span>
+ </a>
+ <a href="/jobs/search">
+ <search-icon-sk></search-icon-sk><span>Search Jobs</span>
+ </a>
+ <a
+ href="https://skia.googlesource.com/buildbot/+/master/task_scheduler/README.md"
+ >
+ <help-icon-sk></help-icon-sk><span>Docs</span>
+ </a>
+ </nav>
+ </aside>
+
+ <main></main>
+
+ <footer><error-toast-sk></error-toast-sk></footer>
+ </app-sk>
+ `;
+
+ constructor() {
+ super(TaskSchedulerScaffoldSk.template);
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ // Don't call more than once.
+ if (this.main) {
+ return;
+ }
+ this.addEventListener('begin-task', this.addBusyTask);
+ this.addEventListener('end-task', this.finishedTask);
+ this.addEventListener('fetch-error', this.fetchError);
+
+ // We aren't using shadow dom so we need to manually move the children of
+ // task-scheduler-scaffold-sk to be children of 'main'. We have to do this for the
+ // existing elements and for all future mutations.
+
+ // Create a temporary holding spot for elements we're moving.
+ const div = document.createElement('div');
+ move(this.children, div);
+
+ // Now that we've moved all the old children out of the way we can render
+ // the template.
+ this._render();
+
+ this.spinner = this.querySelector('header spinner-sk');
+
+ // Move the old children back under main.
+ this.main = this.querySelector('main');
+ if (this.main) {
+ move(div.children, this.main);
+ }
+
+ // Move all future children under main also.
+ const observer = new MutationObserver((mutList) => {
+ mutList.forEach((mut) => {
+ if (this.main) {
+ move(mut.addedNodes, this.main);
+ }
+ });
+ });
+ observer.observe(this, { childList: true });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener('begin-task', this.addBusyTask);
+ this.removeEventListener('end-task', this.finishedTask);
+ this.removeEventListener('fetch-error', this.fetchError);
+ }
+
+ /** @prop loginHost Host name used for login. */
+ get loginHost() {
+ return window.location.host;
+ }
+
+ /** @prop title Reflects the app_title attribute for ease of use. */
+ get title() {
+ return <string>this.getAttribute('title');
+ }
+
+ set title(val: string) {
+ this.setAttribute('title', val);
+ }
+
+ /** @prop busy Indicates if there any on-going tasks (e.g. RPCs). This also
+ * mirrors the status of the embedded spinner-sk. Read-only. */
+ get busy() {
+ return !!this.busyTaskCount;
+ }
+
+ /** @prop testingOffline {boolean} Reflects the testing_offline attribute for
+ * ease of use. */
+ get testingOffline() {
+ return this.hasAttribute('testing_offline');
+ }
+
+ set testingOffline(val) {
+ if (val) {
+ this.setAttribute('testing_offline', '');
+ } else {
+ this.removeAttribute('testing_offline');
+ }
+ }
+
+ /**
+ * Indicate there are some number of tasks (e.g. RPCs) the app is waiting on
+ * and should be in the "busy" state, if it isn't already.
+ */
+ private addBusyTask() {
+ this.busyTaskCount++;
+ if (this.spinner && this.busyTaskCount > 0) {
+ this.spinner.active = true;
+ }
+ }
+
+ /**
+ * Removes one task from the busy count. If there are no more tasks to wait
+ * for, the app will leave the "busy" state and emit the "busy-end" event.
+ */
+ private finishedTask() {
+ this.busyTaskCount--;
+ if (this.busyTaskCount <= 0) {
+ this.busyTaskCount = 0;
+ if (this.spinner) {
+ this.spinner.active = false;
+ }
+ this.dispatchEvent(new CustomEvent('busy-end', { bubbles: true }));
+ }
+ }
+
+ /** Handles a fetch error. Expects the detail of error to contain:
+ * error: the error given to fetch.
+ * loading: A string explaining what was being fetched.
+ */
+ private fetchError(e: any) {
+ const loadingWhat = e.detail.loading;
+ e = e.detail.error;
+ if (e.name !== 'AbortError') {
+ // We can ignore AbortError since they fire anytime an AbortController was
+ // canceled. Chrome and Firefox report a DOMException in this case:
+ // https://developer.mozilla.org/en-US/docs/Web/API/DOMException
+ errorMessage(
+ `Unexpected error loading ${loadingWhat}: ${e.message}`,
+ 5000
+ );
+ }
+ this.finishedTask();
+ }
+}
+
+define('task-scheduler-scaffold-sk', TaskSchedulerScaffoldSk);
diff --git a/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk_puppeteer_test.ts b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk_puppeteer_test.ts
new file mode 100644
index 0000000..3ca9723
--- /dev/null
+++ b/task_scheduler/modules/task-scheduler-scaffold-sk/task-scheduler-scaffold-sk_puppeteer_test.ts
@@ -0,0 +1,28 @@
+import * as path from 'path';
+import {
+ setUpPuppeteerAndDemoPageServer,
+ takeScreenshot,
+} from '../../../puppeteer-tests/util';
+
+describe('task-scheduler-scaffold-sk', () => {
+ const testBed = setUpPuppeteerAndDemoPageServer(
+ path.join(__dirname, '..', '..', 'webpack.config.ts')
+ );
+
+ beforeEach(async () => {
+ await testBed.page.goto(
+ `${testBed.baseUrl}/dist/task-scheduler-scaffold-sk.html`
+ );
+ await testBed.page.setViewport({ width: 650, height: 400 });
+ });
+
+ describe('screenshots', () => {
+ it('shows the default view', async () => {
+ await takeScreenshot(
+ testBed.page,
+ 'task-scheduler',
+ 'task-scheduler-scaffold-sk'
+ );
+ });
+ });
+});