Add "Show all owners" option

The checkbox allows to request all owners for files in a change.

The implementation has an issue: if a user clicks checkbox before
owners has been loaded, the plugin starts loading all owners
without pausing best owners loading. This issue is fixed in the
next change.

Change-Id: Id664476c47d6bcb170095da524877895c9484dfa
diff --git a/ui/code-owners-model-loader.js b/ui/code-owners-model-loader.js
index 2cd9095..0e5ace7 100644
--- a/ui/code-owners-model-loader.js
+++ b/ui/code-owners-model-loader.js
@@ -84,33 +84,47 @@
     );
   }
 
-  async loadSuggestions() {
+  async loadSuggestions(suggestionsType) {
     // If a loading has been started already, do nothing
-    if (this.ownersModel.suggestionsState
-        !== SuggestionsState.NotLoaded) return;
+    if (this.ownersModel.suggestionsByTypes[suggestionsType].state !==
+        SuggestionsState.NotLoaded) return;
 
-    this.ownersModel.setSuggestionsState(SuggestionsState.Loading);
+    this.ownersModel.setSuggestionsState(suggestionsType,
+        SuggestionsState.Loading);
     let suggestedOwners;
     try {
-      suggestedOwners = await this.ownersService.getSuggestedOwners();
+      suggestedOwners =
+          await this.ownersService.getSuggestedOwners(suggestionsType);
     } catch (e) {
-      this.ownersModel.setSuggestionsState(SuggestionsState.LoadFailed);
-      this.ownersModel.setPluginFailed(e.message);
+      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.setSuggestions(suggestedOwners.suggestions);
-    this.ownersModel.setSuggestionsState(SuggestionsState.Loaded);
+    this.ownersModel.setSuggestionsFiles(suggestionsType,
+        suggestedOwners.files);
+    this.ownersModel.setSuggestionsState(suggestionsType,
+        SuggestionsState.Loaded);
   }
 
-  async updateLoadSuggestionsProgress() {
+  async updateLoadSelectedSuggestionsProgress() {
+    const suggestionsType = this.ownersModel.selectedSuggestionsType;
     let suggestedOwners;
     try {
-      suggestedOwners = await this.ownersService.getSuggestedOwnersProgress();
+      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.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..55ad38a 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+import {SuggestionsType, BestSuggestionsLimit, AllSuggestionsLimit} from './code-owners-model.js';
+
 /**
  * All statuses returned for owner status.
  *
@@ -106,10 +108,10 @@
    * @param {string} changeId
    * @param {string} path
    */
-  listOwnersForPath(changeId, path) {
+  listOwnersForPath(changeId, path, limit) {
     return this.restApi.get(
         `/changes/${changeId}/revisions/current/code_owners` +
-        `/${encodeURIComponent(path)}?limit=5&o=DETAILS`
+        `/${encodeURIComponent(path)}?limit=${limit}&o=DETAILS`
     );
   }
 
@@ -224,11 +226,6 @@
         () => 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 {
@@ -318,7 +315,7 @@
   async _fetchOwnersForPath(changeId, filePath) {
     try {
       const owners = await this.codeOwnerApi.listOwnersForPath(changeId,
-          filePath);
+          filePath, this.options.ownersLimit);
       this._fetchedOwners.get(filePath).owners = owners;
     } catch (e) {
       this._fetchedOwners.get(filePath).error = e;
@@ -345,7 +342,16 @@
     const fetcherOptions = {
       maxConcurrentRequests: options.maxConcurrentRequests || 10,
     };
-    this.ownersFetcher = new OwnersFetcher(restApi, change, fetcherOptions);
+    this.ownersFetchers = {
+      [SuggestionsType.BEST_SUGGESTIONS]: new OwnersFetcher(restApi, change, {
+        ...fetcherOptions,
+        ownersLimit: BestSuggestionsLimit,
+      }),
+      [SuggestionsType.ALL_SUGGESTIONS]: new OwnersFetcher(restApi, change, {
+        ...fetcherOptions,
+        ownersLimit: AllSuggestionsLimit,
+      }),
+    };
   }
 
   /**
@@ -476,36 +482,38 @@
    *  }>
    * }}
    */
-  async getSuggestedOwners() {
+  async getSuggestedOwners(suggestionsType) {
     const {codeOwnerStatusMap} = await this.getStatus();
+    const ownersFetcher = this.ownersFetchers[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);
+    if (ownersFetcher.getStatus() === FetchStatus.NOT_STARTED
+      || ownersFetcher.getStatus() === FetchStatus.ABORT) {
+      await ownersFetcher.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: ownersFetcher.getStatus() === FetchStatus.FINISHED,
+      status: ownersFetcher.getStatus(),
+      progress: ownersFetcher.getProgressString(),
+      files: this._getFilesWithStatuses(codeOwnerStatusMap,
+          ownersFetcher.getFiles()),
     };
   }
 
-  async getSuggestedOwnersProgress() {
+  async getSuggestedOwnersProgress(suggestionsType) {
     const {codeOwnerStatusMap} = await this.getStatus();
+    const ownersFetcher = this.ownersFetchers[suggestionsType];
     return {
-      finished: this.ownersFetcher.getStatus() === FetchStatus.FINISHED,
-      status: this.ownersFetcher.getStatus(),
-      progress: this.ownersFetcher.getProgressString(),
-      suggestions: this._groupFilesByOwners(codeOwnerStatusMap,
-          this.ownersFetcher.getFiles()),
+      finished: ownersFetcher.getStatus() === FetchStatus.FINISHED,
+      status: ownersFetcher.getStatus(),
+      progress: ownersFetcher.getProgressString(),
+      files: this._getFilesWithStatuses(codeOwnerStatusMap,
+          ownersFetcher.getFiles()),
     };
   }
 
@@ -542,74 +550,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) {
@@ -618,7 +566,9 @@
   }
 
   abort() {
-    this.ownersFetcher.abort();
+    for (const fetcher of Object.values(this.ownersFetchers)) {
+      fetcher.abort();
+    }
     const codeOwnerApi = new CodeOwnerApi(this.restApi);
     this.codeOwnerCacheApi = new CodeOwnersCacheApi(codeOwnerApi, change);
   }
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..4f9553a 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -15,7 +15,8 @@
  * limitations under the License.
  */
 import {CodeOwnersModelMixin} from './code-owners-model-mixin.js';
-import {SuggestionsState} from './code-owners-model.js';
+import {SuggestionsState, SuggestionsType} from './code-owners-model.js';
+import {getDisplayOwnersGroups} from './suggest-owners-util.js';
 
 const SUGGESTION_POLLING_INTERVAL = 1000;
 
@@ -115,15 +116,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 +171,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 +183,7 @@
           flex: 1;
           padding-left: var(--spacing-m);
         }
-        
+
         .owned-by-all-users-content iron-icon {
           width: 16px;
           height: 16px;
@@ -200,6 +212,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 +304,39 @@
                 </div>
               </template>
               <template is="dom-if" if="[[!suggestion.owners.owned_by_all_users]]">
+                <template is="dom-if" if="[[_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 +359,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 +381,31 @@
     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,
+      };
+    }
+  }
+
   _onShowSuggestionsChanged(showSuggestions) {
-    if (!showSuggestions ||
-        this.model.suggestionsLoadProgress === SuggestionsState.NotLoaded) {
+    if (!showSuggestions) {
       return;
     }
     // this is more of a hack to let review input lose focus
@@ -361,8 +417,24 @@
     // 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');
+  }
+
+  _onShowSuggestionsTypeChanged(showSuggestion, selectedSuggestionsType) {
+    if (!showSuggestion) {
+      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,
+      });
+    }
   }
 
   disconnectedCallback() {
@@ -373,7 +445,7 @@
   _startUpdateProgressTimer() {
     if (this._progressUpdateTimer) return;
     this._progressUpdateTimer = setInterval(() => {
-      this.modelLoader.updateLoadSuggestionsProgress();
+      this.modelLoader.updateLoadSelectedSuggestionsProgress();
     }, SUGGESTION_POLLING_INTERVAL);
   }
 
@@ -403,20 +475,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) {
@@ -511,10 +606,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 +651,13 @@
       '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);
+  }
 }
 
 customElements.define(SuggestOwners.is, SuggestOwners);