Merge changes from topic "show_all_owners"

* changes:
  Do not place all owners on a new line if number of all owners is small
  Pause/resume owners loading when switching between different owners
  Add "Show all owners" option
diff --git a/ui/code-owners-api.js b/ui/code-owners-api.js
new file mode 100644
index 0000000..408428b
--- /dev/null
+++ b/ui/code-owners-api.js
@@ -0,0 +1,183 @@
+/**
+ * @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.
+ */
+
+
+// TODO: Try to remove it. The ResponseError and getErrorMessage duplicates
+// code from the gr-plugin-rest-api.ts. This code is required because
+// we want custom error processing in some functions. For details see
+// the original gr-plugin-rest-api.ts file/
+
+class ResponseError extends Error {
+  constructor(response) {
+    super();
+    this.response = response;
+  }
+}
+
+async function getErrorMessage(response) {
+  const text = await response.text();
+  return text ?
+    `${response.status}: ${text}` :
+    `${response.status}`;
+}
+
+/**
+ * Responsible for communicating with the rest-api
+ *
+ * @see resources/Documentation/rest-api.md
+ */
+export class CodeOwnersApi {
+  constructor(restApi) {
+    this.restApi = restApi;
+  }
+
+  /**
+   * Returns a promise fetching the owner statuses for all files within the change.
+   *
+   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#change-endpoints
+   * @param {string} changeId
+   */
+  listOwnerStatus(changeId) {
+    return this.restApi.get(`/changes/${changeId}/code_owners.status`);
+  }
+
+  /**
+   * Returns a promise fetching the owners for a given path.
+   *
+   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch
+   * @param {string} changeId
+   * @param {string} path
+   */
+  listOwnersForPath(changeId, path, limit) {
+    return this.restApi.get(
+        `/changes/${changeId}/revisions/current/code_owners` +
+        `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS`
+    );
+  }
+
+  /**
+   * Returns a promise fetching the owners config for a given path.
+   *
+   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#branch-endpoints
+   * @param {string} project
+   * @param {string} branch
+   * @param {string} path
+   */
+  getConfigForPath(project, branch, path) {
+    return this.restApi.get(
+        `/projects/${encodeURIComponent(project)}/` +
+        `branches/${encodeURIComponent(branch)}/` +
+        `code_owners.config/${encodeURIComponent(path)}`
+    );
+  }
+
+  /**
+   * Returns a promise fetching the owners config for a given branch.
+   *
+   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#branch-endpoints
+   * @param {string} project
+   * @param {string} branch
+   */
+  async getBranchConfig(project, branch) {
+    const errFn = (response, error) => {
+      if (error) throw error;
+      if (response) throw new ResponseError(response);
+      throw new Error('Generic REST API error');
+    }
+    try {
+      const config = await this.restApi.send(
+          'GET',
+          `/projects/${encodeURIComponent(project)}/` +
+          `branches/${encodeURIComponent(branch)}/` +
+          `code_owners.branch_config`,
+          undefined,
+          errFn
+      );
+      if (config.override_approval && !(config.override_approval
+          instanceof Array)) {
+        // In the upcoming backend changes, the override_approval will be changed
+        // to array with (possible) multiple items.
+        // While this transition is in progress, the frontend supports both API -
+        // the old one and the new one.
+        return {...config, override_approval: [config.override_approval]};
+      }
+      return config;
+    } catch(err) {
+      if (err instanceof ResponseError) {
+        if (err.response.status === 404) {
+          // The 404 error means that the branch doesn't exist and
+          // the plugin should be disabled.
+          return {disabled: true};
+        }
+        return getErrorMessage(err.response).then(msg => {
+          throw new Error(msg);
+        });
+      }
+      throw err;
+    }
+  }
+}
+
+/**
+ * Wrapper around codeOwnerApi, sends each requests only once and then cache
+ * the response. A new CodeOwnersCacheApi instance is created every time when a
+ * new change object is assigned.
+ * Gerrit never updates existing change object, but instead always assigns a new
+ * change object. Particularly, a new change object is assigned when a change
+ * is updated and user clicks reload toasts to see the updated change.
+ * As a result, the lifetime of a cache is the same as a lifetime of an assigned
+ * change object.
+ * Periodical cache invalidation can lead to inconsistency in UI, i.e.
+ * user can see the old reviewers list (reflects a state when a change was
+ * loaded) and code-owners status for the current reviewer list. To avoid
+ * this inconsistency, the cache doesn't invalidate.
+ */
+export class CodeOwnersCacheApi {
+  constructor(codeOwnerApi, change) {
+    this.codeOwnerApi = codeOwnerApi;
+    this.change = change;
+    this.promises = {};
+  }
+
+  _fetchOnce(cacheKey, asyncFn) {
+    if (!this.promises[cacheKey]) {
+      this.promises[cacheKey] = asyncFn();
+    }
+    return this.promises[cacheKey];
+  }
+
+  getAccount() {
+    return this._fetchOnce('getAccount', () => this._getAccount());
+  }
+
+  async _getAccount() {
+    const loggedIn = await this.codeOwnerApi.restApi.getLoggedIn();
+    if (!loggedIn) return undefined;
+    return await this.codeOwnerApi.restApi.getAccount();
+  }
+
+  listOwnerStatus() {
+    return this._fetchOnce('listOwnerStatus',
+        () => this.codeOwnerApi.listOwnerStatus(this.change._number));
+  }
+
+  getBranchConfig() {
+    return this._fetchOnce('getBranchConfig',
+        () => this.codeOwnerApi.getBranchConfig(this.change.project,
+            this.change.branch));
+  }
+}
diff --git a/ui/code-owners-fetcher.js b/ui/code-owners-fetcher.js
new file mode 100644
index 0000000..61e362b
--- /dev/null
+++ b/ui/code-owners-fetcher.js
@@ -0,0 +1,204 @@
+/**
+ * @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.
+ */
+
+import {CodeOwnersApi} from './code-owners-api.js';
+
+/**
+ * All statuses returned for owner status.
+ *
+ * @enum
+ */
+export const OwnerStatus = {
+  INSUFFICIENT_REVIEWERS: 'INSUFFICIENT_REVIEWERS',
+  PENDING: 'PENDING',
+  APPROVED: 'APPROVED',
+};
+
+/**
+ * @enum
+ */
+export const FetchStatus = {
+  /** Fetch hasn't been started */
+  NOT_STARTED: 0,
+  /**
+   * Fetch has been started, but not all files has been finished.
+   * Pausing during fetching doesn't change state.
+   */
+  FETCHING: 1,
+  /**
+   * All owners has been loaded. resume/pause call doesn't change state.
+   */
+  FINISHED: 2,
+};
+
+/**
+ * Fetch owners for files. The class fetches owners in parallel and allows to
+ * pause/resume fetch.
+ */
+class OwnersFetcher {
+  /**
+   * Creates a fetcher in paused state. Actual fetching starts after resume()
+   * is called.
+   *
+   * @param {Array<string>} filesToFetch - Files paths for loading owners.
+   * @param {number} ownersLimit - number of requested owners per file.
+   * @param {number} maxConcurrentRequest - max number of concurrent requests to server.
+   */
+  constructor(codeOwnerApi, changeId, filesToFetch, ownersLimit,
+      maxConcurrentRequest) {
+    this._fetchedOwners = new Map();
+    this._ownersLimit = ownersLimit;
+    this._paused = true;
+    this._pausedFilesFetcher = [];
+    this._filesToFetch = filesToFetch;
+    this._fetchFilesPromises = [];
+    this._codeOwnerApi = codeOwnerApi;
+    this._changeId = changeId;
+
+    for (let i = 0; i < maxConcurrentRequest; i++) {
+      this._fetchFilesPromises.push(this._fetchFiles());
+    }
+  }
+
+  async _fetchFiles() {
+    for (;;) {
+      const filePath = await this._getNextFilePath();
+      if (!filePath) return;
+      try {
+        this._fetchedOwners.set(filePath, {
+          owners: await this._codeOwnerApi.listOwnersForPath(this._changeId,
+              filePath, this._ownersLimit),
+        });
+      } catch (error) {
+        this._fetchedOwners.set(filePath, {error});
+      }
+    }
+  }
+
+  async _getNextFilePath() {
+    if (this._paused) {
+      await new Promise(resolve => this._pausedFilesFetcher.push(resolve));
+    }
+    if (this._filesToFetch.length === 0) return null;
+    return this._filesToFetch.splice(0, 1)[0];
+  }
+
+  async waitFetchComplete() {
+    await Promise.allSettled(this._fetchFilesPromises);
+  }
+
+  resume() {
+    if (!this._paused) return;
+    this._paused = false;
+    for (const fetcher of this._pausedFilesFetcher.splice(0,
+        this._pausedFilesFetcher.length)) {
+      fetcher();
+    }
+  }
+
+  pause() {
+    this._paused = true;
+  }
+
+  getFetchedOwners() {
+    return this._fetchedOwners;
+  }
+
+  getFiles() {
+    const result = [];
+    for (const [path, info] of this._fetchedOwners.entries()) {
+      result.push({path, info});
+    }
+    return result;
+  }
+}
+
+export class OwnersProvider {
+  constructor(restApi, change, options) {
+    this.change = change;
+    this.options = options;
+    this._totalFetchCount = 0;
+    this._status = FetchStatus.NOT_STARTED;
+    this._codeOwnerApi = new CodeOwnersApi(restApi);
+  }
+
+  getStatus() {
+    return this._status;
+  }
+
+  getProgressString() {
+    return !this._ownersFetcher || this._totalFetchCount === 0 ?
+      `Loading suggested owners ...` :
+      `${this._ownersFetcher.getFetchedOwners().size} out of ` +
+      `${this._totalFetchCount} files have returned suggested owners.`;
+  }
+
+  getFiles() {
+    if (!this._ownersFetcher) return [];
+    return this._ownersFetcher.getFiles();
+  }
+
+  async fetchSuggestedOwners(codeOwnerStatusMap) {
+    if (this._status !== FetchStatus.NOT_STARTED) {
+      await this._ownersFetcher.waitFetchComplete();
+      return;
+    }
+    const filesToFetch = this._getFilesToFetch(codeOwnerStatusMap);
+    this._totalFetchCount = filesToFetch.length;
+    this._ownersFetcher = new OwnersFetcher(this._codeOwnerApi, this.change.id,
+        filesToFetch,
+        this.options.ownersLimit, this.options.maxConcurrentRequests);
+    this._status = FetchStatus.FETCHING;
+    this._ownersFetcher.resume();
+    await this._ownersFetcher.waitFetchComplete(filesToFetch);
+    this._status = FetchStatus.FINISHED;
+  }
+
+  _getFilesToFetch(codeOwnerStatusMap) {
+    // only fetch those not approved yet
+    const filesGroupByStatus = [...codeOwnerStatusMap.keys()].reduce(
+        (list, file) => {
+          const status = codeOwnerStatusMap
+              .get(file).status;
+          if (status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
+            list.missing.push(file);
+          } else if (status === OwnerStatus.PENDING) {
+            list.pending.push(file);
+          }
+          return list;
+        }
+        , {pending: [], missing: []});
+    // always fetch INSUFFICIENT_REVIEWERS first and then pending
+    return filesGroupByStatus.missing.concat(filesGroupByStatus.pending);
+  }
+
+  pause() {
+    if (!this._ownersFetcher) return;
+    this._ownersFetcher.pause();
+  }
+
+  resume() {
+    if (!this._ownersFetcher) return;
+    this._ownersFetcher.resume();
+  }
+
+  reset() {
+    this._totalFetchCount = 0;
+    this.ownersFetcher = null;
+    this._status = FetchStatus.NOT_STARTED;
+  }
+}
diff --git a/ui/code-owners-model-loader.js b/ui/code-owners-model-loader.js
index 2cd9095..2fa18ac 100644
--- a/ui/code-owners-model-loader.js
+++ b/ui/code-owners-model-loader.js
@@ -39,6 +39,7 @@
     try {
       newValue = await propertyLoader();
     } catch (e) {
+      console.error(e);
       this.ownersModel.setPluginFailed(e.message);
       return;
     }
@@ -84,33 +85,62 @@
     );
   }
 
-  async loadSuggestions() {
-    // If a loading has been started already, do nothing
-    if (this.ownersModel.suggestionsState
-        !== SuggestionsState.NotLoaded) return;
-
-    this.ownersModel.setSuggestionsState(SuggestionsState.Loading);
-    let suggestedOwners;
-    try {
-      suggestedOwners = await this.ownersService.getSuggestedOwners();
-    } catch (e) {
-      this.ownersModel.setSuggestionsState(SuggestionsState.LoadFailed);
-      this.ownersModel.setPluginFailed(e.message);
+  async loadSuggestions(suggestionsType) {
+    this.pauseActiveSuggestedOwnersLoading();
+    this.activeLoadSuggestionType = suggestionsType;
+    if (this.ownersModel.suggestionsByTypes[suggestionsType].state ===
+        SuggestionsState.Loading) {
+      this.ownersService.resumeSuggestedOwnersLoading(suggestionsType);
       return;
     }
-    this.ownersModel.setSuggestions(suggestedOwners.suggestions);
-    this.ownersModel.setSuggestionsState(SuggestionsState.Loaded);
-  }
 
-  async updateLoadSuggestionsProgress() {
+    // If a loading has been started already, do nothing
+    if (this.ownersModel.suggestionsByTypes[suggestionsType].state !==
+        SuggestionsState.NotLoaded) return;
+
+    this.ownersModel.setSuggestionsState(suggestionsType,
+        SuggestionsState.Loading);
     let suggestedOwners;
     try {
-      suggestedOwners = await this.ownersService.getSuggestedOwnersProgress();
+      suggestedOwners =
+          await this.ownersService.getSuggestedOwners(suggestionsType);
+    } catch (e) {
+      console.error(e);
+      this.ownersModel.setSuggestionsState(suggestionsType,
+          SuggestionsState.LoadFailed);
+      // The selectedSuggestionsType can be changed while getSuggestedOwners
+      // is executed. The plugin should fail only if the selectedSuggestionsType
+      // is the same.
+      if (this.ownersModel.selectedSuggestionsType === suggestionsType) {
+        this.ownersModel.setPluginFailed(e.message);
+      }
+      return;
+    }
+    this.ownersModel.setSuggestionsFiles(suggestionsType,
+        suggestedOwners.files);
+    this.ownersModel.setSuggestionsState(suggestionsType,
+        SuggestionsState.Loaded);
+  }
+
+  pauseActiveSuggestedOwnersLoading() {
+    if (!this.activeLoadSuggestionType) return;
+    this.ownersService.pauseSuggestedOwnersLoading(
+        this.activeLoadSuggestionType);
+  }
+
+  async updateLoadSelectedSuggestionsProgress() {
+    const suggestionsType = this.ownersModel.selectedSuggestionsType;
+    let suggestedOwners;
+    try {
+      suggestedOwners =
+          await this.ownersService.getSuggestedOwnersProgress(suggestionsType);
     } catch {
       // Ignore any error, keep progress unchanged.
       return;
     }
-    this.ownersModel.setSuggestionsLoadProgress(suggestedOwners.progress);
-    this.ownersModel.setSuggestions(suggestedOwners.suggestions);
+    this.ownersModel.setSuggestionsLoadProgress(suggestionsType,
+        suggestedOwners.progress);
+    this.ownersModel.setSuggestionsFiles(suggestionsType,
+        suggestedOwners.files);
   }
 }
diff --git a/ui/code-owners-model-mixin.js b/ui/code-owners-model-mixin.js
index 722af6f..80c8ffb 100644
--- a/ui/code-owners-model-mixin.js
+++ b/ui/code-owners-model-mixin.js
@@ -37,6 +37,7 @@
        */
       this.modelLoader = undefined;
     }
+
     static get properties() {
       return {
         /* The following 3 properties (change, reporting, restApi) have to be
diff --git a/ui/code-owners-model.js b/ui/code-owners-model.js
index cbd9bd2..079eb86 100644
--- a/ui/code-owners-model.js
+++ b/ui/code-owners-model.js
@@ -28,6 +28,14 @@
   Failed: 'Failed',
 };
 
+export const SuggestionsType = {
+  BEST_SUGGESTIONS: 'BEST_SUGGESTIONS',
+  ALL_SUGGESTIONS: 'ALL_SUGGESTIONS',
+};
+
+export const BestSuggestionsLimit = 5;
+export const AllSuggestionsLimit = 1000;
+
 /**
  * Maintain the state of code-owners.
  * Raises 'model-property-changed' event when a property is changed.
@@ -54,13 +62,23 @@
     this.userRole = undefined;
     this.isCodeOwnerEnabled = undefined;
     this.areAllFilesApproved = undefined;
-    this.suggestions = undefined;
-    this.suggestionsState = SuggestionsState.NotLoaded;
-    this.suggestionsLoadProgress = undefined;
+    this.suggestionsByTypes = {};
+    for (const suggestionType of Object.values(SuggestionsType)) {
+      this.suggestionsByTypes[suggestionType] = {
+        files: undefined,
+        state: SuggestionsState.NotLoaded,
+        loadProgress: undefined,
+      };
+    }
+    this.selectedSuggestionsType = SuggestionsType.BEST_SUGGESTIONS;
     this.showSuggestions = false;
     this.pluginStatus = undefined;
   }
 
+  get selectedSuggestions() {
+    return this.suggestionsByTypes[this.selectedSuggestionsType];
+  }
+
   setBranchConfig(config) {
     if (this.branchConfig === config) return;
     this.branchConfig = config;
@@ -91,22 +109,32 @@
     this._firePropertyChanged('areAllFilesApproved');
   }
 
-  setSuggestions(suggestions) {
-    if (this.suggestions === suggestions) return;
-    this.suggestions = suggestions;
-    this._firePropertyChanged('suggestions');
+  setSuggestionsFiles(suggestionsType, files) {
+    const suggestions = this.suggestionsByTypes[suggestionsType];
+    if (suggestions.files === files) return;
+    suggestions.files = files;
+    this._fireSuggestionsChanged(suggestionsType, 'files');
   }
 
-  setSuggestionsState(state) {
-    if (this.suggestionsState === state) return;
-    this.suggestionsState = state;
-    this._firePropertyChanged('suggestionsState');
+  setSuggestionsState(suggestionsType, state) {
+    const suggestions = this.suggestionsByTypes[suggestionsType];
+    if (suggestions.state === state) return;
+    suggestions.state = state;
+    this._fireSuggestionsChanged(suggestionsType, 'state');
   }
 
-  setSuggestionsLoadProgress(progress) {
-    if (this.suggestionsLoadProgress === progress) return;
-    this.suggestionsLoadProgress = progress;
-    this._firePropertyChanged('suggestionsLoadProgress');
+  setSuggestionsLoadProgress(suggestionsType, progress) {
+    const suggestions = this.suggestionsByTypes[suggestionsType];
+    if (suggestions.loadProgress === progress) return;
+    suggestions.loadProgress = progress;
+    this._fireSuggestionsChanged(suggestionsType, 'loadProgress');
+  }
+
+  setSelectedSuggestionType(suggestionsType) {
+    if (this.selectedSuggestionsType === suggestionsType) return;
+    this.selectedSuggestionsType = suggestionsType;
+    this._firePropertyChanged('selectedSuggestionsType');
+    this._firePropertyChanged('selectedSuggestions');
   }
 
   setShowSuggestions(show) {
@@ -148,6 +176,14 @@
     }));
   }
 
+  _fireSuggestionsChanged(suggestionsType, propertyName) {
+    this._firePropertyChanged(
+        `suggestionsByTypes.${suggestionsType}.${propertyName}`);
+    if (suggestionsType === this.selectedSuggestionsType) {
+      this._firePropertyChanged(`selectedSuggestions.${propertyName}`);
+    }
+  }
+
   static getModel(change) {
     if (!this.model || this.model.change !== change) {
       this.model = new CodeOwnersModel(change);
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 633ffbc..0db6226 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -15,26 +15,9 @@
  * limitations under the License.
  */
 
-/**
- * All statuses returned for owner status.
- *
- * @enum
- */
-export const OwnerStatus = {
-  INSUFFICIENT_REVIEWERS: 'INSUFFICIENT_REVIEWERS',
-  PENDING: 'PENDING',
-  APPROVED: 'APPROVED',
-};
-
-/**
- * @enum
- */
-const FetchStatus = {
-  NOT_STARTED: 0,
-  FETCHING: 1,
-  FINISHED: 2,
-  ABORT: 3,
-};
+import {SuggestionsType, BestSuggestionsLimit, AllSuggestionsLimit} from './code-owners-model.js';
+import {OwnersProvider, OwnerStatus, FetchStatus} from './code-owners-fetcher.js';
+import {CodeOwnersApi, CodeOwnersCacheApi} from './code-owners-api.js';
 
 /**
  * Specifies status for a change. The same as ChangeStatus enum in gerrit
@@ -60,278 +43,6 @@
   OTHER: 'OTHER',
 };
 
-// TODO: Try to remove it. The ResponseError and getErrorMessage duplicates
-// code from the gr-plugin-rest-api.ts. This code is required because
-// we want custom error processing in some functions. For details see
-// the original gr-plugin-rest-api.ts file/
-
-class ResponseError extends Error {
-  constructor(response) {
-    super();
-    this.response = response;
-  }
-}
-
-async function getErrorMessage(response) {
-  const text = await response.text();
-  return text ?
-    `${response.status}: ${text}` :
-    `${response.status}`;
-}
-
-/**
- * Responsible for communicating with the rest-api
- *
- * @see resources/Documentation/rest-api.md
- */
-class CodeOwnerApi {
-  constructor(restApi) {
-    this.restApi = restApi;
-  }
-
-  /**
-   * Returns a promise fetching the owner statuses for all files within the change.
-   *
-   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#change-endpoints
-   * @param {string} changeId
-   */
-  listOwnerStatus(changeId) {
-    return this.restApi.get(`/changes/${changeId}/code_owners.status`);
-  }
-
-  /**
-   * Returns a promise fetching the owners for a given path.
-   *
-   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#list-code-owners-for-path-in-branch
-   * @param {string} changeId
-   * @param {string} path
-   */
-  listOwnersForPath(changeId, path) {
-    return this.restApi.get(
-        `/changes/${changeId}/revisions/current/code_owners` +
-        `/${encodeURIComponent(path)}?limit=5&o=DETAILS`
-    );
-  }
-
-  /**
-   * Returns a promise fetching the owners config for a given path.
-   *
-   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#branch-endpoints
-   * @param {string} project
-   * @param {string} branch
-   * @param {string} path
-   */
-  getConfigForPath(project, branch, path) {
-    return this.restApi.get(
-        `/projects/${encodeURIComponent(project)}/` +
-        `branches/${encodeURIComponent(branch)}/` +
-        `code_owners.config/${encodeURIComponent(path)}`
-    );
-  }
-
-  /**
-   * Returns a promise fetching the owners config for a given branch.
-   *
-   * @doc https://gerrit.googlesource.com/plugins/code-owners/+/refs/heads/master/resources/Documentation/rest-api.md#branch-endpoints
-   * @param {string} project
-   * @param {string} branch
-   */
-  async getBranchConfig(project, branch) {
-    const errFn = (response, error) => {
-      if (error) throw error;
-      if (response) throw new ResponseError(response);
-      throw new Error('Generic REST API error');
-    }
-    try {
-      const config = await this.restApi.send(
-          'GET',
-          `/projects/${encodeURIComponent(project)}/` +
-          `branches/${encodeURIComponent(branch)}/` +
-          `code_owners.branch_config`,
-          undefined,
-          errFn
-      );
-      if (config.override_approval && !(config.override_approval
-          instanceof Array)) {
-        // In the upcoming backend changes, the override_approval will be changed
-        // to array with (possible) multiple items.
-        // While this transition is in progress, the frontend supports both API -
-        // the old one and the new one.
-        return {...config, override_approval: [config.override_approval]};
-      }
-      return config;
-    } catch(err) {
-      if (err instanceof ResponseError) {
-        if (err.response.status === 404) {
-          // The 404 error means that the branch doesn't exist and
-          // the plugin should be disabled.
-          return {disabled: true};
-        }
-        return getErrorMessage(err.response).then(msg => {
-          throw new Error(msg);
-        });
-      }
-      throw err;
-    }
-  }
-}
-
-/**
- * Wrapper around codeOwnerApi, sends each requests only once and then cache
- * the response. A new CodeOwnersCacheApi instance is created every time when a
- * new change object is assigned.
- * Gerrit never updates existing change object, but instead always assigns a new
- * change object. Particularly, a new change object is assigned when a change
- * is updated and user clicks reload toasts to see the updated change.
- * As a result, the lifetime of a cache is the same as a lifetime of an assigned
- * change object.
- * Periodical cache invalidation can lead to inconsistency in UI, i.e.
- * user can see the old reviewers list (reflects a state when a change was
- * loaded) and code-owners status for the current reviewer list. To avoid
- * this inconsistency, the cache doesn't invalidate.
- */
-export class CodeOwnersCacheApi {
-  constructor(codeOwnerApi, change) {
-    this.codeOwnerApi = codeOwnerApi;
-    this.change = change;
-    this.promises = {};
-  }
-
-  _fetchOnce(cacheKey, asyncFn) {
-    if (!this.promises[cacheKey]) {
-      this.promises[cacheKey] = asyncFn();
-    }
-    return this.promises[cacheKey];
-  }
-
-  getAccount() {
-    return this._fetchOnce('getAccount', () => this._getAccount());
-  }
-
-  async _getAccount() {
-    const loggedIn = await this.codeOwnerApi.restApi.getLoggedIn();
-    if (!loggedIn) return undefined;
-    return await this.codeOwnerApi.restApi.getAccount();
-  }
-
-  listOwnerStatus() {
-    return this._fetchOnce('listOwnerStatus',
-        () => this.codeOwnerApi.listOwnerStatus(this.change._number));
-  }
-
-  getBranchConfig() {
-    return this._fetchOnce('getBranchConfig',
-        () => this.codeOwnerApi.getBranchConfig(this.change.project,
-            this.change.branch));
-  }
-
-  listOwnersForPath(path) {
-    return this._fetchOnce(`listOwnersForPath:${path}`,
-        () => this.codeOwnerApi.listOwnersForPath(this.change.id, path));
-  }
-}
-
-export class OwnersFetcher {
-  constructor(restApi, change, options) {
-    // fetched files and fetching status
-    this._fetchedOwners = new Map();
-    this._fetchStatus = FetchStatus.NOT_STARTED;
-    this._totalFetchCount = 0;
-    this.change = change;
-    this.options = options;
-    this.codeOwnerApi = new CodeOwnerApi(restApi);
-  }
-
-  getStatus() {
-    return this._fetchStatus;
-  }
-
-  getProgressString() {
-    return this._totalFetchCount === 0 ?
-      `Loading suggested owners ...` :
-      `${this._fetchedOwners.size} out of ${this._totalFetchCount} files have returned suggested owners.`;
-  }
-
-  getFiles() {
-    const result = [];
-    for (const [path, info] of this._fetchedOwners.entries()) {
-      result.push({path, info});
-    }
-    return result;
-  }
-
-  async fetchSuggestedOwners(codeOwnerStatusMap) {
-    // reset existing temporary storage
-    this._fetchedOwners = new Map();
-    this._fetchStatus = FetchStatus.FETCHING;
-    this._totalFetchCount = 0;
-
-    // only fetch those not approved yet
-    const filesGroupByStatus = [...codeOwnerStatusMap.keys()].reduce(
-        (list, file) => {
-          const status = codeOwnerStatusMap
-              .get(file).status;
-          if (status === OwnerStatus.INSUFFICIENT_REVIEWERS) {
-            list.missing.push(file);
-          } else if (status === OwnerStatus.PENDING) {
-            list.pending.push(file);
-          }
-          return list;
-        }
-        , {pending: [], missing: []});
-    // always fetch INSUFFICIENT_REVIEWERS first and then pending
-    const filesToFetch = filesGroupByStatus.missing
-        .concat(filesGroupByStatus.pending);
-    this._totalFetchCount = filesToFetch.length;
-    await this._batchFetchCodeOwners(filesToFetch);
-    this._fetchStatus = FetchStatus.FINISHED;
-  }
-
-  /**
-   * Recursively fetches code owners for all files until finished.
-   *
-   * @param {!Array<string>} files
-   */
-  async _batchFetchCodeOwners(files) {
-    if (this._fetchStatus === FetchStatus.ABORT) {
-      return this._fetchedOwners;
-    }
-
-    const batchRequests = [];
-    const maxConcurrentRequests = this.options.maxConcurrentRequests;
-    for (let i = 0; i < maxConcurrentRequests; i++) {
-      const filePath = files[i];
-      if (filePath) {
-        this._fetchedOwners.set(filePath, {});
-        batchRequests.push(this._fetchOwnersForPath(this.change.id, filePath));
-      }
-    }
-    const resPromise = Promise.all(batchRequests);
-    await resPromise;
-    if (files.length > maxConcurrentRequests) {
-      return await this._batchFetchCodeOwners(
-          files.slice(maxConcurrentRequests));
-    }
-    return this._fetchedOwners;
-  }
-
-  async _fetchOwnersForPath(changeId, filePath) {
-    try {
-      const owners = await this.codeOwnerApi.listOwnersForPath(changeId,
-          filePath);
-      this._fetchedOwners.get(filePath).owners = owners;
-    } catch (e) {
-      this._fetchedOwners.get(filePath).error = e;
-    }
-  }
-
-  abort() {
-    this._fetchStatus = FetchStatus.ABORT;
-    this._fetchedOwners = new Map();
-    this._totalFetchCount = 0;
-  }
-}
-
 /**
  * Service for the data layer used in the plugin UI.
  */
@@ -339,13 +50,22 @@
   constructor(restApi, change, options = {}) {
     this.restApi = restApi;
     this.change = change;
-    const codeOwnerApi = new CodeOwnerApi(restApi);
-    this.codeOwnerCacheApi = new CodeOwnersCacheApi(codeOwnerApi, change);
+    const codeOwnersApi = new CodeOwnersApi(restApi);
+    this.codeOwnersCacheApi = new CodeOwnersCacheApi(codeOwnersApi, change);
 
-    const fetcherOptions = {
+    const providerOptions = {
       maxConcurrentRequests: options.maxConcurrentRequests || 10,
     };
-    this.ownersFetcher = new OwnersFetcher(restApi, change, fetcherOptions);
+    this.ownersProviders = {
+      [SuggestionsType.BEST_SUGGESTIONS]: new OwnersProvider(restApi, change, {
+        ...providerOptions,
+        ownersLimit: BestSuggestionsLimit,
+      }),
+      [SuggestionsType.ALL_SUGGESTIONS]: new OwnersProvider(restApi, change, {
+        ...providerOptions,
+        ownersLimit: AllSuggestionsLimit,
+      }),
+    };
   }
 
   /**
@@ -354,7 +74,7 @@
   async prefetch() {
     try {
       await Promise.all([
-        this.codeOwnerCacheApi.getAccount(),
+        this.codeOwnersCacheApi.getAccount(),
         this.getStatus(),
       ]);
     } catch {
@@ -371,7 +91,7 @@
    * role 'REVIEWER' remains unchanged until the change view is reloaded.
    */
   async getLoggedInUserInitialRole() {
-    const account = await this.codeOwnerCacheApi.getAccount();
+    const account = await this.codeOwnersCacheApi.getAccount();
     if (!account) {
       return UserRole.ANONYMOUS;
     }
@@ -418,9 +138,9 @@
     const status = await this._getStatus();
     if (status.enabled && !this.isOnLatestPatchset(status.patchsetNumber)) {
       // status is outdated, abort and re-init
-      this.abort();
+      this.reset();
       this.prefetch();
-      return await this.codeOwnerCacheApi.getStatus();
+      return await this.codeOwnersCacheApi.getStatus();
     }
     return status;
   }
@@ -436,7 +156,7 @@
       };
     }
 
-    const onwerStatus = await this.codeOwnerCacheApi.listOwnerStatus();
+    const onwerStatus = await this.codeOwnersCacheApi.listOwnerStatus();
 
     return {
       enabled: true,
@@ -476,39 +196,42 @@
    *  }>
    * }}
    */
-  async getSuggestedOwners() {
+  async getSuggestedOwners(suggestionsType) {
     const {codeOwnerStatusMap} = await this.getStatus();
+    const ownersProvider = this.ownersProviders[suggestionsType];
 
-    // In case its aborted due to outdated patches
-    // should kick start the fetching again
-    // Note: we currently are not reusing the instance when switching changes,
-    // so if its `abort` due to different changes, the whole instance will be
-    // outdated and not used.
-    if (this.ownersFetcher.getStatus() === FetchStatus.NOT_STARTED
-      || this.ownersFetcher.getStatus() === FetchStatus.ABORT) {
-      await this.ownersFetcher.fetchSuggestedOwners(codeOwnerStatusMap);
-    }
+    await ownersProvider.fetchSuggestedOwners(codeOwnerStatusMap);
 
     return {
-      finished: this.ownersFetcher.getStatus() === FetchStatus.FINISHED,
-      status: this.ownersFetcher.getStatus(),
-      progress: this.ownersFetcher.getProgressString(),
-      suggestions: this._groupFilesByOwners(codeOwnerStatusMap,
-          this.ownersFetcher.getFiles()),
+      finished: ownersProvider.getStatus() === FetchStatus.FINISHED,
+      status: ownersProvider.getStatus(),
+      progress: ownersProvider.getProgressString(),
+      files: this._getFilesWithStatuses(codeOwnerStatusMap,
+          ownersProvider.getFiles()),
     };
   }
 
-  async getSuggestedOwnersProgress() {
+  async getSuggestedOwnersProgress(suggestionsType) {
     const {codeOwnerStatusMap} = await this.getStatus();
+    const ownersProvider = this.ownersProviders[suggestionsType];
     return {
-      finished: this.ownersFetcher.getStatus() === FetchStatus.FINISHED,
-      status: this.ownersFetcher.getStatus(),
-      progress: this.ownersFetcher.getProgressString(),
-      suggestions: this._groupFilesByOwners(codeOwnerStatusMap,
-          this.ownersFetcher.getFiles()),
+      finished: ownersProvider.getStatus() === FetchStatus.FINISHED,
+      status: ownersProvider.getStatus(),
+      progress: ownersProvider.getProgressString(),
+      files: this._getFilesWithStatuses(codeOwnerStatusMap,
+          ownersProvider.getFiles()),
     };
   }
 
+  pauseSuggestedOwnersLoading(suggestionsType) {
+    this.ownersProviders[suggestionsType].pause();
+  }
+
+  resumeSuggestedOwnersLoading(suggestionsType) {
+    this.ownersProviders[suggestionsType].resume();
+  }
+
+
   _formatStatuses(statuses) {
     // convert the array of statuses to map between file path -> status
     return statuses.reduce((prev, cur) => {
@@ -542,74 +265,14 @@
     return;
   }
 
-  _groupFilesByOwners(codeOwnerStatusMap, files) {
-    // Note: for renamed or moved files, they will have two entries in the map
-    // we will treat them as two entries when group as well
-    const ownersFilesMap = new Map();
-    const failedToFetchFiles = new Set();
-    for (const file of files) {
-      const fileInfo = {
+  _getFilesWithStatuses(codeOwnerStatusMap, files) {
+    return files.map(file => {
+      return {
         path: file.path,
+        info: file.info,
         status: this._computeFileStatus(codeOwnerStatusMap, file.path),
       };
-      // for files failed to fetch, add them to the special group
-      if (file.info.error) {
-        failedToFetchFiles.add(fileInfo);
-        continue;
-      }
-
-      // do not include files still in fetching
-      if (!file.info.owners) {
-        continue;
-      }
-
-      const ownersKey = this._getOwnersGroupKey(file.info.owners);
-      ownersFilesMap.set(
-          ownersKey,
-          ownersFilesMap.get(ownersKey) || {files: [], owners: file.info.owners}
-      );
-      ownersFilesMap.get(ownersKey).files.push(fileInfo);
-    }
-    const groupedItems = [];
-    for (const ownersKey of ownersFilesMap.keys()) {
-      const groupName = this.getGroupName(ownersFilesMap.get(ownersKey).files);
-      groupedItems.push({
-        groupName,
-        files: ownersFilesMap.get(ownersKey).files,
-        owners: ownersFilesMap.get(ownersKey).owners,
-      });
-    }
-
-    if (failedToFetchFiles.size > 0) {
-      const failedFiles = [...failedToFetchFiles];
-      groupedItems.push({
-        groupName: this.getGroupName(failedFiles),
-        files: failedFiles,
-        error: new Error(
-            'Failed to fetch code owner info. Try to refresh the page.'),
-      });
-    }
-
-    return groupedItems;
-  }
-
-  _getOwnersGroupKey(owners) {
-    if (owners.owned_by_all_users) {
-      return '__owned_by_all_users__';
-    }
-    const code_owners = owners.code_owners;
-    return code_owners
-        .map(owner => owner.account._account_id)
-        .sort()
-        .join(',');
-  }
-
-  getGroupName(files) {
-    const fileName = files[0].path.split('/').pop();
-    return {
-      name: fileName,
-      prefix: files.length > 1 ? `+ ${files.length - 1} more` : '',
-    };
+    });
   }
 
   isOnLatestPatchset(patchsetId) {
@@ -617,14 +280,16 @@
     return `${latestRevision._number}` === `${patchsetId}`;
   }
 
-  abort() {
-    this.ownersFetcher.abort();
-    const codeOwnerApi = new CodeOwnerApi(this.restApi);
-    this.codeOwnerCacheApi = new CodeOwnersCacheApi(codeOwnerApi, change);
+  reset() {
+    for (const provider of Object.values(this.ownersProviders)) {
+      provider.reset();
+    }
+    const codeOwnersApi = new CodeOwnersApi(this.restApi);
+    this.codeOwnersCacheApi = new CodeOwnersCacheApi(codeOwnersApi, change);
   }
 
   async getBranchConfig() {
-    return this.codeOwnerCacheApi.getBranchConfig();
+    return this.codeOwnersCacheApi.getBranchConfig();
   }
 
   async isCodeOwnerEnabled() {
@@ -632,7 +297,7 @@
         this.change.status === ChangeStatus.MERGED) {
       return false;
     }
-    const config = await this.codeOwnerCacheApi.getBranchConfig();
+    const config = await this.codeOwnersCacheApi.getBranchConfig();
     return config && !config.disabled;
   }
 
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js
index 1ad5b4b..b506a8b 100644
--- a/ui/owner-requirement.js
+++ b/ui/owner-requirement.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {OwnerStatus} from './code-owners-service.js';
+import {OwnerStatus} from './code-owners-fetcher.js';
 import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
 import {showPluginFailedMessage} from './code-owners-banner.js';
 import {PluginState} from './code-owners-model.js';
diff --git a/ui/owner-status-column.js b/ui/owner-status-column.js
index b3d92fb..4e6e055 100644
--- a/ui/owner-status-column.js
+++ b/ui/owner-status-column.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {OwnerStatus} from './code-owners-service.js';
+import {OwnerStatus} from './code-owners-fetcher.js';
 import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
 
 const MAGIC_FILES = ['/COMMIT_MSG', '/MERGE_LIST', '/PATCHSET_LEVEL'];
diff --git a/ui/suggest-owners-util.js b/ui/suggest-owners-util.js
new file mode 100644
index 0000000..898a6e5
--- /dev/null
+++ b/ui/suggest-owners-util.js
@@ -0,0 +1,124 @@
+/**
+ * @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.
+ */
+
+import {BestSuggestionsLimit} from './code-owners-model.js';
+
+/**
+ * For each file calculates owners to display and group all files by those
+ * owners. The function creates "fake" groups when one or more
+ * reviewers are included in all owners of a file, but none of reviewers is
+ * included in best reviewers for the file.
+ *
+ * Such situations are possible when user turns on "Show all owners", selects
+ * one of newly displayed owners and then turns off "Show all owners". Without
+ * "fake" groups a user can see inconsistent state in dialog.
+ */
+
+export function getDisplayOwnersGroups(files, allOwnersByPathMap,
+    reviewersIdSet, allowAllOwnersSubstition) {
+
+  const getDisplayOwnersFunc =
+      !allowAllOwnersSubstition || allOwnersByPathMap.size === 0 ||
+      reviewersIdSet.size === 0 ?
+        file => file.info.owners :
+        file => getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet);
+  return groupFilesByOwners(files, getDisplayOwnersFunc);
+}
+
+function getDisplayOwners(file, allOwnersByPathMap, reviewersIdSet) {
+  const ownerSelected = owner => reviewersIdSet.has(owner.account._account_id);
+  const defaultOwners = file.info.owners;
+  if (!defaultOwners ||
+      defaultOwners.owned_by_all_users ||
+      defaultOwners.code_owners.some(ownerSelected)) {
+    return defaultOwners;
+  }
+  const allOwners = allOwnersByPathMap.get(file.path);
+  if (!allOwners) return defaultOwners;
+  if (allOwners.owned_by_all_users) return allOwners;
+  const selectedAllOwners = allOwners.code_owners.filter(ownerSelected);
+  if (selectedAllOwners.length === 0) return defaultOwners;
+  return {
+    code_owners: selectedAllOwners.slice(0, BestSuggestionsLimit),
+  };
+}
+
+function groupFilesByOwners(files, getDisplayOwnersFunc) {
+  // Note: for renamed or moved files, they will have two entries in the map
+  // we will treat them as two entries when group as well
+  const ownersFilesMap = new Map();
+  const failedToFetchFiles = new Set();
+  for (const file of files) {
+    // for files failed to fetch, add them to the special group
+    if (file.info.error) {
+      failedToFetchFiles.add(file);
+      continue;
+    }
+
+    // do not include files still in fetching
+    if (!file.info.owners) {
+      continue;
+    }
+    const displayOwners = getDisplayOwnersFunc(file);
+
+    const ownersKey = getOwnersGroupKey(displayOwners);
+    ownersFilesMap.set(
+        ownersKey,
+        ownersFilesMap.get(ownersKey) || {files: [], owners: displayOwners}
+    );
+    ownersFilesMap.get(ownersKey).files.push(file);
+  }
+  const groupedItems = [];
+  for (const ownersKey of ownersFilesMap.keys()) {
+    const groupName = getGroupName(ownersFilesMap.get(ownersKey).files);
+    groupedItems.push({
+      groupName,
+      files: ownersFilesMap.get(ownersKey).files,
+      owners: ownersFilesMap.get(ownersKey).owners,
+    });
+  }
+
+  if (failedToFetchFiles.size > 0) {
+    const failedFiles = [...failedToFetchFiles];
+    groupedItems.push({
+      groupName: getGroupName(failedFiles),
+      files: failedFiles,
+      error: new Error(
+          'Failed to fetch code owner info. Try to refresh the page.'),
+    });
+  }
+  return groupedItems;
+}
+
+function getOwnersGroupKey(owners) {
+  if (owners.owned_by_all_users) {
+    return '__owned_by_all_users__';
+  }
+  const code_owners = owners.code_owners;
+  return code_owners
+      .map(owner => owner.account._account_id)
+      .sort()
+      .join(',');
+}
+
+function getGroupName(files) {
+  const fileName = files[0].path.split('/').pop();
+  return {
+    name: fileName,
+    prefix: files.length > 1 ? `+ ${files.length - 1} more` : '',
+  };
+}
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js
index 91c6f96..781f64b 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -15,7 +15,12 @@
  * limitations under the License.
  */
 import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
-import {SuggestionsState} from './code-owners-model.js';
+import {
+  BestSuggestionsLimit,
+  SuggestionsState,
+  SuggestionsType,
+} from './code-owners-model.js';
+import {getDisplayOwnersGroups} from './suggest-owners-util.js';
 
 const SUGGESTION_POLLING_INTERVAL = 1000;
 
@@ -115,15 +120,23 @@
           max-height: 300px;
           overflow-y: auto;
         }
-        .suggestion-row {
+        .flex-break {
+          height: 0;
+          flex-basis: 100%;
+        }
+        .suggestion-row, .show-all-owners-row {
           display: flex;
           flex-direction: row;
           align-items: flex-start;
+        }
+        .suggestion-row {
+          flex-wrap: wrap;
           border-bottom: 1px solid var(--border-color);
           padding: var(--spacing-s) 0;
         }
-        .suggestion-row:last-of-type {
-          border-bottom: none;
+        .show-all-owners-row {
+          padding: var(--spacing-m) var(--spacing-xl) var(--spacing-s);
+          justify-content: flex-end;    
         }
         .suggestion-row-indicator {
           margin-right: var(--spacing-m);
@@ -162,6 +175,9 @@
           color: var(--deemphasized-text-color);
         }
         .suggested-owners {
+          --account-gap: var(--spacing-s);
+          --negative-account-gap: calc(-1*var(--account-gap));
+          margin: var(--negative-account-gap) 0 0 var(--negative-account-gap);
           flex: 1;
         }
         .fetch-error-content,
@@ -171,7 +187,7 @@
           flex: 1;
           padding-left: var(--spacing-m);
         }
-        
+
         .owned-by-all-users-content iron-icon {
           width: 16px;
           height: 16px;
@@ -200,6 +216,7 @@
            are always fit in a single row */
           --account-max-length: 60px;
           border: 1px solid var(--border-color);
+          margin: var(--account-gap) 0 0 var(--account-gap)
         }
         gr-account-label:focus {
           outline: none;
@@ -291,26 +308,39 @@
                 </div>
               </template>
               <template is="dom-if" if="[[!suggestion.owners.owned_by_all_users]]">
+                <template is="dom-if" if="[[_breakBeforeOwners(suggestion.owners.code_owners, _showAllOwners)]]">
+                  <div class="flex-break"></div>
+                </template>
                 <ul class="suggested-owners">
                   <template
                     is="dom-repeat"
                     items="[[suggestion.owners.code_owners]]"
                     as="owner"
                     index-as="ownerIndex"
-                  >
-                    <gr-account-label
+                  ><!--
+                    --><gr-account-label
                       data-suggestion-index$="[[suggestionIndex]]"
                       data-owner-index$="[[ownerIndex]]"
                       account="[[owner.account]]"
                       selected$="[[isSelected(owner)]]"
                       on-click="toggleAccount">
-                    </gr-account-label>
-                  </template>
+                    </gr-account-label><!--
+                --></template>
                 </ul>
               </template>
             </template>
           </li>
         </template>
+        <li class="show-all-owners-row">
+          <label>
+            <input
+              id="showAllOwnersCheckbox"
+              type="checkbox"
+              checked="{{_showAllOwners::change}}"
+            />
+            Show all owners
+          </label>
+        </li>
       </ul>
     `;
   }
@@ -333,7 +363,21 @@
       reviewers: {
         type: Array,
       },
+      _reviewersIdSet: {
+        type: Object,
+        computed: '_getReviewersIdSet(reviewers)',
+      },
       pendingReviewers: Array,
+      _showAllOwners: {
+        type: Boolean,
+        value: false,
+        observer: '_showAllOwnersChanged',
+      },
+      _allOwnersByPathMap: {
+        type: Object,
+        computed:
+            `_getOwnersByPathMap(model.suggestionsByTypes.${SuggestionsType.ALL_SUGGESTIONS}.files)`,
+      },
     };
   }
 
@@ -341,15 +385,39 @@
     return [
       '_onReviewerChanged(reviewers)',
       '_onShowSuggestionsChanged(model.showSuggestions)',
-      '_onSuggestionsStateChanged(model.suggestionsState)',
-      '_onSuggestionsChanged(model.suggestions, model.suggestionsState)',
-      '_onSuggestionsLoadProgressChanged(model.suggestionsLoadProgress)',
+      '_onShowSuggestionsTypeChanged(model.showSuggestions,' +
+        'model.selectedSuggestionsType)',
+      '_onSuggestionsStateChanged(model.selectedSuggestions.state)',
+      '_onSuggestionsFilesChanged(model.selectedSuggestions.files, ' +
+        '_allOwnersByPathMap, _reviewersIdSet, model.selectedSuggestionsType,' +
+        'model.selectedSuggestions.state)',
+      '_onSuggestionsLoadProgressChanged(' +
+        'model.selectedSuggestions.loadProgress)',
     ];
   }
 
+  constructor() {
+    super();
+    // To prevent multiple reporting when switching back and forth showAllOwners
+    this.reportedEvents = {};
+    for (const suggestionType of Object.values(SuggestionsType)) {
+      this.reportedEvents[suggestionType] = {
+        fetchingStart: false,
+        fetchingFinished: false,
+      };
+    }
+  }
+
+  disconnectedCallback() {
+    super.disconnectedCallback();
+    this._stopUpdateProgressTimer();
+    if (this.modelLoader) {
+      this.modelLoader.pauseActiveSuggestedOwnersLoading();
+    }
+  }
+
   _onShowSuggestionsChanged(showSuggestions) {
-    if (!showSuggestions ||
-        this.model.suggestionsLoadProgress === SuggestionsState.NotLoaded) {
+    if (!showSuggestions) {
       return;
     }
     // this is more of a hack to let review input lose focus
@@ -360,20 +428,31 @@
     // Can not use `this.async` as it's only available in
     // legacy element mixin which not used in this plugin.
     Polymer.Async.timeOut.run(() => this.click(), 100);
-
-    this.modelLoader.loadSuggestions();
-    this.reporting.reportLifeCycle('owners-suggestions-fetching-start');
   }
 
-  disconnectedCallback() {
-    super.disconnectedCallback();
-    this._stopUpdateProgressTimer();
+  _onShowSuggestionsTypeChanged(showSuggestion, selectedSuggestionsType) {
+    if (!showSuggestion) {
+      this.modelLoader.pauseActiveSuggestedOwnersLoading();
+      return;
+    }
+    this.modelLoader.loadSuggestions(selectedSuggestionsType);
+    // The progress is updated at the next _progressUpdateTimer tick.
+    // Without excplicit call to updateLoadSuggestionsProgress it looks like
+    // a slow reaction to checkbox.
+    this.modelLoader.updateLoadSelectedSuggestionsProgress();
+
+    if (!this.reportedEvents[selectedSuggestionsType].fetchingStart) {
+      this.reportedEvents[selectedSuggestionsType].fetchingStart = true;
+      this.reporting.reportLifeCycle('owners-suggestions-fetching-start', {
+        type: selectedSuggestionsType,
+      });
+    }
   }
 
   _startUpdateProgressTimer() {
     if (this._progressUpdateTimer) return;
     this._progressUpdateTimer = setInterval(() => {
-      this.modelLoader.updateLoadSuggestionsProgress();
+      this.modelLoader.updateLoadSelectedSuggestionsProgress();
     }, SUGGESTION_POLLING_INTERVAL);
   }
 
@@ -403,20 +482,43 @@
     this.modelLoader.loadAreAllFilesApproved();
   }
 
-  _onSuggestionsChanged(suggestions, suggestionsState) {
+  _getReviewersIdSet(reviewers) {
+    return new Set((reviewers || []).map(account => account._account_id));
+  }
+
+  _onSuggestionsFilesChanged(files, allOwnersByPathMap, reviewersIdSet,
+      selectedSuggestionsType, suggestionsState) {
+    if (files === undefined || allOwnersByPathMap === undefined ||
+        reviewersIdSet === undefined || selectedSuggestionsType === undefined ||
+        suggestionsState === undefined) return;
+
+    const groups = getDisplayOwnersGroups(
+        files, allOwnersByPathMap, reviewersIdSet,
+        selectedSuggestionsType !== SuggestionsType.ALL_SUGGESTIONS);
     // The updateLoadSuggestionsProgress method also updates suggestions
-    this._updateSuggestions(suggestions || []);
+    this._updateSuggestions(groups);
     this._updateAllChips(this._currentReviewers);
-    if (!suggestions || suggestionsState !== SuggestionsState.Loaded) return;
-    const reportDetails = suggestions.reduce((details, cur) => {
-      details.totalGroups++;
-      details.stats.push([cur.files.length,
-        cur.owners && cur.owners.code_owners ?
-          cur.owners.code_owners.length : 0]);
-      return details;
-    }, {totalGroups: 0, stats: []});
-    this.reporting.reportLifeCycle(
-        'owners-suggestions-fetching-finished', reportDetails);
+
+    if (suggestionsState !== SuggestionsState.Loaded) return;
+    if (!this.reportedEvents[selectedSuggestionsType].fetchingFinished) {
+      this.reportedEvents[selectedSuggestionsType].fetchingFinished = true;
+      const reportDetails = groups.reduce((details, cur) => {
+        details.totalGroups++;
+        details.stats.push([cur.files.length,
+          cur.owners && cur.owners.code_owners ?
+            cur.owners.code_owners.length : 0]);
+        return details;
+      }, {totalGroups: 0, stats: [], type: selectedSuggestionsType});
+      this.reporting.reportLifeCycle(
+          'owners-suggestions-fetching-finished', reportDetails);
+    }
+  }
+
+  _getOwnersByPathMap(files) {
+    return new Map((files || [])
+        .filter(file => !file.info.error && file.info.owners)
+        .map(file => [file.path, file.info.owners])
+    );
   }
 
   _onSuggestionsLoadProgressChanged(progress) {
@@ -446,21 +548,28 @@
     const res = {};
     res.groupName = suggestion.groupName;
     res.files = suggestion.files.slice();
-    const codeOwners = (suggestion.owners.code_owners || []).map(owner => {
-      const updatedOwner = {...owner};
-      const reviewers = this.change.reviewers.REVIEWER;
-      if (
-        reviewers &&
-        reviewers.find(reviewer => reviewer._account_id === owner._account_id)
-      ) {
-        updatedOwner.selected = true;
-      }
-      return updatedOwner;
-    });
-    res.owners = {
-      owned_by_all_users: !!suggestion.owners.owned_by_all_users,
-      code_owners: codeOwners,
-    };
+    if (suggestion.owners) {
+      const codeOwners = (suggestion.owners.code_owners || []).map(owner => {
+        const updatedOwner = {...owner};
+        const reviewers = this.change.reviewers.REVIEWER;
+        if (reviewers &&
+            reviewers.find(
+                reviewer => reviewer._account_id === owner._account_id)
+        ) {
+          updatedOwner.selected = true;
+        }
+        return updatedOwner;
+      });
+      res.owners = {
+        owned_by_all_users: !!suggestion.owners.owned_by_all_users,
+        code_owners: codeOwners,
+      };
+    } else {
+      res.owners = {
+        owned_by_all_users: false,
+        code_owners: [],
+      };
+    }
 
     res.error = suggestion.error;
     return res;
@@ -511,10 +620,7 @@
     this.suggestedOwners.forEach((suggestion, sId) => {
       let hasSelected = false;
       suggestion.owners.code_owners.forEach((owner, oId) => {
-        if (
-          accounts.some(account => account._account_id
-              === owner.account._account_id)
-        ) {
+        if (accounts.some(account => account._account_id === owner.account._account_id)) {
           this.set(
               ['suggestedOwners', sId, 'owners', 'code_owners', oId],
               {...owner,
@@ -559,6 +665,18 @@
       'Any user can approve. Please select a user manually' :
       'Any user from the other files can approve';
   }
+
+  _showAllOwnersChanged(showAll) {
+    // The first call to this method happens before model is set.
+    if (!this.model) return;
+    this.model.setSelectedSuggestionType(showAll ?
+      SuggestionsType.ALL_SUGGESTIONS : SuggestionsType.BEST_SUGGESTIONS);
+  }
+
+  _breakBeforeOwners(codeOwners, showAllOwners) {
+    if (!codeOwners || !showAllOwners) return false;
+    return codeOwners.length > BestSuggestionsLimit;
+  }
 }
 
 customElements.define(SuggestOwners.is, SuggestOwners);