Merge branch 'main' into avm99963-monorail

GitOrigin-RevId: 2bbe35caf837ba29cc5c1a89d66a6881c1230034
diff --git a/README.md b/README.md
index d039d49..bf98967 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@
     1.  Make sure to authenticate with the App Engine SDK and configure Docker. This is needed to install Cloud Tasks Emulator.
         1.  `gcloud auth login`
         1.  `gcloud auth configure-docker`
+            1. If you get authentication errors here, run `setenv PATH = $PATH;/path/to/gcloud/bin`
     1. Run `docker-compose -f dev-services.yml up -d`. This should spin up:
         1. MySQL v5.6
         1. Redis
@@ -57,10 +58,17 @@
         1.  Install build requirements:
             1.  `sudo apt-get install build-essential automake`
     1. On MacOS
+        1. [Install homebrew](https://brew.sh)
         1.  Install node and npm
             1.  Install node version manager `brew install nvm`
             1.  See the brew instructions on updating your shell's configuration
             1.  Install node and npm `nvm install 12.13.0`
+            1.  Add the following to the end of your `~/.zshrc` file: 
+
+                    export NVM_DIR="$HOME/.nvm"
+                    [ -s "/usr/local/opt/nvm/nvm.sh" ] && . "/usr/local/opt/nvm/nvm.sh"  # This loads nvm
+                    [ -s "/usr/local/opt/nvm/etc/bash_completion.d/nvm" ] && . "/usr/local/opt/nvm/etc/bash_completion.d/nvm"  # This loads nvm bash_completion
+
 1.  Install Python and JS dependencies:
     1.  Install MySQL, needed for mysqlclient
         1. For mac: `brew install mysql@5.6`
@@ -68,7 +76,7 @@
     1.  Optional: You may need to install `pip`. You can verify whether you have it installed with `which pip`.
         1. `curl -O https://bootstrap.pypa.io/2.7/get-pip.py`
         1. `sudo python get-pip.py`
-    1.  Optional: Use `virtualenv` to keep from modifying system dependencies.
+    1.  Use `virtualenv` to keep from modifying system dependencies.
         1. `sudo pip install virtualenv`
         1. `virtualenv venv` to set up virtualenv within your monorail directory.
         1. `source venv/bin/activate` to activate it, needed in each terminal instance of the directory.
@@ -79,13 +87,26 @@
 1.  Run the app:
     1.  `make serve`
 1.  Browse the app at localhost:8080 your browser.
-1.  Optional: Create/modify your Monorail User row in the database and make that user a site admin. You will need to be a site admin to gain access to create projects through the UI.
+1. Set up your test user account (these steps are a little odd, but just roll with it):
+       1.  Sign in using `test@example.com`
+       1.  Log back out and log in again as `example@example.com`
+       1.  Log out and finally log in again as `test@example.com`.
+       1.  Everything should work fine now.
+1.  Modify your Monorail User row in the database and make that user a site admin. You will need to be a site admin to gain access to create projects through the UI.
     1.  `docker exec mysql mysql --user=root monorail -e "UPDATE User SET is_site_admin = TRUE WHERE email = 'test@example.com';"`
-    1.  If the admin change isn't immediately apparent, you may need to restart your local dev appserver.
+    1.  If the admin change isn't immediately apparent, you may need to restart your local dev appserver. If you kill the dev server before running the docker command, the restart may not be necessary.
 
 Instructions for deploying Monorail to an existing instance or setting up a new instance are [here](doc/deployment.md).
 
-Here's how to run unit tests from the command-line:
+
+## Feature Launch Tracking
+
+To set up FLT/Approvals in Monorail:
+1. Visit the gear > Development Process > Labels and fields
+1. Add at least one custom field with type "Approval" (this will be your approval
+1. Visit gear > Development Process > Templates
+1. Check "Include Gates and Approval Tasks in issue" 
+1. Fill out the chart - The top row is the gates/phases on your FLT issue and you can select radio buttons for which gate each approval goes
 
 ## Testing
 
@@ -146,6 +167,14 @@
 this is a leftover Monorail devserver process from a past run. To quit whatever process is
 on port 8080, you can run `kill $(lsof -ti:8080)`.
 
+*   `RuntimeError: maximum recursion depth exceeded while calling a Python object`
+
+If running `make serve` gives an output similar to [this](https://paste.googleplex.com/4693398234595328), make sure you're using a virtual environment (see above for how to configure one). Then, make the changes outlined in [this CL](https://chromium-review.googlesource.com/c/infra/infra/+/3152656). 
+
+*   `gcloud: command not found`
+
+Add the following to your `~/.zshrc` file: `alias gcloud='/Users/username/google-cloud-sdk/bin/gcloud'`. Replace `username` with your Google username.
+
 *   `TypeError: connect() got an unexpected keyword argument 'charset'`
 
 This error occurs when `dev_appserver` cannot find the MySQLdb library.  Try installing it via <code>sudo apt-get install python-mysqldb</code>.
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 10a66b3..8d3a527 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -11,6 +11,12 @@
 
 mysqlclient==1.4.6 --hash=sha256:f3fdaa9a38752a3b214a6fe79d7cae3653731a53e577821f9187e67cbecb2e16
 
+# Required by dev_appserver.py
+enum34==1.1.6 --hash=sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79
+
+# Required by gae.py and by dev_appserver.py
+six==1.15.0 --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
+
 # Required by grpc-google-iam-v1 <-- google-cloud-tasks
 # first sha is for cp27m-macosx_10_9_x86
 # second sha is for cp27mu-manylinux2010_x86
@@ -21,5 +27,7 @@
                --hash=sha256:5043440c45c0a031f387e7f48527541c65d672005fb24cf18ef6857483557d39
 
 # Required by google-cloud-tasks
-protobuf==3.12.4 --hash=sha256:3d59825cba9447e8f4fcacc1f3c892cafd28b964e152629b3f420a2fb5918b5a \
-                 --hash=sha256:6009f3ebe761fad319b52199a49f1efa7a3729302947a78a3f5ea8e7e89e3ac2
+# first sha is for cp27m-macosx_10_9_x86_64
+# second sha is for cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64
+protobuf==3.17.3 --hash=sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8 \
+                 --hash=sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637
diff --git a/requirements.py2.txt b/requirements.py2.txt
index d09804e..ec1273c 100644
--- a/requirements.py2.txt
+++ b/requirements.py2.txt
@@ -4,7 +4,7 @@
 
 # Production packages.
 ezt==1.1 --hash=sha256:2131c2aa34d395433410b4e3cb71b22ab1471fae9da1c60e4426f74c86cb0104
-google-auth==1.20.1 --hash=sha256:ce1fb80b5c6d3dd038babcc43e221edeafefc72d983b3dc28b67b996f76f00b9
+google-auth==1.35.0 --hash=sha256:997516b42ecb5b63e8d80f5632c1a61dddf41d2a4c2748057837e06e00014258
 google-cloud-tasks==1.5.0 --hash=sha256:36aa16f0c52aa9a292b1f919d2582725731e9760393c9ca98ce599c68cbf9996
 redis==3.5.3 --hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24
 
@@ -12,26 +12,26 @@
 fakeredis==1.1.1 --hash=sha256:b8cf9c19fbcd53fe0512ece75b2df9430c46f75898111f50cff309c3a35b921d
 
 # Required by fakeredis
-sortedcontainers==2.3.0 --hash=sha256:37257a32add0a3ee490bb170b599e93095eed89a55da91fa9f48753ea12fd73f
+sortedcontainers==2.4.0 --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
 
 # Required by google-cloud-tasks
 enum34==1.1.10 --hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53
 googleapis-common-protos==1.52.0 --hash=sha256:c8961760f5aad9a711d37b675be103e0cc4e9a39327e0d6d857872f698403e24
-google-api-core==1.22.0 --hash=sha256:c4e3b3d914e09d181287abb7101b42f308204fa5e8f89efc4839f607303caa2f
+google-api-core==1.31.2 --hash=sha256:384459a0dc98c1c8cd90b28dc5800b8705e0275a673a7144a513ae80fc77950b
 grpc-google-iam-v1==0.12.3 --hash=sha256:0bfb5b56f648f457021a91c0df0db4934b6e0c300bd0f2de2333383fe958aa72
 
 # Required by google-api-core
 futures==3.3.0 --hash=sha256:49b3f5b064b6e3afc3316421a3f25f66c137ae88f068abbf72830170033c5e16
-pytz==2020.1 --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed
-requests==2.24.0 --hash=sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898
+pytz==2021.1 --hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798
+requests==2.26.0 --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24
 setuptools==44.1.1 --hash=sha256:27a714c09253134e60a6fa68130f78c7037e5562c4f21f8f318f2ae900d152d5
-six==1.15.0 --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
+six==1.16.0 --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
 
 # Required by requests
-certifi==2020.6.20 --hash=sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41
-chardet==3.0.4 --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
+certifi==2021.5.30 --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8
+chardet==4.0.0 --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5
 idna==2.10 --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0
-urllib3==1.25.10 --hash=sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461
+urllib3==1.26.6 --hash=sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4
 
 # Required by google-auth
 cachetools==3.1.1 --hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae
diff --git a/static_src/elements/framework/mr-dropdown/mr-account-dropdown.js b/static_src/elements/framework/mr-dropdown/mr-account-dropdown.js
index 264b976..15c3e7c 100644
--- a/static_src/elements/framework/mr-dropdown/mr-account-dropdown.js
+++ b/static_src/elements/framework/mr-dropdown/mr-account-dropdown.js
@@ -50,7 +50,7 @@
       {text: 'Switch accounts', url: this.loginUrl},
       {separator: true},
       {text: 'Profile', url: `/u/${this.userDisplayName}`},
-      {text: 'Updates', url: `/u/${this.userDisplayName}/updates`},
+      {text: 'History', url: `/u/${this.userDisplayName}/updates`},
       {text: 'Settings', url: '/hosting/settings'},
       {text: 'Saved queries', url: `/u/${this.userDisplayName}/queries`},
       {text: 'Hotlists', url: `/u/${this.userDisplayName}/hotlists`},
diff --git a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.js b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.js
index 804c8d1..2bc79a3 100644
--- a/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.js
+++ b/static_src/elements/issue-detail/metadata/mr-edit-metadata/mr-edit-metadata.js
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import {LitElement, html} from 'lit-element';
+import deepEqual from 'deep-equal';
 
 import 'elements/chops/chops-button/chops-button.js';
 import 'elements/framework/mr-upload/mr-upload.js';
@@ -599,20 +600,20 @@
       approvers: {type: Array},
       setter: {type: Object},
       summary: {type: String},
-      cc: {type: Array},
-      components: {type: Array},
+      cc: {type: Array, hasChanged: _notDeepEqual},
+      components: {type: Array, hasChanged: _notDeepEqual},
       status: {type: String},
       statuses: {type: Array},
-      blockedOn: {type: Array},
-      blocking: {type: Array},
+      blockedOn: {type: Array, hasChanged: _notDeepEqual},
+      blocking: {type: Array, hasChanged: _notDeepEqual},
       mergedInto: {type: Object},
-      ownerName: {type: String},
-      labelNames: {type: Array},
+      ownerName: {type: String, hasChanged: _notDeepEqual},
+      labelNames: {type: Array, hasChanged: _notDeepEqual},
       derivedLabels: {type: Array},
       _permissions: {type: Array},
       phaseName: {type: String},
       projectConfig: {type: Object},
-      projectName: {type: String},
+      projectName: {type: String, hasChanged: _notDeepEqual},
       isApproval: {type: Boolean},
       isStarred: {type: Boolean},
       issuePermissions: {type: Object},
@@ -1185,4 +1186,8 @@
   }
 }
 
+function _notDeepEqual(a, b) {
+  return !deepEqual(a, b);
+}
+
 customElements.define('mr-edit-metadata', MrEditMetadata);
diff --git a/static_src/react/issue-wizard/LandingStep.tsx b/static_src/react/issue-wizard/LandingStep.tsx
index efe6491..2925e87 100644
--- a/static_src/react/issue-wizard/LandingStep.tsx
+++ b/static_src/react/issue-wizard/LandingStep.tsx
@@ -1,10 +1,14 @@
+// Copyright 2021 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 React from 'react';
-import {makeStyles, withStyles} from '@material-ui/styles';
-import {blue, yellow, red, grey} from '@material-ui/core/colors';
+import { makeStyles, withStyles } from '@material-ui/styles';
+import { blue, yellow, red, grey } from '@material-ui/core/colors';
 import FormControlLabel from '@material-ui/core/FormControlLabel';
-import Checkbox, {CheckboxProps} from '@material-ui/core/Checkbox';
+import Checkbox, { CheckboxProps } from '@material-ui/core/Checkbox';
 import SelectMenu from './SelectMenu.tsx';
-import RadioDescription from './RadioDescription.tsx';
+import { RadioDescription } from './RadioDescription/RadioDescription.tsx';
 
 const CustomCheckbox = withStyles({
   root: {
@@ -42,7 +46,7 @@
     fontSize: '16px',
     fontWeight: '500',
   },
-  star:{
+  star: {
     color: red[700],
     marginRight: '8px',
     fontSize: '16px',
@@ -63,8 +67,8 @@
   },
 });
 
-export default function LandingStep({checkExisting, setCheckExisting, userType, setUserType, category, setCategory}:
-  {checkExisting: boolean, setCheckExisting: Function, userType: string, setUserType: Function, category: string, setCategory: Function}) {
+export default function LandingStep({ checkExisting, setCheckExisting, userType, setUserType, category, setCategory }:
+  { checkExisting: boolean, setCheckExisting: Function, userType: string, setUserType: Function, category: string, setCategory: Function }) {
   const classes = useStyles();
 
   const handleCheckChange = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -82,11 +86,11 @@
       <p className={classes.subheader}>
         Please select your following role: <span className={classes.red}>*</span>
       </p>
-      <RadioDescription value={userType} setValue={setUserType}/>
+      <RadioDescription value={userType} setValue={setUserType} />
       <div className={classes.subheader}>
         Which of the following best describes the issue that you are reporting? <span className={classes.red}>*</span>
       </div>
-      <SelectMenu option={category} setOption={setCategory}/>
+      <SelectMenu option={category} setOption={setCategory} />
       <div className={classes.warningBox}>
         <p className={classes.warningHeader}> Avoid duplicate issue reports:</p>
         <div>
diff --git a/static_src/react/issue-wizard/RadioDescription.test.tsx b/static_src/react/issue-wizard/RadioDescription.test.tsx
deleted file mode 100644
index ff65eae..0000000
--- a/static_src/react/issue-wizard/RadioDescription.test.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright 2021 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 React from 'react';
-import {render, screen, cleanup} from '@testing-library/react';
-import userEvent from '@testing-library/user-event'
-import {assert} from 'chai';
-import sinon from 'sinon';
-
-import RadioDescription from './RadioDescription.tsx';
-
-describe('RadioDescription', () => {
-  afterEach(cleanup);
-
-  it('renders', () => {
-    render(<RadioDescription />);
-    // look for blue radios
-      const radioOne = screen.getByRole('radio', {name: /Web Developer/i});
-      assert.isNotNull(radioOne)
-
-      const radioTwo = screen.getByRole('radio', {name: /End User/i});
-      assert.isNotNull(radioTwo)
-
-      const radioThree = screen.getByRole('radio', {name: /Chromium Contributor/i});
-      assert.isNotNull(radioThree)
-  });
-
-  it('checks selected radio value', () => {
-    // We're passing in the "Web Developer" value here manually
-    // to tell our code that that radio button is selected.
-    render(<RadioDescription value={'Web Developer'} />);
-
-    const checkedRadio = screen.getByRole('radio', {name: /Web Developer/i});
-    assert.isTrue(checkedRadio.checked);
-
-    // Extra check to make sure we haven't checked every single radio button.
-    const uncheckedRadio = screen.getByRole('radio', {name: /End User/i});
-    assert.isFalse(uncheckedRadio.checked);
-  });
-
-  it('sets radio value when clicked', () => {
-    // Using the sinon.js testing library to create a function for testing.
-    const setValue = sinon.stub();
-
-    render(<RadioDescription setValue={setValue} />);
-
-    const radio = screen.getByRole('radio', {name: /Web Developer/i});
-    userEvent.click(radio);
-
-    // Asserts that "Web Developer" was passed into our "setValue" function.
-    sinon.assert.calledWith(setValue, 'Web Developer');
-  });
-});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription.tsx b/static_src/react/issue-wizard/RadioDescription.tsx
deleted file mode 100644
index ad78c78..0000000
--- a/static_src/react/issue-wizard/RadioDescription.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2021 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 React from 'react';
-import {makeStyles, withStyles} from '@material-ui/styles';
-import {blue, grey} from '@material-ui/core/colors';
-import Radio, {RadioProps} from '@material-ui/core/Radio';
-
-const userGroups = Object.freeze({
-  END_USER: 'End User',
-  WEB_DEVELOPER: 'Web Developer',
-  CONTRIBUTOR: 'Chromium Contributor',
-});
-
-const BlueRadio = withStyles({
-  root: {
-    color: blue[400],
-    '&$checked': {
-      color: blue[600],
-    },
-  },
-  checked: {},
-})((props: RadioProps) => <Radio color="default" {...props} />);
-
-const useStyles = makeStyles({
-  flex: {
-    display: 'flex',
-    justifyContent: 'space-between',
-  },
-  container: {
-    width: '320px',
-    height: '150px',
-    position: 'relative',
-    display: 'inline-block',
-  },
-  text: {
-    position: 'absolute',
-    display: 'inline-block',
-    left: '55px',
-  },
-  title: {
-    marginTop: '7px',
-    fontSize: '20px',
-    color: grey[900],
-  },
-  subheader: {
-    fontSize: '16px',
-    color: grey[800],
-  },
-  line: {
-    position: 'absolute',
-    bottom: 0,
-    width: '300px',
-    left: '20px',
-  }
-});
-
-/**
- * `<RadioDescription />`
- *
- * React component for radio buttons and their descriptions
- * on the landing step of the Issue Wizard.
- *
- *  @return ReactElement.
- */
-export default function RadioDescription({value, setValue} : {value: string, setValue: Function}): React.ReactElement {
-  const classes = useStyles();
-
-  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    setValue(event.target.value);
-  };
-
-  return (
-    <div className={classes.flex}>
-      <div className={classes.container}>
-        <BlueRadio
-          checked={value === userGroups.END_USER}
-          onChange={handleChange}
-          value={userGroups.END_USER}
-          inputProps={{ 'aria-label': userGroups.END_USER}}
-        />
-        <div className={classes.text}>
-          <p className={classes.title}>{userGroups.END_USER}</p>
-          <p className={classes.subheader}>I am a user trying to do something on a website.</p>
-        </div>
-        <hr color={grey[200]} className={classes.line}/>
-      </div>
-      <div className={classes.container}>
-        <BlueRadio
-          checked={value === userGroups.WEB_DEVELOPER}
-          onChange={handleChange}
-          value={userGroups.WEB_DEVELOPER}
-          inputProps={{ 'aria-label': userGroups.WEB_DEVELOPER }}
-        />
-        <div className={classes.text}>
-          <p className={classes.title}>{userGroups.WEB_DEVELOPER}</p>
-          <p className={classes.subheader}>I am a web developer trying to build something.</p>
-        </div>
-        <hr color={grey[200]} className={classes.line}/>
-      </div>
-      <div className={classes.container}>
-        <BlueRadio
-          checked={value === userGroups.CONTRIBUTOR}
-          onChange={handleChange}
-          value={userGroups.CONTRIBUTOR}
-          inputProps={{ 'aria-label': userGroups.CONTRIBUTOR }}
-        />
-        <div className={classes.text}>
-          <p className={classes.title}>{userGroups.CONTRIBUTOR}</p>
-          <p className={classes.subheader}>I know about a problem in specific tests or code.</p>
-        </div>
-        <hr color={grey[200]} className={classes.line}/>
-      </div>
-    </div>
-    );
-  }
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx b/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx
new file mode 100644
index 0000000..296e449
--- /dev/null
+++ b/static_src/react/issue-wizard/RadioDescription/RadioDescription.test.tsx
@@ -0,0 +1,67 @@
+// Copyright 2021 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 React from 'react';
+import { render, screen, cleanup } from '@testing-library/react';
+import userEvent from '@testing-library/user-event'
+import { assert } from 'chai';
+import sinon from 'sinon';
+
+import { RadioDescription } from './RadioDescription.tsx';
+
+describe('RadioDescription', () => {
+  afterEach(cleanup);
+
+  it('renders', () => {
+    render(<RadioDescription />);
+    // look for blue radios
+    const radioOne = screen.getByRole('radio', { name: /Web Developer/i });
+    assert.isNotNull(radioOne)
+
+    const radioTwo = screen.getByRole('radio', { name: /End User/i });
+    assert.isNotNull(radioTwo)
+
+    const radioThree = screen.getByRole('radio', { name: /Chromium Contributor/i });
+    assert.isNotNull(radioThree)
+  });
+
+  it('checks selected radio value', () => {
+    // We're passing in the "Web Developer" value here manually
+    // to tell our code that that radio button is selected.
+    render(<RadioDescription value={'Web Developer'} />);
+
+    const checkedRadio = screen.getByRole('radio', { name: /Web Developer/i });
+    assert.isTrue(checkedRadio.checked);
+
+    // Extra check to make sure we haven't checked every single radio button.
+    const uncheckedRadio = screen.getByRole('radio', { name: /End User/i });
+    assert.isFalse(uncheckedRadio.checked);
+  });
+
+  it('sets radio value when radio button is clicked', () => {
+    // Using the sinon.js testing library to create a function for testing.
+    const setValue = sinon.stub();
+
+    render(<RadioDescription setValue={setValue} />);
+
+    const radio = screen.getByRole('radio', { name: /Web Developer/i });
+    userEvent.click(radio);
+
+    // Asserts that "Web Developer" was passed into our "setValue" function.
+    sinon.assert.calledWith(setValue, 'Web Developer');
+  });
+
+  it('sets radio value when any part of the parent RoleSelection is clicked', () => {
+    const setValue = sinon.stub();
+
+    render(<RadioDescription setValue={setValue} />);
+
+    // Click text in the RoleSelection component
+    const p = screen.getByText('End User');
+    userEvent.click(p);
+
+    // Asserts that "End User" was passed into our "setValue" function.
+    sinon.assert.calledWith(setValue, 'End User');
+  });
+});
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx b/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx
new file mode 100644
index 0000000..9a5a7d2
--- /dev/null
+++ b/static_src/react/issue-wizard/RadioDescription/RadioDescription.tsx
@@ -0,0 +1,59 @@
+// Copyright 2021 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 React from 'react';
+import { makeStyles } from '@material-ui/styles';
+import { RoleSelection } from './RoleSelection/RoleSelection.tsx';
+
+const userGroups = Object.freeze({
+  END_USER: 'End User',
+  WEB_DEVELOPER: 'Web Developer',
+  CONTRIBUTOR: 'Chromium Contributor',
+});
+
+const useStyles = makeStyles({
+  flex: {
+    display: 'flex',
+    justifyContent: 'space-between',
+  }
+});
+
+/**
+ * RadioDescription contains a set of radio buttons and descriptions (RoleSelection)
+ * to be chosen from in the landing step of the Issue Wizard.
+ *
+ * @returns React.ReactElement
+ */
+export const RadioDescription = ({ value, setValue }: { value: string, setValue: Function }): React.ReactElement => {
+  const classes = useStyles();
+
+  const handleRoleSelectionClick = (userGroup: string) =>
+    (event: React.MouseEvent<HTMLElement>) => setValue(userGroup)
+
+  return (
+    <div className={classes.flex}>
+      <RoleSelection
+        checked={value === userGroups.END_USER}
+        handleOnClick={handleRoleSelectionClick(userGroups.END_USER)}
+        value={userGroups.END_USER}
+        description="I am a user trying to do something on a website."
+        inputProps={{ 'aria-label': userGroups.END_USER }}
+      />
+      <RoleSelection
+        checked={value === userGroups.WEB_DEVELOPER}
+        handleOnClick={handleRoleSelectionClick(userGroups.WEB_DEVELOPER)}
+        value={userGroups.WEB_DEVELOPER}
+        description="I am a web developer trying to build something."
+        inputProps={{ 'aria-label': userGroups.WEB_DEVELOPER }}
+      />
+      <RoleSelection
+        checked={value === userGroups.CONTRIBUTOR}
+        handleOnClick={handleRoleSelectionClick(userGroups.CONTRIBUTOR)}
+        value={userGroups.CONTRIBUTOR}
+        description="I know about a problem in specific tests or code."
+        inputProps={{ 'aria-label': userGroups.CONTRIBUTOR }}
+      />
+    </div>
+  );
+}
\ No newline at end of file
diff --git a/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx b/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx
new file mode 100644
index 0000000..803a7b7
--- /dev/null
+++ b/static_src/react/issue-wizard/RadioDescription/RoleSelection/RoleSelection.tsx
@@ -0,0 +1,95 @@
+// Copyright 2021 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 React from 'react';
+import { makeStyles, withStyles } from '@material-ui/styles';
+import { blue, grey } from '@material-ui/core/colors';
+import Radio, { RadioProps } from '@material-ui/core/Radio';
+
+const useStyles = makeStyles({
+  container: {
+    width: '320px',
+    height: '150px',
+    position: 'relative',
+    display: 'inline-block',
+    cursor: 'pointer',
+  },
+  text: {
+    position: 'absolute',
+    display: 'inline-block',
+    left: '55px',
+  },
+  title: {
+    marginTop: '7px',
+    fontSize: '20px',
+    color: grey[900],
+  },
+  subheader: {
+    fontSize: '16px',
+    color: grey[800],
+  },
+  line: {
+    position: 'absolute',
+    bottom: 0,
+    width: '300px',
+    left: '20px',
+  }
+});
+
+const BlueRadio = withStyles({
+  root: {
+    color: blue[400],
+    '&$checked': {
+      color: blue[600],
+    },
+  },
+  checked: {},
+})((props: RadioProps) => <Radio color="default" {...props} />);
+
+interface RoleSelectionProps {
+  /* Whether or not the radio button should be checked */
+  checked: boolean
+  /* onClick callback defined in parent component */
+  handleOnClick: (event: React.MouseEvent<HTMLElement>) => void
+  /*
+      A string representing the type of user; this is the value of the input
+      see `userGroups`, which is defined in RadioDescription
+  */
+  value: string
+  /* Descriptive text to be displayed along with the radio button */
+  description: string
+  /* Additional props for the radio button component */
+  inputProps: { [key: string]: string }
+}
+
+/**
+ * RoleSelection encapsulates the radio button and details
+ * for selecting a role as an issue reporter in the issue wizard
+ * @see RadioDescription
+ */
+
+export const RoleSelection = ({
+  checked,
+  handleOnClick,
+  value,
+  description,
+  inputProps
+}: RoleSelectionProps): React.ReactElement => {
+  const classes = useStyles();
+  return (
+    <div className={classes.container} onClick={handleOnClick}>
+      <BlueRadio
+        checked={checked}
+        value={value}
+        inputProps={inputProps}
+      />
+      <div className={classes.text}>
+        <p className={classes.title}>{value}</p>
+        <p className={classes.subheader}>{description}</p>
+      </div>
+      <hr color={grey[200]} className={classes.line} />
+    </div>
+  )
+}
+
diff --git a/static_src/react/mr-react-autocomplete.tsx b/static_src/react/mr-react-autocomplete.tsx
index 8cc5f84..65a045f 100644
--- a/static_src/react/mr-react-autocomplete.tsx
+++ b/static_src/react/mr-react-autocomplete.tsx
@@ -77,11 +77,16 @@
   updated(changedProperties: Map<string | number | symbol, unknown>): void {
     super.updated(changedProperties);
 
+    const maxChipLabelWidth = '290px';
     const theme = createTheme({
       components: {
         MuiChip: {
           styleOverrides: {
-            root: {fontSize: 13},
+            root: { fontSize: 13 },
+            label: {
+              textOverflow: 'ellipsis',
+              maxWidth: maxChipLabelWidth
+            }
           },
         },
       },
diff --git a/static_src/shared/md-helper.js b/static_src/shared/md-helper.js
index a2f511c..7e7c8ca 100644
--- a/static_src/shared/md-helper.js
+++ b/static_src/shared/md-helper.js
@@ -29,7 +29,7 @@
 export const shouldRenderMarkdown = ({
   project, author, enabled = true, availableProjects = AVAILABLE_MD_PROJECTS
 } = {}) => {
-  if (author in BLOCKLIST) {
+  if (BLOCKLIST.has(author)) {
     return false;
   } else if (!enabled) {
     return false;
diff --git a/templates/features/updates-page.ezt b/templates/features/updates-page.ezt
index e5306f3..832a55b 100644
--- a/templates/features/updates-page.ezt
+++ b/templates/features/updates-page.ezt
@@ -1,4 +1,4 @@
-[define title]Updates[end]
+[define title]History[end]
 [if-any updates_data]
 
 [define even]Yes[end]
diff --git a/templates/sitewide/usertabs.ezt b/templates/sitewide/usertabs.ezt
index 8216bcb..daf3631 100644
--- a/templates/sitewide/usertabs.ezt
+++ b/templates/sitewide/usertabs.ezt
@@ -10,7 +10,7 @@
   </span>
 
   <span class="inst5">
-    <a href="[viewed_user.profile_url]updates">Updates</a>
+    <a href="[viewed_user.profile_url]updates">History</a>
   </span>
 
   [if-any viewing_self]