Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/static_src/elements/chops/chops-announcement/chops-announcement.js b/static_src/elements/chops/chops-announcement/chops-announcement.js
index 477e7d2..5d5a9d6 100644
--- a/static_src/elements/chops/chops-announcement/chops-announcement.js
+++ b/static_src/elements/chops/chops-announcement/chops-announcement.js
@@ -1,8 +1,13 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
+// Copyright 2020 The Chromium Authors
// 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';
+import { LitElement, html, css } from 'lit-element';
+import 'elements/framework/mr-comment-content/mr-comment-content.js';
+
+import { connectStore } from 'reducers/base.js';
+import * as projectV0 from 'reducers/projectV0.js';
+import * as userV0 from 'reducers/userV0.js';
// URL where announcements are fetched from.
const ANNOUNCEMENT_SERVICE =
@@ -20,9 +25,25 @@
export const REFRESH_TIME_MS = 5 * 60 * 1000;
/**
+ * @type {Array<Announcement>} A list of hardcodded announcements for Monorail.
+ */
+export const HARDCODED_ANNOUNCEMENTS = [{
+ "messageContent": "The Chromium project will be migrating to Buganizer on " +
+ " February 5 (go/chrome-buganizer). Please test your workflows for this " +
+ "transition with these instructions: go/cob-buv-quick-start",
+ "projects": ["chromium"],
+ "groups": ["everyone@google.com", "googlers@chromium.org"],
+}];
+
+/**
* @typedef {Object} Announcement
- * @property {string} id
+ * @property {string=} id
* @property {string} messageContent
+ * @property {Array<string>=} projects Monorail extension for hard-coded
+ * announcements. Specifies the names of projects the announcement will
+ * occur in.
+ * @property {Array<string>=} groups Monorail extension for hard-coded
+ * announcements. Specifies email groups the announces will show up in.
*/
/**
@@ -36,7 +57,7 @@
*
* @customElement chops-announcement
*/
-export class ChopsAnnouncement extends LitElement {
+class _ChopsAnnouncement extends LitElement {
/** @override */
static get styles() {
return css`
@@ -44,7 +65,7 @@
display: block;
width: 100%;
}
- p {
+ mr-comment-content {
display: block;
color: #222;
font-size: 13px;
@@ -65,17 +86,29 @@
return html`<p><strong>Error: </strong>${this._error}</p>`;
}
return html`
- ${this._announcements.map(
- ({messageContent}) => html`<p>${messageContent}</p>`)}
+ ${this._processedAnnouncements().map(
+ ({ messageContent }) => html`
+ <mr-comment-content
+ .content=${messageContent}>
+ </mr-comment-content>`)}
`;
}
/** @override */
static get properties() {
return {
- service: {type: String},
- _error: {type: String},
- _announcements: {type: Array},
+ service: { type: String },
+ additionalAnnouncements: { type: Array },
+
+ // Properties from the currently logged in user, usually feched through
+ // Redux.
+ currentUserName: { type: String },
+ userGroups: { type: Array },
+ currentProject: { type: String },
+
+ // Private properties managing state from requests to Chops Dash.
+ _error: { type: String },
+ _announcements: { type: Array },
};
}
@@ -85,6 +118,13 @@
/** @type {string} */
this.service = undefined;
+ /** @type {Array<Announcement>} */
+ this.additionalAnnouncements = HARDCODED_ANNOUNCEMENTS;
+
+ this.currentUserName = '';
+ this.userGroups = [];
+ this.currentProject = '';
+
/** @type {string} */
this._error = undefined;
/** @type {Array<Announcement>} */
@@ -135,12 +175,12 @@
*/
async refresh() {
try {
- const {announcements = []} = await this.fetch(this.service);
+ const { announcements = [] } = await this.fetch(this.service);
this._error = undefined;
this._announcements = announcements;
} catch (e) {
this._error = e.message;
- this._announcements = [];
+ this._announcements = HARDCODED_ANNOUNCEMENTS;
}
}
@@ -176,6 +216,64 @@
return JSON.parse(text.substr(XSSI_PREFIX.length));
}
+
+ _processedAnnouncements() {
+ const announcements = [...this.additionalAnnouncements, ...this._announcements];
+
+ // Only show announcements relevant to the project the user is viewing and
+ // the group the user is part of, if applicable.
+ return announcements.filter(({ groups, projects }) => {
+ if (groups && groups.length && !this._isUserInGroups(groups,
+ this.userGroups, this.currentUserName)) {
+ return false;
+ }
+ if (projects && projects.length && !this._isViewingProject(projects,
+ this.currentProject)) {
+ return false;
+ }
+ return true;
+ });
+ }
+
+ /**
+ * Helper to check if the user is a member of the allowed groups.
+ * @param {Array<string>} allowedGroups
+ * @param {Array<{{userId: string, displayName: string}}>} userGroups
+ * @param {string} userEmail
+ */
+ _isUserInGroups(allowedGroups, userGroups, userEmail) {
+ const userGroupSet = new Set(userGroups.map(
+ ({ displayName }) => displayName.toLowerCase()));
+ return allowedGroups.find((group) => {
+ group = group.toLowerCase();
+
+ // Handle custom groups in Monorail like everyone@google.com
+ if (group.startsWith('everyone@')) {
+ let [_, suffix] = group.split('@');
+ suffix = '@' + suffix;
+ return userEmail.endsWith(suffix);
+ }
+
+ return userGroupSet.has(group);
+ });
+ }
+
+ _isViewingProject(projects, currentProject) {
+ return projects.find((project = "") => project.toLowerCase() === currentProject.toLowerCase());
+ }
}
+/** Redux-connected version of _ChopsAnnouncement. */
+export class ChopsAnnouncement extends connectStore(_ChopsAnnouncement) {
+ /** @override */
+ stateChanged(state) {
+ const { displayName, groups } = userV0.currentUser(state);
+ this.currentUserName = displayName;
+ this.userGroups = groups;
+
+ this.currentProject = projectV0.viewedProjectName(state);
+ }
+}
+
+customElements.define('chops-announcement-base', _ChopsAnnouncement);
customElements.define('chops-announcement', ChopsAnnouncement);
diff --git a/static_src/elements/chops/chops-announcement/chops-announcement.test.js b/static_src/elements/chops/chops-announcement/chops-announcement.test.js
index fa9643f..bed36f5 100644
--- a/static_src/elements/chops/chops-announcement/chops-announcement.test.js
+++ b/static_src/elements/chops/chops-announcement/chops-announcement.test.js
@@ -1,4 +1,4 @@
-// Copyright 2020 The Chromium Authors. All rights reserved.
+// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chai';
@@ -9,11 +9,22 @@
let element;
let clock;
+function assertRendersMessage(message) {
+ const messageContainer = element.shadowRoot.querySelector('mr-comment-content');
+ assert.include(messageContainer.content, message);
+}
+
+function assertDoesNotRender() {
+ assert.equal(0, element.shadowRoot.children.length);
+}
+
describe('chops-announcement', () => {
beforeEach(() => {
- element = document.createElement('chops-announcement');
+ element = document.createElement('chops-announcement-base');
document.body.appendChild(element);
+ element.additionalAnnouncements = [];
+
clock = sinon.useFakeTimers({
now: new Date(0),
shouldAdvanceTime: false,
@@ -32,10 +43,6 @@
window.fetch.restore();
});
- it('initializes', () => {
- assert.instanceOf(element, ChopsAnnouncement);
- });
-
it('does not request announcements when no service specified', async () => {
sinon.stub(element, 'fetch');
@@ -139,7 +146,8 @@
assert.deepEqual(element._announcements,
[{id: '1234', messageContent: 'test thing'}]);
- assert.include(element.shadowRoot.textContent, 'test thing');
+
+ assertRendersMessage('test thing');
});
it('renders empty on empty announcement', async () => {
@@ -154,7 +162,7 @@
await element.updateComplete;
assert.deepEqual(element._announcements, []);
- assert.equal(0, element.shadowRoot.children.length);
+ assertDoesNotRender()
});
it('fetch returns response data', async () => {
@@ -191,4 +199,85 @@
'Something went wrong while fetching announcements');
}
});
+
+ describe('additional announcement handlings', () => {
+ beforeEach(() => {
+ sinon.stub(element, 'fetch');
+ element.fetch.returns({});
+ element.service = 'monorail';
+ });
+
+ it('renders additional announcement', async () => {
+ element.additionalAnnouncements = [{'messageContent': 'test thing'}];
+ await element.updateComplete;
+
+ assertRendersMessage('test thing');
+ });
+
+ it('renders when user is in group', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'groups': ['hello@group.com']}
+ ];
+ element.userGroups = [
+ {"userId": "12344", "displayName": "hello@group.com"}];
+ await element.updateComplete;
+
+ assertRendersMessage('test thing');
+ });
+
+ it('does not render when user is not in group', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'groups': ['hello@group.com']}
+ ];
+ element.userGroups = [
+ {"userId": "12344", "displayName": "hello@othergroup.com"}];
+ await element.updateComplete;
+
+ assertDoesNotRender();
+ });
+
+ it('renders when user is in everyone@ group', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'groups': ['everyone@world.com']}
+ ];
+ element.userGroups = [
+ {"userId": "12344", "displayName": "hello@group.com"}];
+ element.currentUserName = "hello@world.com";
+ await element.updateComplete;
+
+ assertRendersMessage('test thing');
+ });
+
+ it('does not renders when user is not in everyone@ group', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'groups': ['everyone@word.com']}
+ ];
+ element.userGroups = [
+ {"userId": "12344", "displayName": "hello@world.com"}];
+ element.currentUserName = "hello@world.com";
+ await element.updateComplete;
+
+ assertDoesNotRender();
+ });
+
+ it('renders when viewing referenced project', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'projects': ['chromium']}];
+ element.currentProject = 'chromium';
+
+ await element.updateComplete;
+
+ assertRendersMessage('test thing');
+ });
+
+ it('does not render when not viewing referenced project', async () => {
+ element.additionalAnnouncements = [
+ {'messageContent': 'test thing', 'projects': ['chromium']}];
+ element.currentProject = 'chrome';
+
+ await element.updateComplete;
+
+ assertDoesNotRender();
+ });
+ });
});