Merge "Convert gr-user-header to lit element."
diff --git a/.gitignore b/.gitignore
index b5204d1..95f94ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,9 +32,13 @@
 /node_modules/
 /package-lock.json
 /plugins/*
+!/plugins/.eslintignore
+!/plugins/.eslintrc.js
+!/plugins/.prettierrc.js
 !/plugins/package.json
 !/plugins/rollup.config.js
 !/plugins/tsconfig.json
+!/plugins/tsconfig-plugins-base.json
 !/plugins/yarn.lock
 !/plugins/BUILD
 !/plugins/codemirror-editor
diff --git a/java/com/google/gerrit/server/comment/CommentContextLoader.java b/java/com/google/gerrit/server/comment/CommentContextLoader.java
index a5aca48..8fbb259 100644
--- a/java/com/google/gerrit/server/comment/CommentContextLoader.java
+++ b/java/com/google/gerrit/server/comment/CommentContextLoader.java
@@ -46,6 +46,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.Repository;
@@ -101,7 +103,16 @@
     try (Repository repo = repoManager.openRepository(project);
         RevWalk rw = new RevWalk(repo)) {
       for (ObjectId commitId : commentsByCommitId.keySet()) {
-        RevCommit commit = rw.parseCommit(commitId);
+        RevCommit commit;
+        try {
+          commit = rw.parseCommit(commitId);
+        } catch (IncorrectObjectTypeException | MissingObjectException e) {
+          logger.atWarning().log("Commit %s is missing or has an incorrect object type", commitId);
+          commentsByCommitId
+              .get(commitId)
+              .forEach(contextInput -> result.put(contextInput, CommentContext.empty()));
+          continue;
+        }
         for (ContextInput contextInput : commentsByCommitId.get(commitId)) {
           Optional<Range> range = getStartAndEndLines(contextInput);
           if (!range.isPresent()) {
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
index 29058ef..81cb7159 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentContextIT.java
@@ -30,6 +30,8 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.Comment;
 import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.ContextLineInfo;
 import com.google.gerrit.server.change.FileContentUtil;
@@ -88,6 +90,28 @@
   }
 
   @Test
+  public void commentContextForRootCommitOnParentSideReturnsEmptyContext() throws Exception {
+    // Create a change in a new branch, making the patchset commit a root commit
+    ChangeInfo changeInfo = createChangeInNewBranch("newBranch");
+    String changeId = changeInfo.changeId;
+    String revision = changeInfo.revisions.keySet().iterator().next();
+
+    // Write a comment on the parent side of the commit message. Set parent=1 because if unset, our
+    // handler in PostReview assumes we want to write on the auto-merge commit and fails the
+    // pre-condition.
+    CommentInput comment = CommentsUtil.newComment(COMMIT_MSG, Side.PARENT, 0, "comment", false);
+    comment.parent = 1;
+    CommentsUtil.addComments(gApi, changeId, revision, comment);
+
+    List<CommentInfo> comments =
+        gApi.changes().id(changeId).commentsRequest().withContext(true).getAsList();
+    assertThat(comments).hasSize(1);
+    CommentInfo c = comments.stream().collect(MoreCollectors.onlyElement());
+    assertThat(c.commitId).isEqualTo(ObjectId.zeroId().name());
+    assertThat(c.contextLines).isEmpty();
+  }
+
+  @Test
   public void commentContextForCommitMessageForLineComment() throws Exception {
     PushOneCommit.Result result =
         createChange(testRepo, "master", SUBJECT, FILE_NAME, FILE_CONTENT, "topic");
@@ -568,4 +592,13 @@
     }
     return result;
   }
+
+  private ChangeInfo createChangeInNewBranch(String branchName) throws Exception {
+    ChangeInput in = new ChangeInput();
+    in.project = project.get();
+    in.branch = branchName;
+    in.newBranch = true;
+    in.subject = "New changes";
+    return gApi.changes().create(in).get();
+  }
 }
diff --git a/plugins/.eslintignore b/plugins/.eslintignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/plugins/.eslintignore
diff --git a/plugins/.eslintrc.js b/plugins/.eslintrc.js
new file mode 100644
index 0000000..149a31e
--- /dev/null
+++ b/plugins/.eslintrc.js
@@ -0,0 +1,318 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This is a base template for TypeScript plugins.
+ *
+ * When extending this template you have to:
+ * - Set the __plugindir variable.
+ */
+
+const path = require('path');
+
+module.exports = {
+  extends: ['eslint:recommended', 'google'],
+  parserOptions: {
+    ecmaVersion: 9,
+    sourceType: 'module',
+  },
+  env: {
+    browser: true,
+    es6: true,
+  },
+  rules: {
+    // https://eslint.org/docs/rules/no-confusing-arrow
+    'no-confusing-arrow': 'error',
+    // https://eslint.org/docs/rules/newline-per-chained-call
+    'newline-per-chained-call': ['error', {ignoreChainWithDepth: 2}],
+    // https://eslint.org/docs/rules/arrow-body-style
+    'arrow-body-style': ['error', 'as-needed',
+      {requireReturnForObjectLiteral: true}],
+    // https://eslint.org/docs/rules/arrow-parens
+    'arrow-parens': ['error', 'as-needed'],
+    // https://eslint.org/docs/rules/block-spacing
+    'block-spacing': ['error', 'always'],
+    // https://eslint.org/docs/rules/brace-style
+    'brace-style': ['error', '1tbs', {allowSingleLine: true}],
+    // https://eslint.org/docs/rules/camelcase
+    'camelcase': 'off',
+    // https://eslint.org/docs/rules/comma-dangle
+    'comma-dangle': ['error', {
+      arrays: 'always-multiline',
+      objects: 'always-multiline',
+      imports: 'always-multiline',
+      exports: 'always-multiline',
+      functions: 'never',
+    }],
+    // https://eslint.org/docs/rules/eol-last
+    'eol-last': 'off',
+    'guard-for-in': 'error',
+    // https://eslint.org/docs/rules/indent
+    'indent': ['error', 2, {
+      MemberExpression: 2,
+      FunctionDeclaration: {body: 1, parameters: 2},
+      FunctionExpression: {body: 1, parameters: 2},
+      CallExpression: {arguments: 2},
+      ArrayExpression: 1,
+      ObjectExpression: 1,
+      SwitchCase: 1,
+    }],
+    // https://eslint.org/docs/rules/keyword-spacing
+    'keyword-spacing': ['error', {after: true, before: true}],
+    // https://eslint.org/docs/rules/lines-between-class-members
+    'lines-between-class-members': ['error', 'always'],
+    // https://eslint.org/docs/rules/max-len
+    'max-len': [
+      'error',
+      80,
+      2,
+      {
+        ignoreComments: true,
+        ignorePattern: '^import .*;$',
+      },
+    ],
+    // https://eslint.org/docs/rules/new-cap
+    'new-cap': ['error', {
+      capIsNewExceptions: ['Polymer'],
+      capIsNewExceptionPattern: '^.*Mixin$',
+    }],
+    // https://eslint.org/docs/rules/no-console
+    'no-console': [
+      'error',
+      {allow: ['warn', 'error', 'info', 'assert', 'group', 'groupEnd']},
+    ],
+    // https://eslint.org/docs/rules/no-multiple-empty-lines
+    'no-multiple-empty-lines': ['error', {max: 1}],
+    // https://eslint.org/docs/rules/no-prototype-builtins
+    'no-prototype-builtins': 'off',
+    // https://eslint.org/docs/rules/no-redeclare
+    'no-redeclare': 'off',
+    // https://eslint.org/docs/rules/no-trailing-spaces
+    'no-trailing-spaces': 'error',
+    // https://eslint.org/docs/rules/no-irregular-whitespace
+    'no-irregular-whitespace': 'error',
+    // https://eslint.org/docs/rules/array-callback-return
+    'array-callback-return': ['error', {allowImplicit: true}],
+    // https://eslint.org/docs/rules/no-restricted-syntax
+    'no-restricted-syntax': [
+      'error',
+      {
+        selector: 'ExpressionStatement > CallExpression > ' +
+            'MemberExpression[object.name=\'test\'][property.name=\'only\']',
+        message: 'Remove test.only.',
+      },
+      {
+        selector: 'ExpressionStatement > CallExpression > ' +
+            'MemberExpression[object.name=\'suite\'][property.name=\'only\']',
+        message: 'Remove suite.only.',
+      },
+    ],
+    // no-undef disables global variable.
+    // "globals" declares allowed global variables.
+    // https://eslint.org/docs/rules/no-undef
+    'no-undef': ['error'],
+    // https://eslint.org/docs/rules/no-useless-escape
+    'no-useless-escape': 'off',
+    // https://eslint.org/docs/rules/no-var
+    'no-var': 'error',
+    // https://eslint.org/docs/rules/operator-linebreak
+    'operator-linebreak': 'off',
+    // https://eslint.org/docs/rules/object-shorthand
+    'object-shorthand': ['error', 'always'],
+    // https://eslint.org/docs/rules/padding-line-between-statements
+    'padding-line-between-statements': [
+      'error',
+      {
+        blankLine: 'always',
+        prev: 'class',
+        next: '*',
+      },
+      {
+        blankLine: 'always',
+        prev: '*',
+        next: 'class',
+      },
+    ],
+    // https://eslint.org/docs/rules/prefer-arrow-callback
+    'prefer-arrow-callback': 'error',
+    // https://eslint.org/docs/rules/prefer-const
+    'prefer-const': 'error',
+    // https://eslint.org/docs/rules/prefer-promise-reject-errors
+    'prefer-promise-reject-errors': 'error',
+    // https://eslint.org/docs/rules/prefer-spread
+    'prefer-spread': 'error',
+    // https://eslint.org/docs/rules/prefer-object-spread
+    'prefer-object-spread': 'error',
+    // https://eslint.org/docs/rules/quote-props
+    'quote-props': ['error', 'consistent-as-needed'],
+    // https://eslint.org/docs/rules/semi
+    'semi': ['error', 'always'],
+    // https://eslint.org/docs/rules/template-curly-spacing
+    'template-curly-spacing': 'error',
+
+    // https://eslint.org/docs/rules/require-jsdoc
+    'require-jsdoc': 0,
+    // https://eslint.org/docs/rules/valid-jsdoc
+    'valid-jsdoc': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-alignment
+    'jsdoc/check-alignment': 2,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-examples
+    'jsdoc/check-examples': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-indentation
+    'jsdoc/check-indentation': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-param-names
+    'jsdoc/check-param-names': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-syntax
+    'jsdoc/check-syntax': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-tag-names
+    'jsdoc/check-tag-names': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-check-types
+    'jsdoc/check-types': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-implements-on-classes
+    'jsdoc/implements-on-classes': 2,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-match-description
+    'jsdoc/match-description': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-newline-after-description
+    'jsdoc/newline-after-description': 2,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-types
+    'jsdoc/no-types': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-no-undefined-types
+    'jsdoc/no-undefined-types': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description
+    'jsdoc/require-description': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-description-complete-sentence
+    'jsdoc/require-description-complete-sentence': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-example
+    'jsdoc/require-example': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-hyphen-before-param-description
+    'jsdoc/require-hyphen-before-param-description': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-jsdoc
+    'jsdoc/require-jsdoc': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param
+    'jsdoc/require-param': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-description
+    'jsdoc/require-param-description': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-name
+    'jsdoc/require-param-name': 2,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns
+    'jsdoc/require-returns': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-check
+    'jsdoc/require-returns-check': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-description
+    'jsdoc/require-returns-description': 0,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-valid-types
+    'jsdoc/valid-types': 2,
+    // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-file-overview
+    'jsdoc/require-file-overview': ['error', {
+      tags: {
+        license: {
+          mustExist: true,
+          preventDuplicates: true,
+        },
+      },
+    }],
+    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
+    'import/no-self-import': 2,
+    // The no-cycle rule is slow, because it doesn't cache dependencies.
+    // Disable it.
+    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
+    'import/no-cycle': 0,
+    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
+    'import/no-useless-path-segments': 2,
+    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unused-modules.md
+    'import/no-unused-modules': 2,
+    // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-default-export.md
+    'import/no-default-export': 2,
+    // Prevents certain identifiers being used.
+    // Prefer flush() over flushAsynchronousOperations().
+    'id-blacklist': ['error', 'flushAsynchronousOperations'],
+  },
+
+  overrides: [
+    {
+      files: ['.eslintrc.js'],
+      env: {
+        browser: false,
+        es6: true,
+        node: true,
+      },
+    },
+    {
+      // .js-only rules
+      files: ['**/*.js'],
+      rules: {
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-param-type
+        'jsdoc/require-param-type': 2,
+        // https://github.com/gajus/eslint-plugin-jsdoc#eslint-plugin-jsdoc-rules-require-returns-type
+        'jsdoc/require-returns-type': 2,
+        // The rule is required for .js files only, because typescript compiler
+        // always checks import.
+        'import/no-unresolved': 2,
+        'import/named': 2,
+      },
+    },
+    {
+      files: ['**/*.ts'],
+      extends: [require.resolve('gts/.eslintrc.json')],
+      rules: {
+        'no-restricted-imports': ['error', {
+          name: '@polymer/decorators/lib/decorators',
+          message: 'Use @polymer/decorators instead',
+        }],
+        '@typescript-eslint/no-explicit-any': 'error',
+        // See https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/9
+        '@typescript-eslint/ban-ts-comment': 'off',
+        // The following rules is required to match internal google rules
+        '@typescript-eslint/restrict-plus-operands': 'error',
+        '@typescript-eslint/no-unused-vars': [
+          'error',
+          {argsIgnorePattern: '^_'},
+        ],
+        // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
+        'node/no-unsupported-features/node-builtins': 'off',
+        // Disable no-invalid-this for ts files, because it incorrectly reports
+        // errors in some cases (see https://github.com/typescript-eslint/typescript-eslint/issues/491)
+        // At the same time, we are using typescript in a strict mode and
+        // it catches almost all errors related to invalid usage of this.
+        'no-invalid-this': 'off',
+
+        'node/no-extraneous-import': 'off',
+
+        // Typescript already checks for undef
+        'no-undef': 'off',
+
+        'jsdoc/no-types': 2,
+      },
+      parserOptions: {
+        // The __plugindir variable has to be defined by the plugin config.
+        project: path.resolve(__dirname, __plugindir, 'tsconfig.json'),
+      },
+    },
+  ],
+  plugins: [
+    'html',
+    'jsdoc',
+    'import',
+    'prettier',
+  ],
+  settings: {
+    'html/report-bad-indent': 'error',
+    'import/resolver': {
+      node: {},
+    },
+  },
+};
diff --git a/plugins/.prettierrc.js b/plugins/.prettierrc.js
new file mode 100644
index 0000000..64dbcb3
--- /dev/null
+++ b/plugins/.prettierrc.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This is a base template for TypeScript plugins.
+ */
+module.exports = {
+  "overrides": [
+    {
+      "files": ["**/*.ts"],
+      "options": {
+          ...require('gts/.prettierrc.json')
+      }
+    }
+  ]
+};
diff --git a/plugins/BUILD b/plugins/BUILD
index ad517ed..250d1a6 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -10,7 +10,13 @@
 
 package(default_visibility = ["//visibility:public"])
 
-exports_files(["rollup.config.js"])
+exports_files([
+    ".eslintrc.js",
+    ".eslintignore",
+    ".prettierrc.js",
+    "rollup.config.js",
+    "tsconfig-plugins-base.json",
+])
 
 ts_config(
     name = "plugin-tsconfig",
diff --git a/plugins/delete-project b/plugins/delete-project
index 7f2f1c5..7dce6f7 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit 7f2f1c5961f89c7f44ac4a26bf8e035db5e70e0c
+Subproject commit 7dce6f70611cd8dbf1d38628698155258ee8ef82
diff --git a/plugins/package.json b/plugins/package.json
index 2ed03e5..cd85e8c 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -1,6 +1,6 @@
 {
-    "name": "polygerrit-plugin-dependencies-placeholder",
-    "description": "Gerrit Code Review - Polygerrit plugin dependencies placeholder, expected to be overridden by plugins",
+    "name": "gerrit-plugin-dependencies",
+    "description": "Gerrit Code Review - frontend plugin dependencies, each plugin may depend on a subset of these",
     "browser": true,
     "dependencies": {
       "@polymer/decorators": "^3.0.0",
diff --git a/plugins/tsconfig-plugins-base.json b/plugins/tsconfig-plugins-base.json
new file mode 100644
index 0000000..b9d14e1
--- /dev/null
+++ b/plugins/tsconfig-plugins-base.json
@@ -0,0 +1,39 @@
+{
+  "compilerOptions": {
+    /* Basic Options */
+    "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
+    "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
+    "inlineSourceMap": true, /* Generates corresponding '.map' file. */
+    "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    "removeComments": false, /* Emit comments to output */
+
+    /* Strict Type-Checking Options */
+    "strict": true, /* Enable all strict type-checking options. */
+    "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+    "strictNullChecks": true, /* Enable strict null checks. */
+    "strictFunctionTypes": true, /* Enable strict checking of function types. */
+    "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+    "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+
+    /* Additional Checks */
+    "noUnusedLocals": true, /* Report errors on unused locals. */
+    "noUnusedParameters": true, /* Report errors on unused parameters. */
+    "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+    "noFallthroughCasesInSwitch": true,/* Report errors for fallthrough cases in switch statement. */
+
+    "skipLibCheck": true, /* Do not check node_modules */
+
+    /* Module Resolution Options */
+    "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+
+    /* Advanced Options */
+    "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
+    "incremental": true,
+    "experimentalDecorators": true,
+
+    "allowUmdGlobalAccess": true
+  },
+}
diff --git a/polygerrit-ui/.gitignore b/polygerrit-ui/.gitignore
index fb20ac5..3e1bb74 100644
--- a/polygerrit-ui/.gitignore
+++ b/polygerrit-ui/.gitignore
@@ -1,8 +1,8 @@
+/.tmp/
+/.vscode/
+/bower_components/
+/dist/
+/fonts/
 /node_modules/
+/npm-debug.log
 /package-lock.json
-npm-debug.log
-dist
-fonts
-bower_components
-.tmp
-.vscode
diff --git a/polygerrit-ui/app/.gitignore b/polygerrit-ui/app/.gitignore
index 6b96e60..c45bac3 100644
--- a/polygerrit-ui/app/.gitignore
+++ b/polygerrit-ui/app/.gitignore
@@ -1,3 +1,4 @@
-/plugins/
 /node_modules/
+/package-lock.json
+/plugins/
 /tmpl_out/
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 63d85a2..4a186c1 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -100,7 +100,6 @@
     "elements/admin/gr-access-section/gr-access-section_html.ts",
     "elements/admin/gr-admin-view/gr-admin-view_html.ts",
     "elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.ts",
-    "elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts",
     "elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.ts",
     "elements/admin/gr-group-members/gr-group-members_html.ts",
     "elements/admin/gr-group/gr-group_html.ts",
@@ -132,7 +131,6 @@
     "elements/change/gr-thread-list/gr-thread-list_html.ts",
     "elements/diff/gr-diff-builder/gr-diff-builder-element_html.ts",
     "elements/diff/gr-diff-host/gr-diff-host_html.ts",
-    "elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts",
     "elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.ts",
     "elements/diff/gr-diff-view/gr-diff-view_html.ts",
     "elements/diff/gr-diff/gr-diff_html.ts",
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
index e483fc4..7fce8e5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.ts
@@ -94,7 +94,7 @@
     throw new Error(`Invalid itemDetail: ${this.itemDetail}`);
   }
 
-  _computeHideItemClass(type: RepoDetailView.BRANCHES | RepoDetailView.TAGS) {
+  _computeHideItemClass(type?: RepoDetailView.BRANCHES | RepoDetailView.TAGS) {
     return type === RepoDetailView.BRANCHES ? 'hideItem' : '';
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
index 452aab7..0e2b157 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.ts
@@ -38,28 +38,14 @@
     <div id="form">
       <section id="itemNameSection">
         <span class="title">[[detailType]] name</span>
-        <iron-input
-          placeholder="[[detailType]] Name"
-          bind-value="{{_itemName}}"
-        >
-          <input
-            is="iron-input"
-            placeholder="[[detailType]] Name"
-            bind-value="{{_itemName}}"
-          />
+        <iron-input bind-value="{{_itemName}}">
+          <input placeholder="[[detailType]] Name" />
         </iron-input>
       </section>
       <section id="itemRevisionSection">
         <span class="title">Initial Revision</span>
-        <iron-input
-          placeholder="Revision (Branch or SHA-1)"
-          bind-value="{{_itemRevision}}"
-        >
-          <input
-            is="iron-input"
-            placeholder="Revision (Branch or SHA-1)"
-            bind-value="{{_itemRevision}}"
-          />
+        <iron-input bind-value="{{_itemRevision}}">
+          <input placeholder="Revision (Branch or SHA-1)" />
         </iron-input>
       </section>
       <section
@@ -67,15 +53,8 @@
         class$="[[_computeHideItemClass(itemDetail)]]"
       >
         <span class="title">Annotation</span>
-        <iron-input
-          placeholder="Annotation (Optional)"
-          bind-value="{{_itemAnnotation}}"
-        >
-          <input
-            is="iron-input"
-            placeholder="Annotation (Optional)"
-            bind-value="{{_itemAnnotation}}"
-          />
+        <iron-input bind-value="{{_itemAnnotation}}">
+          <input placeholder="Annotation (Optional)" />
         </iron-input>
       </section>
     </div>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
similarity index 60%
rename from polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
rename to polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
index 60af4d5..b6a08b87 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.ts
@@ -15,69 +15,72 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-create-pointer-dialog.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-create-pointer-dialog';
+import {GrCreatePointerDialog} from './gr-create-pointer-dialog';
+import {queryAndAssert, stubRestApi} from '../../../test/test-utils';
+import {BranchName} from '../../../types/common';
+import {RepoDetailView} from '../../core/gr-navigation/gr-navigation';
+import {IronInputElement} from '@polymer/iron-input';
 
 const basicFixture = fixtureFromElement('gr-create-pointer-dialog');
 
 suite('gr-create-pointer-dialog tests', () => {
-  let element;
+  let element: GrCreatePointerDialog;
 
-  const ironInput = function(element) {
-    return element.querySelector('iron-input');
-  };
+  const ironInput = (element: Element) =>
+    queryAndAssert<IronInputElement>(element, 'iron-input');
 
   setup(() => {
     element = basicFixture.instantiate();
   });
 
   test('branch created', done => {
-    stubRestApi('createRepoBranch').returns(Promise.resolve({}));
+    stubRestApi('createRepoBranch').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
 
-    element._itemName = 'test-branch';
-    element.itemDetail = 'branches';
+    element._itemName = 'test-branch' as BranchName;
+    element.itemDetail = 'branches' as RepoDetailView.BRANCHES;
 
     ironInput(element.$.itemNameSection).bindValue = 'test-branch2';
     ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
     setTimeout(() => {
       assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-branch2');
+      assert.equal(element._itemName, 'test-branch2' as BranchName);
       assert.equal(element._itemRevision, 'HEAD');
       done();
     });
   });
 
   test('tag created', done => {
-    stubRestApi('createRepoTag').returns(Promise.resolve({}));
+    stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
 
-    element._itemName = 'test-tag';
-    element.itemDetail = 'tags';
+    element._itemName = 'test-tag' as BranchName;
+    element.itemDetail = 'tags' as RepoDetailView.TAGS;
 
     ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
     ironInput(element.$.itemRevisionSection).bindValue = 'HEAD';
 
     setTimeout(() => {
       assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2');
+      assert.equal(element._itemName, 'test-tag2' as BranchName);
       assert.equal(element._itemRevision, 'HEAD');
       done();
     });
   });
 
   test('tag created with annotations', done => {
-    stubRestApi('createRepoTag').returns(() => Promise.resolve({}));
+    stubRestApi('createRepoTag').returns(Promise.resolve(new Response()));
 
     assert.isFalse(element.hasNewItemName);
 
-    element._itemName = 'test-tag';
+    element._itemName = 'test-tag' as BranchName;
     element._itemAnnotation = 'test-message';
-    element.itemDetail = 'tags';
+    element.itemDetail = 'tags' as RepoDetailView.TAGS;
 
     ironInput(element.$.itemNameSection).bindValue = 'test-tag2';
     ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2';
@@ -85,7 +88,7 @@
 
     setTimeout(() => {
       assert.isTrue(element.hasNewItemName);
-      assert.equal(element._itemName, 'test-tag2');
+      assert.equal(element._itemName, 'test-tag2' as BranchName);
       assert.equal(element._itemAnnotation, 'test-message2');
       assert.equal(element._itemRevision, 'HEAD');
       done();
@@ -93,11 +96,13 @@
   });
 
   test('_computeHideItemClass returns hideItem if type is branches', () => {
-    assert.equal(element._computeHideItemClass('branches'), 'hideItem');
+    assert.equal(
+      element._computeHideItemClass(RepoDetailView.BRANCHES),
+      'hideItem'
+    );
   });
 
   test('_computeHideItemClass returns strings if not branches', () => {
-    assert.equal(element._computeHideItemClass('tags'), '');
+    assert.equal(element._computeHideItemClass(RepoDetailView.TAGS), '');
   });
 });
-
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
index 80c58ca..411bb27 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts
@@ -91,7 +91,7 @@
   _groups?: ProjectAccessGroups;
 
   @property({type: Object})
-  _inheritsFrom?: ProjectInfo | null | {};
+  _inheritsFrom?: ProjectInfo;
 
   @property({type: Object})
   _labels?: LabelNameToLabelTypeInfoMap;
@@ -114,7 +114,7 @@
   @property({type: Boolean})
   _loading = true;
 
-  private originalInheritsFrom?: ProjectInfo | null;
+  private originalInheritsFrom?: ProjectInfo;
 
   private readonly restApiService = appContext.restApiService;
 
@@ -159,16 +159,13 @@
         // Keep a copy of the original inherit from values separate from
         // the ones data bound to gr-autocomplete, so the original value
         // can be restored if the user cancels.
-        this._inheritsFrom = res.inherits_from
-          ? {
-              ...res.inherits_from,
-            }
-          : null;
-        this.originalInheritsFrom = res.inherits_from
-          ? {
-              ...res.inherits_from,
-            }
-          : null;
+        if (res.inherits_from) {
+          this._inheritsFrom = {...res.inherits_from};
+          this.originalInheritsFrom = {...res.inherits_from};
+        } else {
+          this._inheritsFrom = undefined;
+          this.originalInheritsFrom = undefined;
+        }
         // Initialize the filter value so when the user clicks edit, the
         // current value appears. If there is no parent repo, it is
         // initialized as an empty string.
@@ -218,19 +215,11 @@
   }
 
   _handleUpdateInheritFrom(e: CustomEvent<{value: string}>) {
-    const parentProject: ProjectInfo = {
+    this._inheritsFrom = {
+      ...(this._inheritsFrom ?? {}),
       id: e.detail.value as UrlEncodedRepoName,
       name: this._inheritFromFilter,
     };
-    if (!this._inheritsFrom) {
-      this._inheritsFrom = parentProject;
-    } else {
-      // TODO(TS): replace with
-      // this._inheritsFrom = {...this._inheritsFrom, ...parentProject};
-      const projectInfo = this._inheritsFrom as ProjectInfo;
-      projectInfo.id = parentProject.id;
-      projectInfo.name = parentProject.name;
-    }
     this._handleAccessModified();
   }
 
@@ -268,8 +257,8 @@
     return weblinks && weblinks.length ? 'show' : '';
   }
 
-  _computeShowInherit(inheritsFrom?: RepoName) {
-    return inheritsFrom ? 'show' : '';
+  _computeShowInherit(inheritsFrom?: ProjectInfo) {
+    return inheritsFrom?.id?.length ? 'show' : '';
   }
 
   // TODO(TS): Unclear what is model here, provide a better explanation
@@ -297,18 +286,10 @@
     }
     // Restore inheritFrom.
     if (this._inheritsFrom) {
-      // Can't assign this._inheritsFrom = {...this.originalInheritsFrom}
-      // directly, because this._inheritsFrom is declared as
-      // '...|null|undefined` and typescript reports error when trying
-      // to access .name property (because 'name' in null and 'name' in undefined
-      // lead to runtime error)
-      // After migrating to Typescript v4.2 the code below can be rewritten as
-      // const copy = {...this.originalInheritsFrom};
-      const copy: ProjectInfo | {} = this.originalInheritsFrom
+      this._inheritsFrom = this.originalInheritsFrom
         ? {...this.originalInheritsFrom}
-        : {};
-      this._inheritsFrom = copy;
-      this._inheritFromFilter = 'name' in copy ? copy.name : undefined;
+        : undefined;
+      this._inheritFromFilter = this.originalInheritsFrom?.name;
     }
     if (!this._local) {
       return;
@@ -448,12 +429,10 @@
 
     const originalInheritsFromId = this.originalInheritsFrom
       ? singleDecodeURL(this.originalInheritsFrom.id)
-      : null;
-    // TODO(TS): this._inheritsFrom as ProjectInfo might be a mistake.
-    // _inheritsFrom can be {}
+      : undefined;
     const inheritsFromId = this._inheritsFrom
-      ? singleDecodeURL((this._inheritsFrom as ProjectInfo).id)
-      : null;
+      ? singleDecodeURL(this._inheritsFrom.id)
+      : undefined;
 
     const inheritFromChanged =
       // Inherit from changed
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index a4e019e..2b7fdf5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -100,22 +100,24 @@
       name: 'Create Account',
     },
   };
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
     stubRestApi('getAccount').returns(Promise.resolve(null));
     repoStub = stubRestApi('getRepo').returns(Promise.resolve(repoRes));
     element._loading = false;
     element._ownerOf = [];
     element._canUpload = false;
+    await flush();
   });
 
-  test('_repoChanged called when repo name changes', () => {
+  test('_repoChanged called when repo name changes', async () => {
     sinon.stub(element, '_repoChanged');
     element.repo = 'New Repo';
+    await flush();
     assert.isTrue(element._repoChanged.called);
   });
 
-  test('_repoChanged', done => {
+  test('_repoChanged', async () => {
     const accessStub = stubRestApi(
         'getRepoAccessRights');
 
@@ -127,31 +129,28 @@
         'getCapabilities');
     capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
 
-    element._repoChanged('New Repo').then(() => {
-      assert.isTrue(accessStub.called);
-      assert.isTrue(capabilitiesStub.called);
-      assert.isTrue(repoStub.called);
-      assert.isNotOk(element._inheritsFrom);
-      assert.deepEqual(element._local, accessRes.local);
-      assert.deepEqual(element._sections,
-          toSortedPermissionsArray(accessRes.local));
-      assert.deepEqual(element._labels, repoRes.labels);
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('.weblinks')).display,
-      'block');
-      return element._repoChanged('Another New Repo');
-    })
-        .then(() => {
-          assert.deepEqual(element._sections,
-              toSortedPermissionsArray(accessRes2.local));
-          assert.equal(getComputedStyle(element.shadowRoot
-              .querySelector('.weblinks')).display,
-          'none');
-          done();
-        });
+    await element._repoChanged('New Repo');
+    assert.isTrue(accessStub.called);
+    assert.isTrue(capabilitiesStub.called);
+    assert.isTrue(repoStub.called);
+    assert.isNotOk(element._inheritsFrom);
+    assert.deepEqual(element._local, accessRes.local);
+    assert.deepEqual(element._sections,
+        toSortedPermissionsArray(accessRes.local));
+    assert.deepEqual(element._labels, repoRes.labels);
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.weblinks')).display,
+    'block');
+
+    await element._repoChanged('Another New Repo');
+    assert.deepEqual(element._sections,
+        toSortedPermissionsArray(accessRes2.local));
+    assert.equal(getComputedStyle(element.shadowRoot
+        .querySelector('.weblinks')).display,
+    'none');
   });
 
-  test('_repoChanged when repo changes to undefined returns', done => {
+  test('_repoChanged when repo changes to undefined returns', async () => {
     const capabilitiesRes = {
       accessDatabase: {
         id: 'accessDatabase',
@@ -163,12 +162,10 @@
     const capabilitiesStub = stubRestApi(
         'getCapabilities').returns(Promise.resolve(capabilitiesRes));
 
-    element._repoChanged().then(() => {
-      assert.isFalse(accessStub.called);
-      assert.isFalse(capabilitiesStub.called);
-      assert.isFalse(repoStub.called);
-      done();
-    });
+    await element._repoChanged();
+    assert.isFalse(accessStub.called);
+    assert.isFalse(capabilitiesStub.called);
+    assert.isFalse(repoStub.called);
   });
 
   test('_computeParentHref', () => {
@@ -190,24 +187,29 @@
         'editing');
   });
 
-  test('inherit section', () => {
+  test('inherit section', async () => {
     element._local = {};
     element._ownerOf = [];
     sinon.stub(element, '_computeParentHref');
+    await flush();
+
     // Nothing should appear when no inherit from and not in edit mode.
     assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
     // The autocomplete should be hidden, and the link should be  displayed.
     assert.isFalse(element._computeParentHref.called);
-    // When it edit mode, the autocomplete should appear.
+    // When in edit mode, the autocomplete should appear.
     element._editing = true;
     // When editing, the autocomplete should still not be shown.
     assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none');
+
     element._editing = false;
     element._inheritsFrom = {
+      id: '1234',
       name: 'another-repo',
     };
+    await flush();
+
     // When there is a parent project, the link should be displayed.
-    flush();
     assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none');
     assert.notEqual(getComputedStyle(element.$.inheritFromName).display,
         'none');
@@ -222,9 +224,10 @@
         'none');
   });
 
-  test('_handleUpdateInheritFrom', () => {
+  test('_handleUpdateInheritFrom', async () => {
     element._inheritFromFilter = 'foo bar baz';
     element._handleUpdateInheritFrom({detail: {value: 'abc+123'}});
+    await flush();
     assert.isOk(element._inheritsFrom);
     assert.equal(element._inheritsFrom.id, 'abc+123');
     assert.equal(element._inheritsFrom.name, 'foo bar baz');
@@ -251,46 +254,61 @@
   });
 
   suite('with defined sections', () => {
-    const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => {
+    const testEditSaveCancelBtns = async (
+        shouldShowSave,
+        shouldShowSaveReview
+    ) => {
       // Edit button is visible and Save button is hidden.
       assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none');
       assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
       assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
       assert.equal(element.$.editBtn.innerText, 'EDIT');
-      assert.equal(getComputedStyle(element.$.editInheritFromInput).display,
-          'none');
+      assert.equal(
+          getComputedStyle(element.$.editInheritFromInput).display,
+          'none'
+      );
       element._inheritsFrom = {
         id: 'test-project',
       };
-      flush();
-      assert.equal(getComputedStyle(element.shadowRoot
-          .querySelector('#editInheritFromInput'))
-          .display, 'none');
+      await flush();
+      assert.equal(
+          getComputedStyle(
+              element.shadowRoot.querySelector('#editInheritFromInput')
+          ).display,
+          'none'
+      );
 
       MockInteractions.tap(element.$.editBtn);
-      flush();
+      await flush();
 
       // Edit button changes to Cancel button, and Save button is visible but
       // disabled.
       assert.equal(element.$.editBtn.innerText, 'CANCEL');
       if (shouldShowSaveReview) {
-        assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display,
-            'none');
+        assert.notEqual(
+            getComputedStyle(element.$.saveReviewBtn).display,
+            'none'
+        );
         assert.isTrue(element.$.saveReviewBtn.disabled);
       }
       if (shouldShowSave) {
         assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none');
         assert.isTrue(element.$.saveBtn.disabled);
       }
-      assert.notEqual(getComputedStyle(element.shadowRoot
-          .querySelector('#editInheritFromInput'))
-          .display, 'none');
+      assert.notEqual(
+          getComputedStyle(
+              element.shadowRoot.querySelector('#editInheritFromInput')
+          ).display,
+          'none'
+      );
 
       // Save button should be enabled after access is modified
       element.dispatchEvent(
           new CustomEvent('access-modified', {
-            composed: true, bubbles: true,
-          }));
+            composed: true,
+            bubbles: true,
+          })
+      );
       if (shouldShowSaveReview) {
         assert.isFalse(element.$.saveReviewBtn.disabled);
       }
@@ -299,7 +317,7 @@
       }
     };
 
-    setup(() => {
+    setup(async () => {
       // Create deep copies of these objects so the originals are not modified
       // by any tests.
       element._local = JSON.parse(JSON.stringify(accessRes.local));
@@ -308,18 +326,19 @@
       element._groups = JSON.parse(JSON.stringify(accessRes.groups));
       element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
       element._labels = JSON.parse(JSON.stringify(repoRes.labels));
-      flush();
+      await flush();
     });
 
-    test('removing an added section', () => {
+    test('removing an added section', async () => {
       element.editing = true;
+      await flush();
       assert.equal(element._sections.length, 1);
       element.shadowRoot
           .querySelector('gr-access-section').dispatchEvent(
               new CustomEvent('added-section-removed', {
                 composed: true, bubbles: true,
               }));
-      flush();
+      await flush();
       assert.equal(element._sections.length, 0);
     });
 
@@ -328,36 +347,41 @@
       assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
     });
 
-    test('button visibility for non ref owner with upload privilege', () => {
-      element._canUpload = true;
-      testEditSaveCancelBtns(false, true);
-    });
+    test('button visibility for non ref owner with upload privilege',
+        async () => {
+          element._canUpload = true;
+          await flush();
+          testEditSaveCancelBtns(false, true);
+        });
 
-    test('button visibility for ref owner', () => {
+    test('button visibility for ref owner', async () => {
       element._ownerOf = ['refs/for/*'];
+      await flush();
       testEditSaveCancelBtns(true, false);
     });
 
-    test('button visibility for ref owner and upload', () => {
+    test('button visibility for ref owner and upload', async () => {
       element._ownerOf = ['refs/for/*'];
       element._canUpload = true;
+      await flush();
       testEditSaveCancelBtns(true, false);
     });
 
-    test('_handleAccessModified called with event fired', () => {
+    test('_handleAccessModified called with event fired', async () => {
       sinon.spy(element, '_handleAccessModified');
       element.dispatchEvent(
           new CustomEvent('access-modified', {
             composed: true, bubbles: true,
           }));
+      await flush();
       assert.isTrue(element._handleAccessModified.called);
     });
 
-    test('_handleAccessModified called when parent changes', () => {
+    test('_handleAccessModified called when parent changes', async () => {
       element._inheritsFrom = {
         id: 'test-project',
       };
-      flush();
+      await flush();
       element.shadowRoot.querySelector('#editInheritFromInput').dispatchEvent(
           new CustomEvent('commit', {
             detail: {},
@@ -369,10 +393,11 @@
             detail: {},
             composed: true, bubbles: true,
           }));
+      await flush();
       assert.isTrue(element._handleAccessModified.called);
     });
 
-    test('_handleSaveForReview', () => {
+    test('_handleSaveForReview', async () => {
       const saveStub =
           stubRestApi('setRepoAccessRightsForReview');
       sinon.stub(element, '_computeAddAndRemove').returns({
@@ -380,6 +405,7 @@
         remove: {},
       });
       element._handleSaveForReview();
+      await flush();
       assert.isFalse(saveStub.called);
     });
 
@@ -487,29 +513,32 @@
       assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}});
     });
 
-    test('_handleSaveForReview parent change', () => {
+    test('_handleSaveForReview parent change', async () => {
       element._inheritsFrom = {
         id: 'test-project',
       };
       element._originalInheritsFrom = {
         id: 'test-project-original',
       };
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), {
         parent: 'test-project', add: {}, remove: {},
       });
     });
 
-    test('_handleSaveForReview new parent with spaces', () => {
+    test('_handleSaveForReview new parent with spaces', async () => {
       element._inheritsFrom = {id: 'spaces+in+project+name'};
       element._originalInheritsFrom = {id: 'old-project'};
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), {
         parent: 'spaces in project name', add: {}, remove: {},
       });
     });
 
-    test('_handleSaveForReview rules', () => {
+    test('_handleSaveForReview rules', async () => {
       // Delete a rule.
       element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+      await flush();
       let expectedInput = {
         add: {},
         remove: {
@@ -531,6 +560,7 @@
 
       // Modify a rule.
       element._local['refs/*'].permissions.owner.rules[123].modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/*': {
@@ -558,7 +588,7 @@
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
     });
 
-    test('_computeAddAndRemove permissions', () => {
+    test('_computeAddAndRemove permissions', async () => {
       // Add a new rule to a permission.
       let expectedInput = {
         add: {
@@ -584,7 +614,7 @@
           ._handleAddRuleItem(
               {detail: {value: 'Maintainers'}});
 
-      flush();
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Remove the added rule.
@@ -592,6 +622,7 @@
 
       // Delete a permission.
       element._local['refs/*'].permissions.owner.deleted = true;
+      await flush();
       expectedInput = {
         add: {},
         remove: {
@@ -609,6 +640,7 @@
 
       // Modify a permission.
       element._local['refs/*'].permissions.owner.modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/*': {
@@ -634,7 +666,7 @@
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
     });
 
-    test('_computeAddAndRemove sections', () => {
+    test('_computeAddAndRemove sections', async () => {
       // Add a new permission to a section
       let expectedInput = {
         add: {
@@ -652,7 +684,7 @@
       };
       element.shadowRoot
           .querySelector('gr-access-section')._handleAddPermission();
-      flush();
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Add a new rule to the new permission.
@@ -683,11 +715,13 @@
               'gr-permission')[2];
       newPermission._handleAddRuleItem(
           {detail: {value: 'Maintainers'}});
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Modify a section reference.
       element._local['refs/*'].updatedId = 'refs/for/bar';
       element._local['refs/*'].modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/for/bar': {
@@ -726,10 +760,12 @@
           },
         },
       };
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Delete a section.
       element._local['refs/*'].deleted = true;
+      await flush();
       expectedInput = {
         add: {},
         remove: {
@@ -741,7 +777,7 @@
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
     });
 
-    test('_computeAddAndRemove new section', () => {
+    test('_computeAddAndRemove new section', async () => {
       // Add a new permission to a section
       let expectedInput = {
         add: {
@@ -753,6 +789,7 @@
         remove: {},
       };
       MockInteractions.tap(element.$.addReferenceBtn);
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       expectedInput = {
@@ -773,7 +810,7 @@
       const newSection = dom(element.root)
           .querySelectorAll('gr-access-section')[1];
       newSection._handleAddPermission();
-      flush();
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Add rule to the new permission.
@@ -803,12 +840,12 @@
       newSection.shadowRoot
           .querySelector('gr-permission')._handleAddRuleItem(
               {detail: {value: 'Maintainers'}});
-
-      flush();
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Modify a the reference from the default value.
       element._local['refs/for/*'].updatedId = 'refs/for/new';
+      await flush();
       expectedInput = {
         add: {
           'refs/for/new': {
@@ -835,10 +872,11 @@
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
     });
 
-    test('_computeAddAndRemove combinations', () => {
+    test('_computeAddAndRemove combinations', async () => {
       // Modify rule and delete permission that it is inside of.
       element._local['refs/*'].permissions.owner.rules[123].modified = true;
       element._local['refs/*'].permissions.owner.deleted = true;
+      await flush();
       let expectedInput = {
         add: {},
         remove: {
@@ -853,10 +891,12 @@
       // Delete rule and delete permission that it is inside of.
       element._local['refs/*'].permissions.owner.rules[123].modified = false;
       element._local['refs/*'].permissions.owner.rules[123].deleted = true;
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Also modify a different rule inside of another permission.
       element._local['refs/*'].permissions.read.modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/*': {
@@ -886,6 +926,7 @@
       element._local['refs/*'].permissions.owner.modified = true;
       element._local['refs/*'].permissions.read.exclusive = true;
       element._local['refs/*'].permissions.read.modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/*': {
@@ -918,6 +959,7 @@
               'gr-permission')[1];
       readPermission._handleAddRuleItem(
           {detail: {value: 'Maintainers'}});
+      await flush();
 
       expectedInput = {
         add: {
@@ -948,6 +990,7 @@
       // Change one of the refs
       element._local['refs/*'].updatedId = 'refs/for/bar';
       element._local['refs/*'].modified = true;
+      await flush();
 
       expectedInput = {
         add: {
@@ -983,6 +1026,7 @@
         },
       };
       element._local['refs/*'].deleted = true;
+      await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
       // Add a new section.
@@ -990,12 +1034,13 @@
       let newSection = dom(element.root)
           .querySelectorAll('gr-access-section')[1];
       newSection._handleAddPermission();
-      flush();
+      await flush();
       newSection.shadowRoot
           .querySelector('gr-permission')._handleAddRuleItem(
               {detail: {value: 'Maintainers'}});
       // Modify a the reference from the default value.
       element._local['refs/for/*'].updatedId = 'refs/for/new';
+      await flush();
 
       expectedInput = {
         add: {
@@ -1029,6 +1074,7 @@
       // Modify newly added rule inside new ref.
       element._local['refs/for/*'].permissions['label-Code-Review'].
           rules['Maintainers'].modified = true;
+      await flush();
       expectedInput = {
         add: {
           'refs/for/new': {
@@ -1061,15 +1107,17 @@
 
       // Add a second new section.
       MockInteractions.tap(element.$.addReferenceBtn);
+      await flush();
       newSection = dom(element.root)
           .querySelectorAll('gr-access-section')[2];
       newSection._handleAddPermission();
-      flush();
+      await flush();
       newSection.shadowRoot
           .querySelector('gr-permission')._handleAddRuleItem(
               {detail: {value: 'Maintainers'}});
       // Modify a the reference from the default value.
       element._local['refs/for/**'].updatedId = 'refs/for/new2';
+      await flush();
       expectedInput = {
         add: {
           'refs/for/new': {
@@ -1119,20 +1167,23 @@
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
     });
 
-    test('Unsaved added refs are discarded when edit cancelled', () => {
+    test('Unsaved added refs are discarded when edit cancelled', async () => {
       // Unsaved changes are discarded when editing is cancelled.
       MockInteractions.tap(element.$.editBtn);
+      await flush();
       assert.equal(element._sections.length, 1);
       assert.equal(Object.keys(element._local).length, 1);
       MockInteractions.tap(element.$.addReferenceBtn);
+      await flush();
       assert.equal(element._sections.length, 2);
       assert.equal(Object.keys(element._local).length, 2);
       MockInteractions.tap(element.$.editBtn);
+      await flush();
       assert.equal(element._sections.length, 1);
       assert.equal(Object.keys(element._local).length, 1);
     });
 
-    test('_handleSave', done => {
+    test('_handleSave', async () => {
       const repoAccessInput = {
         add: {
           'refs/*': {
@@ -1170,16 +1221,15 @@
 
       element._modified = true;
       MockInteractions.tap(element.$.saveBtn);
+      await flush();
       assert.equal(element.$.saveBtn.hasAttribute('loading'), true);
       resolver({_number: 1});
-      flush(() => {
-        assert.isTrue(saveStub.called);
-        assert.isTrue(GerritNav.navigateToChange.notCalled);
-        done();
-      });
+      await flush();
+      assert.isTrue(saveStub.called);
+      assert.isTrue(GerritNav.navigateToChange.notCalled);
     });
 
-    test('_handleSaveForReview', done => {
+    test('_handleSaveForReview', async () => {
       const repoAccessInput = {
         add: {
           'refs/*': {
@@ -1217,14 +1267,13 @@
 
       element._modified = true;
       MockInteractions.tap(element.$.saveReviewBtn);
+      await flush();
       assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true);
       resolver({_number: 1});
-      flush(() => {
-        assert.isTrue(saveForReviewStub.called);
-        assert.isTrue(GerritNav.navigateToChange
-            .lastCall.calledWithExactly({_number: 1}));
-        done();
-      });
+      await flush();
+      assert.isTrue(saveForReviewStub.called);
+      assert.isTrue(GerritNav.navigateToChange
+          .lastCall.calledWithExactly({_number: 1}));
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
index 98f75ce..4cc0a80 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.ts
@@ -15,16 +15,15 @@
  * limitations under the License.
  */
 
-import '../../../styles/shared-styles';
-import {flush} from '@polymer/polymer/lib/legacy/polymer.dom';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-repo-dashboards_html';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property} from '@polymer/decorators';
 import {RepoName, DashboardId, DashboardInfo} from '../../../types/common';
 import {firePageError} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {tableStyles} from '../../../styles/gr-table-styles';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property, PropertyValues} from 'lit-element';
 
 interface DashboardRef {
   section: string;
@@ -32,12 +31,8 @@
 }
 
 @customElement('gr-repo-dashboards')
-export class GrRepoDashboards extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
-  @property({type: String, observer: '_repoChanged'})
+export class GrRepoDashboards extends GrLitElement {
+  @property({type: String})
   repo?: RepoName;
 
   @property({type: Boolean})
@@ -48,7 +43,85 @@
 
   private readonly restApiService = appContext.restApiService;
 
-  _repoChanged(repo?: RepoName) {
+  static get styles() {
+    return [
+      sharedStyles,
+      tableStyles,
+      css`
+        :host {
+          display: block;
+          margin-bottom: var(--spacing-xxl);
+        }
+        .loading #dashboards,
+        #loadingContainer {
+          display: none;
+        }
+        .loading #loadingContainer {
+          display: block;
+        }
+      `,
+    ];
+  }
+
+  render() {
+    return html` <table
+      id="list"
+      class="genericList ${this._computeLoadingClass(this._loading)}"
+    >
+      <tbody>
+        <tr class="headerRow">
+          <th class="topHeader">Dashboard name</th>
+          <th class="topHeader">Dashboard title</th>
+          <th class="topHeader">Dashboard description</th>
+          <th class="topHeader">Inherited from</th>
+          <th class="topHeader">Default</th>
+        </tr>
+        <tr id="loadingContainer">
+          <td>Loading...</td>
+        </tr>
+      </tbody>
+      <tbody id="dashboards">
+        ${(this._dashboards ?? []).map(
+          item => html`
+            <tr class="groupHeader">
+              <td colspan="5">${item.section}</td>
+            </tr>
+            ${(item.dashboards ?? []).map(
+              info => html`
+                <tr class="table">
+                  <td class="name">
+                    <a href="${this._getUrl(info.project, info.id)}"
+                      >${info.path}</a
+                    >
+                  </td>
+                  <td class="title">${info.title}</td>
+                  <td class="desc">${info.description}</td>
+                  <td class="inherited">
+                    ${this._computeInheritedFrom(
+                      info.project,
+                      info.defining_project
+                    )}
+                  </td>
+                  <td class="default">
+                    ${this._computeIsDefault(info.is_default)}
+                  </td>
+                </tr>
+              `
+            )}
+          `
+        )}
+      </tbody>
+    </table>`;
+  }
+
+  updated(changedProperties: PropertyValues) {
+    if (changedProperties.has('repo')) {
+      this.repoChanged();
+    }
+  }
+
+  private repoChanged() {
+    const repo = this.repo;
     this._loading = true;
     if (!repo) {
       return Promise.resolve();
@@ -89,7 +162,6 @@
 
         this._dashboards = dashboardBuilder;
         this._loading = false;
-        flush();
       });
   }
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts
deleted file mode 100644
index 6657a20..0000000
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    :host {
-      display: block;
-      margin-bottom: var(--spacing-xxl);
-    }
-    .loading #dashboards,
-    #loadingContainer {
-      display: none;
-    }
-    .loading #loadingContainer {
-      display: block;
-    }
-  </style>
-  <style include="gr-table-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <table id="list" class$="genericList [[_computeLoadingClass(_loading)]]">
-    <tbody>
-      <tr class="headerRow">
-        <th class="topHeader">Dashboard name</th>
-        <th class="topHeader">Dashboard title</th>
-        <th class="topHeader">Dashboard description</th>
-        <th class="topHeader">Inherited from</th>
-        <th class="topHeader">Default</th>
-      </tr>
-      <tr id="loadingContainer">
-        <td>Loading...</td>
-      </tr>
-    </tbody>
-    <tbody id="dashboards">
-      <template is="dom-repeat" items="[[_dashboards]]">
-        <tr class="groupHeader">
-          <td colspan="5">[[item.section]]</td>
-        </tr>
-        <template is="dom-repeat" items="[[item.dashboards]]" as="info">
-          <tr class="table">
-            <td class="name">
-              <a href$="[[_getUrl(info.project, info.id)]]">[[info.path]]</a>
-            </td>
-            <td class="title">[[info.title]]</td>
-            <td class="desc">[[info.description]]</td>
-            <td class="inherited">
-              [[_computeInheritedFrom(info.project, info.defining_project)]]
-            </td>
-            <td class="default">[[_computeIsDefault(info.is_default)]]</td>
-          </tr>
-        </template>
-      </template>
-    </tbody>
-  </table>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
index 8c50db2..ede2bb9 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.ts
@@ -19,7 +19,11 @@
 import './gr-repo-dashboards';
 import {GrRepoDashboards} from './gr-repo-dashboards';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {addListenerForTest, stubRestApi} from '../../../test/test-utils';
+import {
+  addListenerForTest,
+  queryAndAssert,
+  stubRestApi,
+} from '../../../test/test-utils';
 import {DashboardId, DashboardInfo, RepoName} from '../../../types/common';
 import {PageErrorEvent} from '../../../types/events.js';
 
@@ -28,8 +32,9 @@
 suite('gr-repo-dashboards tests', () => {
   let element: GrRepoDashboards;
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await flush();
   });
 
   suite('dashboard table', () => {
@@ -90,17 +95,24 @@
     test('loading, sections, and ordering', done => {
       assert.isTrue(element._loading);
       assert.notEqual(
-        getComputedStyle(element.$.loadingContainer).display,
+        getComputedStyle(queryAndAssert(element, '#loadingContainer')).display,
         'none'
       );
-      assert.equal(getComputedStyle(element.$.dashboards).display, 'none');
+      assert.equal(
+        getComputedStyle(queryAndAssert(element, '#dashboards')).display,
+        'none'
+      );
       element.repo = 'test' as RepoName;
       flush(() => {
         assert.equal(
-          getComputedStyle(element.$.loadingContainer).display,
+          getComputedStyle(queryAndAssert(element, '#loadingContainer'))
+            .display,
           'none'
         );
-        assert.notEqual(getComputedStyle(element.$.dashboards).display, 'none');
+        assert.notEqual(
+          getComputedStyle(queryAndAssert(element, '#dashboards')).display,
+          'none'
+        );
 
         const dashboard = element._dashboards!;
         assert.equal(dashboard.length!, 2);
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
index cc2f650..796b35d 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.ts
@@ -14,13 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../styles/shared-styles';
+
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-commit-info_html';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
-import {customElement, property, computed} from '@polymer/decorators';
 import {ChangeInfo, CommitInfo, ServerInfo} from '../../../types/common';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property} from 'lit-element';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,11 +29,7 @@
 }
 
 @customElement('gr-commit-info')
-export class GrCommitInfo extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrCommitInfo extends GrLitElement {
   // TODO(TS): can not use `?` here as @computed require dependencies as
   // not optional
   @property({type: Object})
@@ -47,7 +43,48 @@
   @property({type: Object})
   serverConfig: ServerInfo | undefined;
 
-  @computed('change', 'commitInfo', 'serverConfig')
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        .container {
+          align-items: center;
+          display: flex;
+        }
+      `,
+    ];
+  }
+
+  render() {
+    return html` <div class="container">
+      <a
+        target="_blank"
+        rel="noopener"
+        href="${this.computeCommitLink(
+          this._webLink,
+          this.change,
+          this.commitInfo,
+          this.serverConfig
+        )}"
+        >${this._computeShortHash(
+          this.change,
+          this.commitInfo,
+          this.serverConfig
+        )}</a
+      >
+      <gr-copy-clipboard
+        hasTooltip=""
+        .buttonTitle="Copy full SHA to clipboard"
+        hideInput=""
+        .text="${this.commitInfo?.commit}"
+      >
+      </gr-copy-clipboard>
+    </div>`;
+  }
+
+  /**
+   * Used only within the tests.
+   */
   get _showWebLink(): boolean {
     if (!this.change || !this.commitInfo || !this.serverConfig) {
       return false;
@@ -61,7 +98,6 @@
     return !!weblink && !!weblink.url;
   }
 
-  @computed('change', 'commitInfo', 'serverConfig')
   get _webLink(): string | undefined {
     if (!this.change || !this.commitInfo || !this.serverConfig) {
       return '';
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
deleted file mode 100644
index 02fa090..0000000
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    .container {
-      align-items: center;
-      display: flex;
-    }
-  </style>
-  <div class="container">
-    <a
-      target="_blank"
-      rel="noopener"
-      href$="[[computeCommitLink(_webLink, change, commitInfo, serverConfig)]]"
-      >[[_computeShortHash(change, commitInfo, serverConfig)]]</a
-    >
-    <gr-copy-clipboard
-      hasTooltip=""
-      buttonTitle="Copy full SHA to clipboard"
-      hideInput=""
-      text="[[commitInfo.commit]]"
-    >
-    </gr-copy-clipboard>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js
deleted file mode 100644
index ffaed23..0000000
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * @license
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import '../../../test/common-test-setup-karma.js';
-import '../../core/gr-router/gr-router.js';
-import './gr-commit-info.js';
-import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
-
-const basicFixture = fixtureFromElement('gr-commit-info');
-
-suite('gr-commit-info tests', () => {
-  let element;
-
-  setup(() => {
-    element = basicFixture.instantiate();
-  });
-
-  test('weblinks use GerritNav interface', () => {
-    const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
-        .returns([{name: 'stubb', url: '#s'}]);
-    element.change = {};
-    element.commitInfo = {};
-    element.serverConfig = {};
-    assert.isTrue(weblinksStub.called);
-  });
-
-  test('no web link when unavailable', () => {
-    element.commitInfo = {};
-    element.serverConfig = {};
-    element.change = {labels: [], project: ''};
-
-    assert.isNotOk(element._showWebLink);
-  });
-
-  test('use web link when available', () => {
-    const router = document.createElement('gr-router');
-    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
-        router._generateWeblinks.bind(router));
-
-    element.change = {labels: [], project: ''};
-    element.commitInfo =
-        {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]};
-    element.serverConfig = {};
-
-    assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'link-url');
-  });
-
-  test('does not relativize web links that begin with scheme', () => {
-    const router = document.createElement('gr-router');
-    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
-        router._generateWeblinks.bind(router));
-
-    element.change = {labels: [], project: ''};
-    element.commitInfo = {
-      commit: 'commitsha',
-      web_links: [{name: 'gitweb', url: 'https://link-url'}],
-    };
-    element.serverConfig = {};
-
-    assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'https://link-url');
-  });
-
-  test('ignore web links that are neither gitweb nor gitiles', () => {
-    const router = document.createElement('gr-router');
-    sinon.stub(GerritNav, '_generateWeblinks').callsFake(
-        router._generateWeblinks.bind(router));
-
-    element.change = {project: 'project-name'};
-    element.commitInfo = {
-      commit: 'commit-sha',
-      web_links: [
-        {
-          name: 'ignore',
-          url: 'ignore',
-        },
-        {
-          name: 'gitiles',
-          url: 'https://link-url',
-        },
-      ],
-    };
-    element.serverConfig = {};
-
-    assert.isOk(element._showWebLink);
-    assert.equal(element._webLink, 'https://link-url');
-
-    // Remove gitiles link.
-    element.commitInfo = {
-      commit: 'commit-sha',
-      web_links: [
-        {
-          name: 'ignore',
-          url: 'ignore',
-        },
-      ],
-    };
-    assert.isNotOk(element._showWebLink);
-    assert.isNotOk(element._webLink);
-  });
-});
-
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
new file mode 100644
index 0000000..cb6c9e4
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.ts
@@ -0,0 +1,134 @@
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma';
+import '../../core/gr-router/gr-router';
+import './gr-commit-info';
+import {GrCommitInfo} from './gr-commit-info';
+import {GerritNav} from '../../core/gr-navigation/gr-navigation';
+import {
+  createChange,
+  createCommit,
+  createServerInfo,
+} from '../../../test/test-data-generators';
+import {CommitId, RepoName} from '../../../types/common';
+
+const basicFixture = fixtureFromElement('gr-commit-info');
+
+suite('gr-commit-info tests', () => {
+  let element: GrCommitInfo;
+
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+
+  test('weblinks use GerritNav interface', async () => {
+    const weblinksStub = sinon
+      .stub(GerritNav, '_generateWeblinks')
+      .returns([{name: 'stubb', url: '#s'}]);
+    element.change = createChange();
+    element.commitInfo = createCommit();
+    element.serverConfig = createServerInfo();
+    await flush();
+    assert.isTrue(weblinksStub.called);
+  });
+
+  test('no web link when unavailable', () => {
+    element.commitInfo = createCommit();
+    element.serverConfig = createServerInfo();
+    element.change = {...createChange(), labels: {}, project: '' as RepoName};
+
+    assert.isNotOk(element._showWebLink);
+  });
+
+  test('use web link when available', () => {
+    const router = document.createElement('gr-router');
+    sinon
+      .stub(GerritNav, '_generateWeblinks')
+      .callsFake(router._generateWeblinks.bind(router));
+
+    element.change = {...createChange(), labels: {}, project: '' as RepoName};
+    element.commitInfo = {
+      ...createCommit(),
+      commit: 'commitsha' as CommitId,
+      web_links: [{name: 'gitweb', url: 'link-url'}],
+    };
+    element.serverConfig = createServerInfo();
+
+    assert.isOk(element._showWebLink);
+    assert.equal(element._webLink, 'link-url');
+  });
+
+  test('does not relativize web links that begin with scheme', () => {
+    const router = document.createElement('gr-router');
+    sinon
+      .stub(GerritNav, '_generateWeblinks')
+      .callsFake(router._generateWeblinks.bind(router));
+
+    element.change = {...createChange(), labels: {}, project: '' as RepoName};
+    element.commitInfo = {
+      ...createCommit(),
+      commit: 'commitsha' as CommitId,
+      web_links: [{name: 'gitweb', url: 'https://link-url'}],
+    };
+    element.serverConfig = createServerInfo();
+
+    assert.isOk(element._showWebLink);
+    assert.equal(element._webLink, 'https://link-url');
+  });
+
+  test('ignore web links that are neither gitweb nor gitiles', () => {
+    const router = document.createElement('gr-router');
+    sinon
+      .stub(GerritNav, '_generateWeblinks')
+      .callsFake(router._generateWeblinks.bind(router));
+
+    element.change = {...createChange(), project: 'project-name' as RepoName};
+    element.commitInfo = {
+      ...createCommit(),
+      commit: 'commit-sha' as CommitId,
+      web_links: [
+        {
+          name: 'ignore',
+          url: 'ignore',
+        },
+        {
+          name: 'gitiles',
+          url: 'https://link-url',
+        },
+      ],
+    };
+    element.serverConfig = createServerInfo();
+
+    assert.isOk(element._showWebLink);
+    assert.equal(element._webLink, 'https://link-url');
+
+    // Remove gitiles link.
+    element.commitInfo = {
+      ...createCommit(),
+      commit: 'commit-sha' as CommitId,
+      web_links: [
+        {
+          name: 'ignore',
+          url: 'ignore',
+        },
+      ],
+    };
+    assert.isNotOk(element._showWebLink);
+    assert.isNotOk(element._webLink);
+  });
+});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index bd8eaac..95169ca 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -19,26 +19,19 @@
 import '../../shared/gr-dialog/gr-dialog';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param';
-import '../../../styles/shared-styles';
 import '../gr-thread-list/gr-thread-list';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-confirm-submit-dialog_html';
-import {customElement, property} from '@polymer/decorators';
 import {ChangeInfo, ActionInfo} from '../../../types/common';
 import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
 import {pluralize} from '../../../utils/string-util';
 import {CommentThread, isUnresolved} from '../../../utils/comment-util';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {css, customElement, html, property, query} from 'lit-element';
 
-export interface GrConfirmSubmitDialog {
-  $: {
-    dialog: GrDialog;
-  };
-}
 @customElement('gr-confirm-submit-dialog')
-export class GrConfirmSubmitDialog extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
+export class GrConfirmSubmitDialog extends GrLitElement {
+  @query('#dialog')
+  dialog?: GrDialog;
 
   /**
    * Fired when the confirm button is pressed.
@@ -64,12 +57,119 @@
   @property({type: Boolean})
   _initialised = false;
 
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        #dialog {
+          min-width: 40em;
+        }
+        p {
+          margin-bottom: var(--spacing-l);
+        }
+        .warningBeforeSubmit {
+          color: var(--warning-foreground);
+          vertical-align: top;
+          margin-right: var(--spacing-s);
+        }
+        @media screen and (max-width: 50em) {
+          #dialog {
+            min-width: inherit;
+            width: 100%;
+          }
+        }
+      `,
+    ];
+  }
+
+  private renderPrivate() {
+    if (!this.change?.is_private) return '';
+    return html`
+      <p>
+        <iron-icon
+          icon="gr-icons:warning"
+          class="warningBeforeSubmit"
+        ></iron-icon>
+        <strong>Heads Up!</strong>
+        Submitting this private change will also make it public.
+      </p>
+    `;
+  }
+
+  private renderUnresolvedCommentCount() {
+    if (!this.change?.unresolved_comment_count) return '';
+    return html`
+      <p>
+        <iron-icon
+          icon="gr-icons:warning"
+          class="warningBeforeSubmit"
+        ></iron-icon>
+        ${this._computeUnresolvedCommentsWarning(this.change)}
+      </p>
+      <gr-thread-list
+        id="commentList"
+        .threads="${this._computeUnresolvedThreads(this.commentThreads)}"
+        .change="${this.change}"
+        .change-num="${this.change?._number}"
+        logged-in="true"
+        hide-dropdown
+      >
+      </gr-thread-list>
+    `;
+  }
+
+  private renderChangeEdit() {
+    if (!this._computeHasChangeEdit(this.change)) return '';
+    return html`
+      <iron-icon
+        icon="gr-icons:warning"
+        class="warningBeforeSubmit"
+      ></iron-icon>
+      Your unpublished edit will not be submitted. Did you forget to click
+      <b>PUBLISH</b>
+    `;
+  }
+
+  private renderInitialised() {
+    if (!this._initialised) return '';
+    return html`
+      <div class="header" slot="header">${this.action?.label}</div>
+      <div class="main" slot="main">
+        <gr-endpoint-decorator name="confirm-submit-change">
+          <p>Ready to submit “<strong>${this.change?.subject}</strong>”?</p>
+          ${this.renderPrivate()} ${this.renderUnresolvedCommentCount()}
+          ${this.renderChangeEdit()}
+          <gr-endpoint-param
+            name="change"
+            .value="${this.change}"
+          ></gr-endpoint-param>
+          <gr-endpoint-param
+            name="action"
+            .value="${this.action}"
+          ></gr-endpoint-param>
+        </gr-endpoint-decorator>
+      </div>
+    `;
+  }
+
+  render() {
+    return html` <gr-dialog
+      id="dialog"
+      confirm-label="Continue"
+      confirm-on-enter=""
+      @cancel=${this._handleCancelTap}
+      @confirm=${this._handleConfirmTap}
+    >
+      ${this.renderInitialised()}
+    </gr-dialog>`;
+  }
+
   init() {
     this._initialised = true;
   }
 
   resetFocus() {
-    this.$.dialog.resetFocus();
+    this.dialog?.resetFocus();
   }
 
   _computeHasChangeEdit(change?: ChangeInfo) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
deleted file mode 100644
index 5f99ee6..0000000
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    #dialog {
-      min-width: 40em;
-    }
-    p {
-      margin-bottom: var(--spacing-l);
-    }
-    .warningBeforeSubmit {
-      color: var(--warning-foreground);
-      vertical-align: top;
-      margin-right: var(--spacing-s);
-    }
-    @media screen and (max-width: 50em) {
-      #dialog {
-        min-width: inherit;
-        width: 100%;
-      }
-    }
-  </style>
-  <gr-dialog
-    id="dialog"
-    confirm-label="Continue"
-    confirm-on-enter=""
-    on-cancel="_handleCancelTap"
-    on-confirm="_handleConfirmTap"
-  >
-    <template is="dom-if" if="[[_initialised]]">
-      <div class="header" slot="header">[[action.label]]</div>
-      <div class="main" slot="main">
-        <gr-endpoint-decorator name="confirm-submit-change">
-          <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p>
-          <template is="dom-if" if="[[change.is_private]]">
-            <p>
-              <iron-icon
-                icon="gr-icons:warning"
-                class="warningBeforeSubmit"
-              ></iron-icon>
-              <strong>Heads Up!</strong>
-              Submitting this private change will also make it public.
-            </p>
-          </template>
-          <template is="dom-if" if="[[change.unresolved_comment_count]]">
-            <p>
-              <iron-icon
-                icon="gr-icons:warning"
-                class="warningBeforeSubmit"
-              ></iron-icon>
-              [[_computeUnresolvedCommentsWarning(change)]]
-            </p>
-            <gr-thread-list
-              id="commentList"
-              threads="[[_computeUnresolvedThreads(commentThreads)]]"
-              change="[[change]]"
-              change-num="[[change._number]]"
-              logged-in="true"
-              hide-dropdown
-            >
-            </gr-thread-list>
-          </template>
-          <template is="dom-if" if="[[_computeHasChangeEdit(change)]]">
-            <iron-icon
-              icon="gr-icons:warning"
-              class="warningBeforeSubmit"
-            ></iron-icon>
-            Your unpublished edit will not be submitted. Did you forget to click
-            <b>PUBLISH</b>?
-          </template>
-          <gr-endpoint-param
-            name="change"
-            value="[[change]]"
-          ></gr-endpoint-param>
-          <gr-endpoint-param
-            name="action"
-            value="[[action]]"
-          ></gr-endpoint-param>
-        </gr-endpoint-decorator>
-      </div>
-    </template>
-  </gr-dialog>
-`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
index e9f3019..e1823b1 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.ts
@@ -31,14 +31,14 @@
     element._initialised = true;
   });
 
-  test('display', () => {
+  test('display', async () => {
     element.action = {label: 'my-label'};
     element.change = {
       ...createChange(),
       subject: 'my-subject',
       revisions: {},
     };
-    flush();
+    await flush();
     const header = queryAndAssert(element, '.header');
     assert.equal(header.textContent!.trim(), 'my-label');
 
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
similarity index 71%
rename from polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js
rename to polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
index c109538..283a133 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.ts
@@ -15,25 +15,37 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-included-in-dialog.js';
+import '../../../test/common-test-setup-karma';
+import './gr-included-in-dialog';
+import {GrIncludedInDialog} from './gr-included-in-dialog';
+import {BranchName, IncludedInInfo, TagName} from '../../../types/common';
+import {IronInputElement} from '@polymer/iron-input';
+import {queryAndAssert} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-included-in-dialog');
 
 suite('gr-included-in-dialog', () => {
-  let element;
+  let element: GrIncludedInDialog;
 
   setup(() => {
     element = basicFixture.instantiate();
   });
 
   test('_computeGroups', () => {
-    const includedIn = {branches: [], tags: []};
+    const includedIn = {branches: [], tags: []} as IncludedInInfo;
     let filterText = '';
     assert.deepEqual(element._computeGroups(includedIn, filterText), []);
 
-    includedIn.branches.push('master', 'development', 'stable-2.0');
-    includedIn.tags.push('v1.9', 'v2.0', 'v2.1');
+    includedIn.branches.push(
+      'master' as BranchName,
+      'development' as BranchName,
+      'stable-2.0' as BranchName
+    );
+    includedIn.tags.push(
+      'v1.9' as TagName,
+      'v2.0' as TagName,
+      'v2.1' as TagName
+    );
     assert.deepEqual(element._computeGroups(includedIn, filterText), [
       {title: 'Branches', items: ['master', 'development', 'stable-2.0']},
       {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']},
@@ -65,9 +77,13 @@
   });
 
   test('_computeGroups with .bindValue', done => {
-    element.$.filterInput.bindValue = 'stable-3.2';
-    const includedIn = {branches: [], tags: []};
-    includedIn.branches.push('master', 'stable-3.2');
+    queryAndAssert<IronInputElement>(element, '#filterInput')!.bindValue =
+      'stable-3.2';
+    const includedIn = {branches: [], tags: []} as IncludedInInfo;
+    includedIn.branches.push(
+      'master' as BranchName,
+      'stable-3.2' as BranchName
+    );
 
     setTimeout(() => {
       const filterText = element._filterText;
@@ -79,4 +95,3 @@
     });
   });
 });
-
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
index 1f74b70..3ccc960 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.ts
@@ -363,8 +363,8 @@
       // toast
       let toast = toastSpy.lastCall.returnValue;
       assert.isOk(toast);
-      assert.include(toast.root.textContent, 'Credentials expired.');
-      assert.include(toast.root.textContent, 'Refresh credentials');
+      assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
+      assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
 
       // noInteractionOverlay
       const noInteractionOverlay = element.$.noInteractionOverlay;
@@ -401,7 +401,7 @@
       assert.notStrictEqual(toastSpy.lastCall.returnValue, toast);
       toast = toastSpy.lastCall.returnValue;
       assert.isOk(toast);
-      assert.include(toast.root.textContent, 'Credentials refreshed');
+      assert.include(toast.shadowRoot.textContent, 'Credentials refreshed');
 
       // close overlay
       assert.isTrue(noInteractionOverlayCloseSpy.called);
@@ -421,9 +421,10 @@
           bubbles: true,
         })
       );
+      await flush();
       let toast = toastSpy.lastCall.returnValue;
       assert.isOk(toast);
-      assert.include(toast.root.textContent, 'test reload');
+      assert.include(toast.shadowRoot.textContent, 'test reload');
 
       // fake auth
       fetchStub.returns(Promise.resolve({status: 403}));
@@ -452,11 +453,11 @@
       await flush();
       // toast
       toast = toastSpy.lastCall.returnValue;
-      assert.include(toast.root.textContent, 'Credentials expired.');
-      assert.include(toast.root.textContent, 'Refresh credentials');
+      assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
+      assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
     });
 
-    test('regular toast should dismiss regular toast', () => {
+    test('regular toast should dismiss regular toast', async () => {
       // Set status to AUTHED.
       appContext.authService.authCheck();
 
@@ -468,9 +469,10 @@
           bubbles: true,
         })
       );
+      await flush();
       let toast = toastSpy.lastCall.returnValue;
       assert.isOk(toast);
-      assert.include(toast.root.textContent, 'test reload');
+      assert.include(toast.shadowRoot.textContent, 'test reload');
 
       // new alert
       element.dispatchEvent(
@@ -480,9 +482,9 @@
           bubbles: true,
         })
       );
-
+      await flush();
       toast = toastSpy.lastCall.returnValue;
-      assert.include(toast.root.textContent, 'second-test');
+      assert.include(toast.shadowRoot.textContent, 'second-test');
     });
 
     test('regular toast should not dismiss auth toast', done => {
@@ -513,8 +515,8 @@
         assert.equal(fetchStub.callCount, 2);
         flush(() => {
           let toast = toastSpy.lastCall.returnValue;
-          assert.include(toast.root.textContent, 'Credentials expired.');
-          assert.include(toast.root.textContent, 'Refresh credentials');
+          assert.include(toast.shadowRoot.textContent, 'Credentials expired.');
+          assert.include(toast.shadowRoot.textContent, 'Refresh credentials');
 
           // fake an alert
           element.dispatchEvent(
@@ -530,7 +532,10 @@
           flush(() => {
             toast = toastSpy.lastCall.returnValue;
             assert.isOk(toast);
-            assert.include(toast.root.textContent, 'Credentials expired.');
+            assert.include(
+              toast.shadowRoot.textContent,
+              'Credentials expired.'
+            );
             done();
           });
         });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index 0f8752d..6fe9d27 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -73,19 +73,19 @@
     }
   }
 
-  _computeSideBySideSelected(mode: DiffViewMode) {
+  _computeSideBySideSelected(mode?: DiffViewMode) {
     return mode === DiffViewMode.SIDE_BY_SIDE ? 'selected' : '';
   }
 
-  _computeUnifiedSelected(mode: DiffViewMode) {
+  _computeUnifiedSelected(mode?: DiffViewMode) {
     return mode === DiffViewMode.UNIFIED ? 'selected' : '';
   }
 
-  isSideBySideSelected(mode: DiffViewMode) {
+  isSideBySideSelected(mode?: DiffViewMode) {
     return mode === DiffViewMode.SIDE_BY_SIDE;
   }
 
-  isUnifiedSelected(mode: DiffViewMode) {
+  isUnifiedSelected(mode?: DiffViewMode) {
     return mode === DiffViewMode.UNIFIED;
   }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
index 3ebb58f..4e2b6a1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.ts
@@ -37,7 +37,7 @@
     position-below="[[showTooltipBelow]]"
     class$="[[_computeSideBySideSelected(mode)]]"
     title="Side-by-side diff"
-    aria-pressed="[[isSideBySideSelected(mode)]]"
+    aria-pressed$="[[isSideBySideSelected(mode)]]"
     on-click="_handleSideBySideTap"
   >
     <iron-icon icon="gr-icons:side-by-side"></iron-icon>
@@ -49,7 +49,7 @@
     position-below="[[showTooltipBelow]]"
     title="Unified diff"
     class$="[[_computeUnifiedSelected(mode)]]"
-    aria-pressed="[[isUnifiedSelected(mode)]]"
+    aria-pressed$="[[isUnifiedSelected(mode)]]"
     on-click="_handleUnifiedTap"
   >
     <iron-icon icon="gr-icons:unified"></iron-icon>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
similarity index 65%
rename from polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
rename to polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
index f554227..aa97394 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.ts
@@ -15,29 +15,35 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
-import './gr-diff-mode-selector.js';
-import {DiffViewMode} from '../../../constants/constants.js';
-import {stubRestApi} from '../../../test/test-utils.js';
+import '../../../test/common-test-setup-karma';
+import './gr-diff-mode-selector';
+import {GrDiffModeSelector} from './gr-diff-mode-selector';
+import {DiffViewMode} from '../../../constants/constants';
+import {stubRestApi} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-diff-mode-selector');
 
 suite('gr-diff-mode-selector tests', () => {
-  let element;
+  let element: GrDiffModeSelector;
 
   setup(() => {
     element = basicFixture.instantiate();
   });
 
   test('_computeSelectedClass', () => {
-    assert.equal(element._computeSideBySideSelected(DiffViewMode.SIDE_BY_SIDE),
-        'selected');
-    assert.equal(element._computeSideBySideSelected(DiffViewMode.UNIFIED),
-        '');
-    assert.equal(element._computeUnifiedSelected(DiffViewMode.UNIFIED),
-        'selected');
-    assert.equal(element._computeUnifiedSelected(DiffViewMode.SIDE_BY_SIDE),
-        '');
+    assert.equal(
+      element._computeSideBySideSelected(DiffViewMode.SIDE_BY_SIDE),
+      'selected'
+    );
+    assert.equal(element._computeSideBySideSelected(DiffViewMode.UNIFIED), '');
+    assert.equal(
+      element._computeUnifiedSelected(DiffViewMode.UNIFIED),
+      'selected'
+    );
+    assert.equal(
+      element._computeUnifiedSelected(DiffViewMode.SIDE_BY_SIDE),
+      ''
+    );
   });
 
   test('setMode', () => {
@@ -45,24 +51,23 @@
 
     // Setting the mode initially does not save prefs.
     element.saveOnChange = true;
-    element.setMode('SIDE_BY_SIDE');
+    element.setMode(DiffViewMode.SIDE_BY_SIDE);
     assert.isFalse(saveStub.called);
 
     // Setting the mode to itself does not save prefs.
-    element.setMode('SIDE_BY_SIDE');
+    element.setMode(DiffViewMode.SIDE_BY_SIDE);
     assert.isFalse(saveStub.called);
 
     // Setting the mode to something else does not save prefs if saveOnChange
     // is false.
     element.saveOnChange = false;
-    element.setMode('UNIFIED_DIFF');
+    element.setMode(DiffViewMode.UNIFIED);
     assert.isFalse(saveStub.called);
 
     // Setting the mode to something else does not save prefs if saveOnChange
     // is false.
     element.saveOnChange = true;
-    element.setMode('SIDE_BY_SIDE');
+    element.setMode(DiffViewMode.SIDE_BY_SIDE);
     assert.isTrue(saveStub.calledOnce);
   });
 });
-
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
similarity index 80%
rename from polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
rename to polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
index 07cca9a..5c62e7a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.ts
@@ -15,17 +15,24 @@
  * limitations under the License.
  */
 
-import '../../../test/common-test-setup-karma.js';
+import '../../../test/common-test-setup-karma';
+import './gr-diff-preferences-dialog';
+import {GrDiffPreferencesDialog} from './gr-diff-preferences-dialog';
+import {createDefaultDiffPrefs} from '../../../constants/constants';
+import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions';
 
 const basicFixture = fixtureFromElement('gr-diff-preferences-dialog');
 
 suite('gr-diff-preferences-dialog', () => {
-  let element;
+  let element: GrDiffPreferencesDialog;
+
   setup(() => {
     element = basicFixture.instantiate();
   });
+
   test('changes applies only on save', async () => {
     const originalDiffPrefs = {
+      ...createDefaultDiffPrefs(),
       line_wrapping: true,
     };
     element.diffPrefs = originalDiffPrefs;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
index 4d2576a..008ddb0 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.ts
@@ -16,11 +16,11 @@
  */
 import '../gr-button/gr-button';
 import '../../../styles/shared-styles';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-alert_html';
 import {getRootElement} from '../../../scripts/rootElement';
-import {customElement, property} from '@polymer/decorators';
 import {ErrorType} from '../../../types/types';
+import {GrLitElement} from '../../lit/gr-lit-element';
+import {customElement, property, css, html} from 'lit-element';
+import {sharedStyles} from '../../../styles/shared-styles';
 
 declare global {
   interface HTMLElementTagNameMap {
@@ -29,9 +29,93 @@
 }
 
 @customElement('gr-alert')
-export class GrAlert extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
+export class GrAlert extends GrLitElement {
+  static get styles() {
+    return [
+      sharedStyles,
+      css`
+        /**
+         * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
+         * HOW THEY ARE USED IN THE CODE.
+         */
+        :host([toast]) {
+          background-color: var(--tooltip-background-color);
+          bottom: 1.25rem;
+          border-radius: var(--border-radius);
+          box-shadow: var(--elevation-level-2);
+          color: var(--tooltip-text-color);
+          left: 1.25rem;
+          position: fixed;
+          transform: translateY(5rem);
+          transition: transform var(--gr-alert-transition-duration, 80ms)
+            ease-out;
+          z-index: 1000;
+        }
+        :host([shown]) {
+          transform: translateY(0);
+        }
+        /**
+         * NOTE: To avoid style being overwritten by outside of the shadow DOM
+         * (as outside styles always win), .content-wrapper is introduced as a
+         * wrapper around main content to have better encapsulation, styles that
+         * may be affected by outside should be defined on it.
+         * In this case, \`padding:0px\` is defined in main.css for all elements
+         * with the universal selector: *.
+         */
+        .content-wrapper {
+          padding: var(--spacing-l) var(--spacing-xl);
+        }
+        .text {
+          color: var(--tooltip-text-color);
+          display: inline-block;
+          max-height: 10rem;
+          max-width: 80vw;
+          vertical-align: bottom;
+          word-break: break-all;
+        }
+        .action {
+          color: var(--link-color);
+          font-weight: var(--font-weight-bold);
+          margin-left: var(--spacing-l);
+          text-decoration: none;
+        }
+      `,
+    ];
+  }
+
+  renderDismissButton() {
+    if (!this.showDismiss) return '';
+    return html`<gr-button
+      link=""
+      class="action"
+      @click=${this._handleDismissTap}
+      >Dismiss</gr-button
+    >`;
+  }
+
+  render() {
+    // To pass CSS mixins for @apply to Polymer components, they need to appear
+    // in <style> inside the template.
+    const style = html`<style>
+      .action {
+        --gr-button: {
+          padding: 0;
+        }
+      }
+    </style>`;
+    const {text, actionText} = this;
+    return html`${style}
+      <div class="content-wrapper">
+        <span class="text">${text}</span>
+        <gr-button
+          link=""
+          class="action"
+          ?hidden="${this._hideActionButton}"
+          @click=${this._handleActionTap}
+          >${actionText}
+        </gr-button>
+        ${this.renderDismissButton()}
+      </div> `;
   }
 
   /**
@@ -49,10 +133,10 @@
   @property({type: String})
   type?: ErrorType;
 
-  @property({type: Boolean, reflectToAttribute: true})
+  @property({type: Boolean, reflect: true})
   shown = true;
 
-  @property({type: Boolean, reflectToAttribute: true})
+  @property({type: Boolean, reflect: true})
   toast = true;
 
   @property({type: Boolean})
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
deleted file mode 100644
index bc517a8..0000000
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag';
-
-export const htmlTemplate = html`
-  <style include="shared-styles">
-    /**
-       * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING
-       * HOW THEY ARE USED IN THE CODE.
-       */
-    :host([toast]) {
-      background-color: var(--tooltip-background-color);
-      bottom: 1.25rem;
-      border-radius: var(--border-radius);
-      box-shadow: var(--elevation-level-2);
-      color: var(--tooltip-text-color);
-      left: 1.25rem;
-      position: fixed;
-      transform: translateY(5rem);
-      transition: transform var(--gr-alert-transition-duration, 80ms) ease-out;
-      z-index: 1000;
-    }
-    :host([shown]) {
-      transform: translateY(0);
-    }
-    /**
-       * NOTE: To avoid style being overwritten by outside of the shadow DOM
-       * (as outside styles always win), .content-wrapper is introduced as a
-       * wrapper around main content to have better encapsulation, styles that
-       * may be affected by outside should be defined on it.
-       * In this case, \`padding:0px\` is defined in main.css for all elements
-       * with the universal selector: *.
-       */
-    .content-wrapper {
-      padding: var(--spacing-l) var(--spacing-xl);
-    }
-    .text {
-      color: var(--tooltip-text-color);
-      display: inline-block;
-      max-height: 10rem;
-      max-width: 80vw;
-      vertical-align: bottom;
-      word-break: break-all;
-    }
-    .action {
-      color: var(--link-color);
-      font-weight: var(--font-weight-bold);
-      margin-left: var(--spacing-l);
-      text-decoration: none;
-      --gr-button: {
-        padding: 0;
-      }
-    }
-  </style>
-  <div class="content-wrapper">
-    <span class="text">[[text]]</span>
-    <gr-button
-      link=""
-      class="action"
-      hidden$="[[_hideActionButton]]"
-      on-click="_handleActionTap"
-      >[[actionText]]</gr-button
-    ><template is="dom-if" if="[[showDismiss]]"
-      ><gr-button link="" class="action" on-click="_handleDismissTap"
-        >Dismiss</gr-button
-      ></template
-    >
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
index 3478a9a..d0fe563 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.ts
@@ -33,18 +33,21 @@
     }
   });
 
-  test('show/hide', () => {
+  test('show/hide', async () => {
     assert.isNull(element.parentNode);
     element.show('Alert text');
+    // wait for element to be rendered after being attached to DOM
+    await flush();
     assert.equal(element.parentNode, document.body);
-    element.updateStyles({'--gr-alert-transition-duration': '0ms'});
+    element.style.setProperty('--gr-alert-transition-duration', '0ms');
     element.hide();
     assert.isNull(element.parentNode);
   });
 
-  test('action event', () => {
+  test('action event', async () => {
     const spy = sinon.spy();
     element.show('Alert text');
+    await flush();
     element._actionCallback = spy;
     assert.isFalse(spy.called);
     MockInteractions.tap(element.shadowRoot!.querySelector('.action')!);
diff --git a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
index 08f2e25..024fa2a 100644
--- a/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
+++ b/polygerrit-ui/app/services/gr-auth/gr-auth_impl.ts
@@ -103,6 +103,15 @@
 
     return this.authCheckPromise
       .then(res => {
+        // Make a call that requires loading the body of the request. This makes it so that the browser
+        // can close the request even though callers of this method might only ever read headers.
+        // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
+        try {
+          res.clone().text();
+        } catch (error) {
+          // Ignore error
+        }
+
         // auth-check will return 204 if authed
         // treat the rest as unauthed
         if (res.status === 204) {
@@ -220,7 +229,7 @@
       }
     }
     options.credentials = 'same-origin';
-    return fetch(url, options);
+    return this._ensureBodyLoaded(fetch(url, options));
   }
 
   private _getAccessToken(): Promise<string | null> {
@@ -286,6 +295,22 @@
     if (params.length) {
       url = url + (url.indexOf('?') === -1 ? '?' : '&') + params.join('&');
     }
-    return fetch(url, options);
+    return this._ensureBodyLoaded(fetch(url, options));
+  }
+
+  private _ensureBodyLoaded(response: Promise<Response>): Promise<Response> {
+    return response.then(response => {
+      if (!response.ok) {
+        // Make a call that requires loading the body of the request. This makes it so that the browser
+        // can close the request even though callers of this method might only ever read headers.
+        // See https://stackoverflow.com/questions/45816743/how-to-solve-this-caution-request-is-not-finished-yet-in-chrome
+        try {
+          response.clone().text();
+        } catch (error) {
+          // Ignore error
+        }
+      }
+      return response;
+    });
   }
 }
diff --git a/polygerrit-ui/app/styles/gr-table-styles.ts b/polygerrit-ui/app/styles/gr-table-styles.ts
index 09d1161..d92bf55 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.ts
+++ b/polygerrit-ui/app/styles/gr-table-styles.ts
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {css} from 'lit-element';
+
 // Mark the file as a module. Otherwise typescript assumes this is a script
 // and $_documentContainer is a global variable.
 // See: https://www.typescriptlang.org/docs/handbook/modules.html
@@ -22,95 +24,99 @@
 
 const $_documentContainer = document.createElement('template');
 
+export const tableStyles = css`
+  .genericList {
+    background-color: var(--background-color-primary);
+    border-collapse: collapse;
+    width: 100%;
+  }
+  .genericList th,
+  .genericList td {
+    padding: var(--spacing-m) 0;
+    vertical-align: middle;
+  }
+  .genericList tr {
+    border-bottom: 1px solid var(--border-color);
+  }
+  .genericList tr:hover {
+    background-color: var(--hover-background-color);
+  }
+  .genericList th {
+    white-space: nowrap;
+  }
+  .genericList th,
+  .genericList td {
+    padding-right: var(--spacing-l);
+  }
+  .genericList tr th:first-of-type,
+  .genericList tr td:first-of-type {
+    padding-left: var(--spacing-l);
+  }
+  .genericList tr:first-of-type {
+    border-top: 1px solid var(--border-color);
+  }
+  .genericList tr th:last-of-type,
+  .genericList tr td:last-of-type {
+    border-left: 1px solid var(--border-color);
+    text-align: center;
+    padding-left: var(--spacing-l);
+  }
+  .genericList tr th.delete,
+  .genericList tr td.delete {
+    padding-top: 0;
+    padding-bottom: 0;
+  }
+  .genericList tr th.delete,
+  .genericList tr td.delete,
+  .genericList tr.loadingMsg td,
+  .genericList tr.groupHeader td {
+    border-left: none;
+  }
+  .genericList .loading {
+    border: none;
+    display: none;
+  }
+  .genericList td {
+    flex-shrink: 0;
+  }
+  .genericList .topHeader,
+  .genericList .groupHeader {
+    color: var(--primary-text-color);
+    font-weight: var(--font-weight-bold);
+    text-align: left;
+    vertical-align: middle;
+  }
+  .genericList .groupHeader {
+    background-color: var(--background-color-secondary);
+    font-family: var(--header-font-family);
+    font-size: var(--font-size-h3);
+    font-weight: var(--font-weight-h3);
+    line-height: var(--line-height-h3);
+  }
+  .genericList a {
+    color: var(--primary-text-color);
+    text-decoration: none;
+  }
+  .genericList a:hover {
+    text-decoration: underline;
+  }
+  .genericList .description {
+    width: 99%;
+  }
+  .genericList .loadingMsg {
+    color: var(--deemphasized-text-color);
+    display: block;
+    padding: var(--spacing-s) var(--spacing-l);
+  }
+  .genericList .loadingMsg:not(.loading) {
+    display: none;
+  }
+`;
+
 $_documentContainer.innerHTML = `<dom-module id="gr-table-styles">
   <template>
     <style>
-      .genericList {
-        background-color: var(--background-color-primary);
-        border-collapse: collapse;
-        width: 100%;
-      }
-      .genericList th,
-      .genericList td {
-        padding: var(--spacing-m) 0;
-        vertical-align: middle;
-      }
-      .genericList tr {
-        border-bottom: 1px solid var(--border-color);
-      }
-      .genericList tr:hover {
-        background-color: var(--hover-background-color);
-      }
-      .genericList th {
-        white-space: nowrap;
-      }
-      .genericList th,
-      .genericList td {
-        padding-right: var(--spacing-l);
-      }
-      .genericList tr th:first-of-type,
-      .genericList tr td:first-of-type {
-        padding-left: var(--spacing-l);
-      }
-      .genericList tr:first-of-type {
-        border-top: 1px solid var(--border-color);
-      }
-      .genericList tr th:last-of-type,
-      .genericList tr td:last-of-type {
-        border-left: 1px solid var(--border-color);
-        text-align: center;
-        padding-left: var(--spacing-l);
-      }
-      .genericList tr th.delete,
-      .genericList tr td.delete {
-        padding-top: 0;
-        padding-bottom: 0;
-      }
-      .genericList tr th.delete,
-      .genericList tr td.delete,
-      .genericList tr.loadingMsg td,
-      .genericList tr.groupHeader td {
-        border-left: none;
-      }
-      .genericList .loading {
-        border: none;
-        display: none;
-      }
-      .genericList td {
-        flex-shrink: 0;
-      }
-      .genericList .topHeader,
-      .genericList .groupHeader {
-        color: var(--primary-text-color);
-        font-weight: var(--font-weight-bold);
-        text-align: left;
-        vertical-align: middle
-      }
-      .genericList .groupHeader {
-        background-color: var(--background-color-secondary);
-        font-family: var(--header-font-family);
-        font-size: var(--font-size-h3);
-        font-weight: var(--font-weight-h3);
-        line-height: var(--line-height-h3);
-      }
-      .genericList a {
-        color: var(--primary-text-color);
-        text-decoration: none;
-      }
-      .genericList a:hover {
-        text-decoration: underline;
-      }
-      .genericList .description {
-        width: 99%;
-      }
-      .genericList .loadingMsg {
-        color: var(--deemphasized-text-color);
-        display: block;
-        padding: var(--spacing-s) var(--spacing-l);
-      }
-      .genericList .loadingMsg:not(.loading) {
-        display: none;
-      }
+    ${tableStyles.cssText}
     </style>
   </template>
 </dom-module>`;