Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/chops/chops-announcement/chops-announcement.js b/static_src/elements/chops/chops-announcement/chops-announcement.js
new file mode 100644
index 0000000..477e7d2
--- /dev/null
+++ b/static_src/elements/chops/chops-announcement/chops-announcement.js
@@ -0,0 +1,181 @@
+// Copyright 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {LitElement, html, css} from 'lit-element';
+
+// URL where announcements are fetched from.
+const ANNOUNCEMENT_SERVICE =
+ 'https://chopsdash.appspot.com/prpc/dashboard.ChopsAnnouncements/SearchAnnouncements';
+
+// Prefix prepended to responses for security reasons.
+export const XSSI_PREFIX = ')]}\'';
+
+const FETCH_HEADERS = Object.freeze({
+ 'accept': 'application/json',
+ 'content-type': 'application/json',
+});
+
+// How often to refresh announcements.
+export const REFRESH_TIME_MS = 5 * 60 * 1000;
+
+/**
+ * @typedef {Object} Announcement
+ * @property {string} id
+ * @property {string} messageContent
+ */
+
+/**
+ * @typedef {Object} AnnouncementResponse
+ * @property {Array<Announcement>} announcements
+ */
+
+/**
+ * `<chops-announcement>` displays a ChopsDash message when there's an outage
+ * or other important announcement.
+ *
+ * @customElement chops-announcement
+ */
+export class ChopsAnnouncement extends LitElement {
+ /** @override */
+ static get styles() {
+ return css`
+ :host {
+ display: block;
+ width: 100%;
+ }
+ p {
+ display: block;
+ color: #222;
+ font-size: 13px;
+ background: #FFCDD2; /* Material design red */
+ width: 100%;
+ text-align: center;
+ padding: 0.5em 16px;
+ box-sizing: border-box;
+ margin: 0;
+ /* Using a red-tinted grey border makes hues feel harmonious. */
+ border-bottom: 1px solid #D6B3B6;
+ }
+ `;
+ }
+ /** @override */
+ render() {
+ if (this._error) {
+ return html`<p><strong>Error: </strong>${this._error}</p>`;
+ }
+ return html`
+ ${this._announcements.map(
+ ({messageContent}) => html`<p>${messageContent}</p>`)}
+ `;
+ }
+
+ /** @override */
+ static get properties() {
+ return {
+ service: {type: String},
+ _error: {type: String},
+ _announcements: {type: Array},
+ };
+ }
+
+ /** @override */
+ constructor() {
+ super();
+
+ /** @type {string} */
+ this.service = undefined;
+ /** @type {string} */
+ this._error = undefined;
+ /** @type {Array<Announcement>} */
+ this._announcements = [];
+
+ /** @type {number} Interval ID returned by window.setInterval. */
+ this._interval = undefined;
+ }
+
+ /** @override */
+ updated(changedProperties) {
+ if (changedProperties.has('service')) {
+ if (this.service) {
+ this.startRefresh();
+ } else {
+ this.stopRefresh();
+ }
+ }
+ }
+
+ /** @override */
+ disconnectedCallback() {
+ super.disconnectedCallback();
+
+ this.stopRefresh();
+ }
+
+ /**
+ * Set up autorefreshing logic or announcement information.
+ */
+ startRefresh() {
+ this.stopRefresh();
+ this.refresh();
+ this._interval = window.setInterval(() => this.refresh(), REFRESH_TIME_MS);
+ }
+
+ /**
+ * Logic for clearing refresh behavior.
+ */
+ stopRefresh() {
+ if (this._interval) {
+ window.clearInterval(this._interval);
+ }
+ }
+
+ /**
+ * Refresh the announcement banner.
+ */
+ async refresh() {
+ try {
+ const {announcements = []} = await this.fetch(this.service);
+ this._error = undefined;
+ this._announcements = announcements;
+ } catch (e) {
+ this._error = e.message;
+ this._announcements = [];
+ }
+ }
+
+ /**
+ * Fetches the announcement for a given service.
+ * @param {string} service Name of the service to fetch from ChopsDash.
+ * ie: "monorail"
+ * @return {Promise<AnnouncementResponse>} ChopsDash response JSON.
+ * @throws {Error} If something went wrong while fetching.
+ */
+ async fetch(service) {
+ const message = {
+ retired: false,
+ platformName: service,
+ };
+
+ const response = await window.fetch(ANNOUNCEMENT_SERVICE, {
+ method: 'POST',
+ headers: FETCH_HEADERS,
+ body: JSON.stringify(message),
+ });
+
+ if (!response.ok) {
+ throw new Error('Something went wrong while fetching announcements');
+ }
+
+ // We can't use response.json() because of the XSSI prefix.
+ const text = await response.text();
+
+ if (!text.startsWith(XSSI_PREFIX)) {
+ throw new Error(`No XSSI prefix in announce response: ${XSSI_PREFIX}`);
+ }
+
+ return JSON.parse(text.substr(XSSI_PREFIX.length));
+ }
+}
+
+customElements.define('chops-announcement', ChopsAnnouncement);