blob: 477e7d2c898177ff230fd6ed0999d7e811a32953 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {LitElement, html, css} from 'lit-element';
6
7// URL where announcements are fetched from.
8const ANNOUNCEMENT_SERVICE =
9 'https://chopsdash.appspot.com/prpc/dashboard.ChopsAnnouncements/SearchAnnouncements';
10
11// Prefix prepended to responses for security reasons.
12export const XSSI_PREFIX = ')]}\'';
13
14const FETCH_HEADERS = Object.freeze({
15 'accept': 'application/json',
16 'content-type': 'application/json',
17});
18
19// How often to refresh announcements.
20export const REFRESH_TIME_MS = 5 * 60 * 1000;
21
22/**
23 * @typedef {Object} Announcement
24 * @property {string} id
25 * @property {string} messageContent
26 */
27
28/**
29 * @typedef {Object} AnnouncementResponse
30 * @property {Array<Announcement>} announcements
31 */
32
33/**
34 * `<chops-announcement>` displays a ChopsDash message when there's an outage
35 * or other important announcement.
36 *
37 * @customElement chops-announcement
38 */
39export class ChopsAnnouncement extends LitElement {
40 /** @override */
41 static get styles() {
42 return css`
43 :host {
44 display: block;
45 width: 100%;
46 }
47 p {
48 display: block;
49 color: #222;
50 font-size: 13px;
51 background: #FFCDD2; /* Material design red */
52 width: 100%;
53 text-align: center;
54 padding: 0.5em 16px;
55 box-sizing: border-box;
56 margin: 0;
57 /* Using a red-tinted grey border makes hues feel harmonious. */
58 border-bottom: 1px solid #D6B3B6;
59 }
60 `;
61 }
62 /** @override */
63 render() {
64 if (this._error) {
65 return html`<p><strong>Error: </strong>${this._error}</p>`;
66 }
67 return html`
68 ${this._announcements.map(
69 ({messageContent}) => html`<p>${messageContent}</p>`)}
70 `;
71 }
72
73 /** @override */
74 static get properties() {
75 return {
76 service: {type: String},
77 _error: {type: String},
78 _announcements: {type: Array},
79 };
80 }
81
82 /** @override */
83 constructor() {
84 super();
85
86 /** @type {string} */
87 this.service = undefined;
88 /** @type {string} */
89 this._error = undefined;
90 /** @type {Array<Announcement>} */
91 this._announcements = [];
92
93 /** @type {number} Interval ID returned by window.setInterval. */
94 this._interval = undefined;
95 }
96
97 /** @override */
98 updated(changedProperties) {
99 if (changedProperties.has('service')) {
100 if (this.service) {
101 this.startRefresh();
102 } else {
103 this.stopRefresh();
104 }
105 }
106 }
107
108 /** @override */
109 disconnectedCallback() {
110 super.disconnectedCallback();
111
112 this.stopRefresh();
113 }
114
115 /**
116 * Set up autorefreshing logic or announcement information.
117 */
118 startRefresh() {
119 this.stopRefresh();
120 this.refresh();
121 this._interval = window.setInterval(() => this.refresh(), REFRESH_TIME_MS);
122 }
123
124 /**
125 * Logic for clearing refresh behavior.
126 */
127 stopRefresh() {
128 if (this._interval) {
129 window.clearInterval(this._interval);
130 }
131 }
132
133 /**
134 * Refresh the announcement banner.
135 */
136 async refresh() {
137 try {
138 const {announcements = []} = await this.fetch(this.service);
139 this._error = undefined;
140 this._announcements = announcements;
141 } catch (e) {
142 this._error = e.message;
143 this._announcements = [];
144 }
145 }
146
147 /**
148 * Fetches the announcement for a given service.
149 * @param {string} service Name of the service to fetch from ChopsDash.
150 * ie: "monorail"
151 * @return {Promise<AnnouncementResponse>} ChopsDash response JSON.
152 * @throws {Error} If something went wrong while fetching.
153 */
154 async fetch(service) {
155 const message = {
156 retired: false,
157 platformName: service,
158 };
159
160 const response = await window.fetch(ANNOUNCEMENT_SERVICE, {
161 method: 'POST',
162 headers: FETCH_HEADERS,
163 body: JSON.stringify(message),
164 });
165
166 if (!response.ok) {
167 throw new Error('Something went wrong while fetching announcements');
168 }
169
170 // We can't use response.json() because of the XSSI prefix.
171 const text = await response.text();
172
173 if (!text.startsWith(XSSI_PREFIX)) {
174 throw new Error(`No XSSI prefix in announce response: ${XSSI_PREFIX}`);
175 }
176
177 return JSON.parse(text.substr(XSSI_PREFIX.length));
178 }
179}
180
181customElements.define('chops-announcement', ChopsAnnouncement);