Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | // Copyright 2020 The Chromium Authors |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 5 | import { LitElement, html, css } from 'lit-element'; |
| 6 | import 'elements/framework/mr-comment-content/mr-comment-content.js'; |
| 7 | |
| 8 | import { connectStore } from 'reducers/base.js'; |
| 9 | import * as projectV0 from 'reducers/projectV0.js'; |
| 10 | import * as userV0 from 'reducers/userV0.js'; |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 11 | |
| 12 | // URL where announcements are fetched from. |
| 13 | const ANNOUNCEMENT_SERVICE = |
| 14 | 'https://chopsdash.appspot.com/prpc/dashboard.ChopsAnnouncements/SearchAnnouncements'; |
| 15 | |
| 16 | // Prefix prepended to responses for security reasons. |
| 17 | export const XSSI_PREFIX = ')]}\''; |
| 18 | |
| 19 | const FETCH_HEADERS = Object.freeze({ |
| 20 | 'accept': 'application/json', |
| 21 | 'content-type': 'application/json', |
| 22 | }); |
| 23 | |
| 24 | // How often to refresh announcements. |
| 25 | export const REFRESH_TIME_MS = 5 * 60 * 1000; |
| 26 | |
| 27 | /** |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 28 | * @type {Array<Announcement>} A list of hardcodded announcements for Monorail. |
| 29 | */ |
| 30 | export const HARDCODED_ANNOUNCEMENTS = [{ |
| 31 | "messageContent": "The Chromium project will be migrating to Buganizer on " + |
| 32 | " February 5 (go/chrome-buganizer). Please test your workflows for this " + |
| 33 | "transition with these instructions: go/cob-buv-quick-start", |
| 34 | "projects": ["chromium"], |
| 35 | "groups": ["everyone@google.com", "googlers@chromium.org"], |
| 36 | }]; |
| 37 | |
| 38 | /** |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 39 | * @typedef {Object} Announcement |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 40 | * @property {string=} id |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 41 | * @property {string} messageContent |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 42 | * @property {Array<string>=} projects Monorail extension for hard-coded |
| 43 | * announcements. Specifies the names of projects the announcement will |
| 44 | * occur in. |
| 45 | * @property {Array<string>=} groups Monorail extension for hard-coded |
| 46 | * announcements. Specifies email groups the announces will show up in. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 47 | */ |
| 48 | |
| 49 | /** |
| 50 | * @typedef {Object} AnnouncementResponse |
| 51 | * @property {Array<Announcement>} announcements |
| 52 | */ |
| 53 | |
| 54 | /** |
| 55 | * `<chops-announcement>` displays a ChopsDash message when there's an outage |
| 56 | * or other important announcement. |
| 57 | * |
| 58 | * @customElement chops-announcement |
| 59 | */ |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 60 | class _ChopsAnnouncement extends LitElement { |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 61 | /** @override */ |
| 62 | static get styles() { |
| 63 | return css` |
| 64 | :host { |
| 65 | display: block; |
| 66 | width: 100%; |
| 67 | } |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 68 | mr-comment-content { |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 69 | display: block; |
| 70 | color: #222; |
| 71 | font-size: 13px; |
| 72 | background: #FFCDD2; /* Material design red */ |
| 73 | width: 100%; |
| 74 | text-align: center; |
| 75 | padding: 0.5em 16px; |
| 76 | box-sizing: border-box; |
| 77 | margin: 0; |
| 78 | /* Using a red-tinted grey border makes hues feel harmonious. */ |
| 79 | border-bottom: 1px solid #D6B3B6; |
| 80 | } |
| 81 | `; |
| 82 | } |
| 83 | /** @override */ |
| 84 | render() { |
| 85 | if (this._error) { |
| 86 | return html`<p><strong>Error: </strong>${this._error}</p>`; |
| 87 | } |
| 88 | return html` |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 89 | ${this._processedAnnouncements().map( |
| 90 | ({ messageContent }) => html` |
| 91 | <mr-comment-content |
| 92 | .content=${messageContent}> |
| 93 | </mr-comment-content>`)} |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 94 | `; |
| 95 | } |
| 96 | |
| 97 | /** @override */ |
| 98 | static get properties() { |
| 99 | return { |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 100 | service: { type: String }, |
| 101 | additionalAnnouncements: { type: Array }, |
| 102 | |
| 103 | // Properties from the currently logged in user, usually feched through |
| 104 | // Redux. |
| 105 | currentUserName: { type: String }, |
| 106 | userGroups: { type: Array }, |
| 107 | currentProject: { type: String }, |
| 108 | |
| 109 | // Private properties managing state from requests to Chops Dash. |
| 110 | _error: { type: String }, |
| 111 | _announcements: { type: Array }, |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 112 | }; |
| 113 | } |
| 114 | |
| 115 | /** @override */ |
| 116 | constructor() { |
| 117 | super(); |
| 118 | |
| 119 | /** @type {string} */ |
| 120 | this.service = undefined; |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 121 | /** @type {Array<Announcement>} */ |
| 122 | this.additionalAnnouncements = HARDCODED_ANNOUNCEMENTS; |
| 123 | |
| 124 | this.currentUserName = ''; |
| 125 | this.userGroups = []; |
| 126 | this.currentProject = ''; |
| 127 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 128 | /** @type {string} */ |
| 129 | this._error = undefined; |
| 130 | /** @type {Array<Announcement>} */ |
| 131 | this._announcements = []; |
| 132 | |
| 133 | /** @type {number} Interval ID returned by window.setInterval. */ |
| 134 | this._interval = undefined; |
| 135 | } |
| 136 | |
| 137 | /** @override */ |
| 138 | updated(changedProperties) { |
| 139 | if (changedProperties.has('service')) { |
| 140 | if (this.service) { |
| 141 | this.startRefresh(); |
| 142 | } else { |
| 143 | this.stopRefresh(); |
| 144 | } |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | /** @override */ |
| 149 | disconnectedCallback() { |
| 150 | super.disconnectedCallback(); |
| 151 | |
| 152 | this.stopRefresh(); |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Set up autorefreshing logic or announcement information. |
| 157 | */ |
| 158 | startRefresh() { |
| 159 | this.stopRefresh(); |
| 160 | this.refresh(); |
| 161 | this._interval = window.setInterval(() => this.refresh(), REFRESH_TIME_MS); |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Logic for clearing refresh behavior. |
| 166 | */ |
| 167 | stopRefresh() { |
| 168 | if (this._interval) { |
| 169 | window.clearInterval(this._interval); |
| 170 | } |
| 171 | } |
| 172 | |
| 173 | /** |
| 174 | * Refresh the announcement banner. |
| 175 | */ |
| 176 | async refresh() { |
| 177 | try { |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 178 | const { announcements = [] } = await this.fetch(this.service); |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 179 | this._error = undefined; |
| 180 | this._announcements = announcements; |
| 181 | } catch (e) { |
| 182 | this._error = e.message; |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 183 | this._announcements = HARDCODED_ANNOUNCEMENTS; |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 184 | } |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Fetches the announcement for a given service. |
| 189 | * @param {string} service Name of the service to fetch from ChopsDash. |
| 190 | * ie: "monorail" |
| 191 | * @return {Promise<AnnouncementResponse>} ChopsDash response JSON. |
| 192 | * @throws {Error} If something went wrong while fetching. |
| 193 | */ |
| 194 | async fetch(service) { |
| 195 | const message = { |
| 196 | retired: false, |
| 197 | platformName: service, |
| 198 | }; |
| 199 | |
| 200 | const response = await window.fetch(ANNOUNCEMENT_SERVICE, { |
| 201 | method: 'POST', |
| 202 | headers: FETCH_HEADERS, |
| 203 | body: JSON.stringify(message), |
| 204 | }); |
| 205 | |
| 206 | if (!response.ok) { |
| 207 | throw new Error('Something went wrong while fetching announcements'); |
| 208 | } |
| 209 | |
| 210 | // We can't use response.json() because of the XSSI prefix. |
| 211 | const text = await response.text(); |
| 212 | |
| 213 | if (!text.startsWith(XSSI_PREFIX)) { |
| 214 | throw new Error(`No XSSI prefix in announce response: ${XSSI_PREFIX}`); |
| 215 | } |
| 216 | |
| 217 | return JSON.parse(text.substr(XSSI_PREFIX.length)); |
| 218 | } |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 219 | |
| 220 | _processedAnnouncements() { |
| 221 | const announcements = [...this.additionalAnnouncements, ...this._announcements]; |
| 222 | |
| 223 | // Only show announcements relevant to the project the user is viewing and |
| 224 | // the group the user is part of, if applicable. |
| 225 | return announcements.filter(({ groups, projects }) => { |
| 226 | if (groups && groups.length && !this._isUserInGroups(groups, |
| 227 | this.userGroups, this.currentUserName)) { |
| 228 | return false; |
| 229 | } |
| 230 | if (projects && projects.length && !this._isViewingProject(projects, |
| 231 | this.currentProject)) { |
| 232 | return false; |
| 233 | } |
| 234 | return true; |
| 235 | }); |
| 236 | } |
| 237 | |
| 238 | /** |
| 239 | * Helper to check if the user is a member of the allowed groups. |
| 240 | * @param {Array<string>} allowedGroups |
| 241 | * @param {Array<{{userId: string, displayName: string}}>} userGroups |
| 242 | * @param {string} userEmail |
| 243 | */ |
| 244 | _isUserInGroups(allowedGroups, userGroups, userEmail) { |
| 245 | const userGroupSet = new Set(userGroups.map( |
| 246 | ({ displayName }) => displayName.toLowerCase())); |
| 247 | return allowedGroups.find((group) => { |
| 248 | group = group.toLowerCase(); |
| 249 | |
| 250 | // Handle custom groups in Monorail like everyone@google.com |
| 251 | if (group.startsWith('everyone@')) { |
| 252 | let [_, suffix] = group.split('@'); |
| 253 | suffix = '@' + suffix; |
| 254 | return userEmail.endsWith(suffix); |
| 255 | } |
| 256 | |
| 257 | return userGroupSet.has(group); |
| 258 | }); |
| 259 | } |
| 260 | |
| 261 | _isViewingProject(projects, currentProject) { |
| 262 | return projects.find((project = "") => project.toLowerCase() === currentProject.toLowerCase()); |
| 263 | } |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 264 | } |
| 265 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 266 | /** Redux-connected version of _ChopsAnnouncement. */ |
| 267 | export class ChopsAnnouncement extends connectStore(_ChopsAnnouncement) { |
| 268 | /** @override */ |
| 269 | stateChanged(state) { |
| 270 | const { displayName, groups } = userV0.currentUser(state); |
| 271 | this.currentUserName = displayName; |
| 272 | this.userGroups = groups; |
| 273 | |
| 274 | this.currentProject = projectV0.viewedProjectName(state); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | customElements.define('chops-announcement-base', _ChopsAnnouncement); |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 279 | customElements.define('chops-announcement', ChopsAnnouncement); |