Merge "CodeOwnerConfigOperations: Add method to get file path of a code owner config"
diff --git a/ui/code-owners-service.js b/ui/code-owners-service.js
index 426d0a7..bcac5cf 100644
--- a/ui/code-owners-service.js
+++ b/ui/code-owners-service.js
@@ -37,6 +37,30 @@
};
/**
+ * Specifies status for a change. The same as ChangeStatus enum in gerrit
+ *
+ * @enum
+ */
+const ChangeStatus = {
+ ABANDONED: 'ABANDONED',
+ MERGED: 'MERGED',
+ NEW: 'NEW',
+};
+
+/**
+ * @enum
+ */
+const UserRole = {
+ ANONYMOUS: 'ANONYMOUS',
+ AUTHOR: 'AUTHOR',
+ CHANGE_OWNER: 'CHANGE_OWNER',
+ REVIEWER: 'REVIEWER',
+ CC: 'CC',
+ REMOVED_REVIEWER: 'REMOVED_REVIEWER',
+ OTHER: 'OTHER',
+};
+
+/**
* Responsible for communicating with the rest-api
*
* @see resources/Documentation/rest-api.md
@@ -123,22 +147,90 @@
* Initial fetches.
*/
init() {
- this.statusPromise = this.codeOwnerApi
- .listOwnerStatus(this.change._number)
- .then(res => {
- return {
- patchsetNumber: res.patch_set_number,
- codeOwnerStatusMap: this._formatStatuses(
- res.file_code_owner_statuses
- ),
- rawStatuses: res.file_code_owner_statuses,
- };
- });
+ this.accountPromise = this.restApi.getLoggedIn().then(loggedIn => {
+ if (!loggedIn) {
+ return undefined;
+ }
+ return this.restApi.getAccount();
+ });
+
+ this.statusPromise = this.isCodeOwnerEnabled().then(enabled => {
+ if (!enabled) {
+ return {
+ patchsetNumber: 0,
+ enabled: false,
+ codeOwnerStatusMap: new Map(),
+ rawStatuses: [],
+ };
+ }
+ return this.codeOwnerApi
+ .listOwnerStatus(this.change._number)
+ .then(res => {
+ return {
+ enabled: true,
+ patchsetNumber: res.patch_set_number,
+ codeOwnerStatusMap: this._formatStatuses(
+ res.file_code_owner_statuses
+ ),
+ rawStatuses: res.file_code_owner_statuses,
+ };
+ });
+ });
+ }
+
+ /**
+ * Returns the role of the current user. The returned value reflects the
+ * role of the user at the time when the change is loaded.
+ * For example, if a user removes themselves as a reviewer, the returned
+ * role 'REVIEWER' remains unchanged until the change view is reloaded.
+ */
+ getLoggedInUserInitialRole() {
+ return this.accountPromise.then(account => {
+ if (!account) {
+ return UserRole.ANONYMOUS;
+ }
+ const change = this.change;
+ if (
+ change.revisions &&
+ change.current_revision &&
+ change.revisions[change.current_revision]
+ ) {
+ const commit = change.revisions[change.current_revision].commit;
+ if (
+ commit &&
+ commit.author &&
+ account.email &&
+ commit.author.email === account.email
+ ) {
+ return UserRole.AUTHOR;
+ }
+ }
+ if (change.owner._account_id === account._account_id) {
+ return UserRole.CHANGE_OWNER;
+ }
+ if (change.reviewers) {
+ if (this._accountInReviewers(change.reviewers.REVIEWER, account)) {
+ return UserRole.REVIEWER;
+ } else if (this._accountInReviewers(change.reviewers.CC, account)) {
+ return UserRole.CC;
+ } else if (this._accountInReviewers(change.reviewers.REMOVED, account)) {
+ return UserRole.REMOVED_REVIEWER;
+ }
+ }
+ return UserRole.OTHER;
+ })
+ }
+
+ _accountInReviewers(reviewers, account) {
+ if (!reviewers) {
+ return false;
+ }
+ return reviewers.some(reviewer => reviewer._account_id === account._account_id);
}
getStatus() {
return this.statusPromise.then(res => {
- if (!this.isOnLatestPatchset(res.patchsetNumber)) {
+ if (res.enabled && !this.isOnLatestPatchset(res.patchsetNumber)) {
// status is outdated, abort and re-init
this.abort();
this.init();
@@ -325,15 +417,6 @@
};
}
- /**
- * Returns a promise with whether status is for latest patchset or not.
- */
- isStatusOnLatestPatchset() {
- return this.statusPromise.then(({patch_set_id}) => {
- return this.isOnLatestPatchset(patch_set_id);
- });
- }
-
isOnLatestPatchset(patchsetId) {
const latestRevision = this.change.revisions[this.change.current_revision];
return `${latestRevision._number}` === `${patchsetId}`;
@@ -388,10 +471,18 @@
}
getProjectConfig() {
- return this.codeOwnerApi.getProjectConfig(this.change.project);
+ if (!this.getProjectConfigPromise) {
+ this.getProjectConfigPromise =
+ this.codeOwnerApi.getProjectConfig(this.change.project);
+ }
+ return this.getProjectConfigPromise;
}
isCodeOwnerEnabled() {
+ if (this.change.status === ChangeStatus.ABANDONED ||
+ this.change.status === ChangeStatus.MERGED) {
+ return Promise.resolve(false);
+ }
return this.getProjectConfig().then(config => {
if (config.status && config.status.disabled) {
return false;
@@ -414,4 +505,4 @@
}
return this.ownerService;
}
-}
\ No newline at end of file
+}
diff --git a/ui/code-owners-service_test.html b/ui/code-owners-service_test.html
index f0678da..6986ea9 100644
--- a/ui/code-owners-service_test.html
+++ b/ui/code-owners-service_test.html
@@ -18,6 +18,9 @@
get() {
return Promise.resolve({});
},
+ getLoggedIn() {
+ return Promise.resolve(undefined);
+ },
};
const getApiStub = sinon.stub(fakeRestApi, 'get');
getApiStub.returns(Promise.resolve({}));
@@ -233,4 +236,4 @@
});
});
});
-</script>
\ No newline at end of file
+</script>
diff --git a/ui/owner-requirement.js b/ui/owner-requirement.js
index e456976..ce8b37a 100644
--- a/ui/owner-requirement.js
+++ b/ui/owner-requirement.js
@@ -52,6 +52,9 @@
gr-button {
padding-left: var(--spacing-m);
}
+ a {
+ text-decoration: none;
+ }
</style>
<p class="loading" hidden="[[!_isLoading]]">
<span class="loadingSpin"></span>
@@ -59,6 +62,11 @@
</p>
<template is="dom-if" if="[[!_isLoading]]">
<span>[[_computeStatusText(_statusCount, _isOverriden)]]</span>
+ <template is="dom-if" if="[[_overrideInfoUrl]]">
+ <a on-click="_reportDocClick" href="[[_overrideInfoUrl]]" target="_blank">
+ <iron-icon icon="gr-icons:help-outline" title="Documentation for overriding code owners"></iron-icon>
+ </a>
+ </template>
<template is="dom-if" if="[[!_allApproved]]">
<gr-button link on-click="_openReplyDialog">
Suggest owners
@@ -77,12 +85,16 @@
ownerService: Object,
_statusCount: Object,
- _isLoading: Boolean,
+ _isLoading: {
+ type: Boolean,
+ value: true,
+ },
_allApproved: {
type: Boolean,
computed: '_computeAllApproved(_statusCount)',
},
_isOverriden: Boolean,
+ _overrideInfoUrl: String,
};
}
@@ -123,19 +135,28 @@
return this.ownerService.getStatus()
.then(({rawStatuses}) => {
this._statusCount = this._getStatusCount(rawStatuses);
-
- // Send a metric with overall summary when code owners submit
- // requirement shown and finished fetching status
- this.reporting.reportLifeCycle(
- 'owners-submit-requirement-summary-shown',
- {...this._statusCount}
- );
+ this.ownerService.getLoggedInUserInitialRole().then(role => {
+ // Send a metric with overall summary when code owners submit
+ // requirement shown and finished fetching status
+ this.reporting.reportLifeCycle(
+ 'owners-submit-requirement-summary-shown',
+ {...this._statusCount, user_role: role}
+ );
+ });
})
.finally(() => {
this._isLoading = false;
});
}
+ _updateOverrideInfoUrl() {
+ this.ownerService.getProjectConfig().then(config => {
+ this._overrideInfoUrl = config.general && config.general.override_info_url
+ ?
+ config.general.override_info_url : '';
+ });
+ }
+
_computeAllApproved(statusCount) {
return statusCount.missing === 0
&& statusCount.pending === 0;
@@ -194,6 +215,7 @@
this.ownerService = CodeOwnerService.getOwnerService(this.restApi, change);
this._updateStatus();
this._checkIfOverriden();
+ this._updateOverrideInfoUrl();
}
_openReplyDialog() {
@@ -205,9 +227,11 @@
})
);
ownerState.expandSuggestion = true;
-
- this.reporting.reportInteraction('suggest-owners-from-submit-requirement');
+ this.ownerService.getLoggedInUserInitialRole().then(role => {
+ this.reporting.reportInteraction(
+ 'suggest-owners-from-submit-requirement', {user_role: role});
+ });
}
}
-customElements.define(OwnerRequirementValue.is, OwnerRequirementValue);
\ No newline at end of file
+customElements.define(OwnerRequirementValue.is, OwnerRequirementValue);
diff --git a/ui/owner-ui-state.js b/ui/owner-ui-state.js
index 9d82771..3d47ef9 100644
--- a/ui/owner-ui-state.js
+++ b/ui/owner-ui-state.js
@@ -14,6 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+/**
+ * @enum
+ */
+const OwnerUIStateEventType = {
+ EXPAND_SUGGESTION: 'expandSuggestion',
+};
+
/**
* For states used in code owners plugin across multiple components.
*/
@@ -21,7 +29,7 @@
constructor() {
this._expandSuggestion = false;
this._listeners = new Map();
- this._listeners.set('expandSuggestion', []);
+ this._listeners.set(OwnerUIStateEventType.EXPAND_SUGGESTION, []);
}
get expandSuggestion() {
@@ -30,7 +38,7 @@
set expandSuggestion(value) {
this._expandSuggestion = value;
- this._listeners.get('expandSuggestion').forEach(cb => {
+ this._listeners.get(OwnerUIStateEventType.EXPAND_SUGGESTION).forEach(cb => {
try {
cb(value);
} catch (e) {
@@ -39,9 +47,23 @@
});
}
+ _subscribeEvent(eventType, cb) {
+ this._listeners.get(eventType).push(cb);
+ return () => {
+ this._unsubscribeEvent(eventType, cb);
+ };
+ }
+
+ _unsubscribeEvent(eventType, cb) {
+ this._listeners.set(
+ eventType,
+ this._listeners.get(eventType).filter(handler => handler !== cb)
+ );
+ }
+
onExpandSuggestionChange(cb) {
- this._listeners.get('expandSuggestion').push(cb);
+ return this._subscribeEvent(OwnerUIStateEventType.EXPAND_SUGGESTION, cb);
}
}
-export const ownerState = new OwnerUIState();
\ No newline at end of file
+export const ownerState = new OwnerUIState();
diff --git a/ui/suggest-owners-trigger.js b/ui/suggest-owners-trigger.js
index 5745545..2c0ccc0 100644
--- a/ui/suggest-owners-trigger.js
+++ b/ui/suggest-owners-trigger.js
@@ -22,6 +22,11 @@
return 'suggest-owners-trigger';
}
+ constructor(props) {
+ super(props);
+ this.expandSuggestionStateUnsubscriber = undefined;
+ }
+
static get properties() {
return {
change: Object,
@@ -78,9 +83,18 @@
connectedCallback() {
super.connectedCallback();
- ownerState.onExpandSuggestionChange(expanded => {
- this.expanded = expanded;
- });
+ this.expandSuggestionStateUnsubscriber = ownerState
+ .onExpandSuggestionChange(expanded => {
+ this.expanded = expanded;
+ });
+ }
+
+ disconnnectedCallback() {
+ super.disconnectedCallback();
+ if (this.expandSuggestionStateUnsubscriber) {
+ this.expandSuggestionStateUnsubscriber();
+ this.expandSuggestionStateUnsubscriber = undefined;
+ }
}
onInputChanged(restApi, change) {
@@ -90,13 +104,18 @@
this.change
);
- Promise.all([this.ownerService.isCodeOwnerEnabled(), this.ownerService.areAllFilesApproved()])
- .then(([enabled, approved]) => {
+ Promise.all([
+ this.ownerService.isCodeOwnerEnabled(),
+ this.ownerService.areAllFilesApproved(),
+ this.ownerService.getLoggedInUserInitialRole()
+ ])
+ .then(([enabled, approved, userRole]) => {
if (enabled) {
this.hidden = approved;
} else {
this.hidden = true;
}
+ this._userRole = userRole;
});
}
@@ -105,6 +124,7 @@
ownerState.expandSuggestion = this.expanded;
this.reporting.reportInteraction('toggle-suggest-owners', {
expanded: this.expanded,
+ user_role: this._userRole ? this._userRole : 'UNKNOWN',
});
}
@@ -121,4 +141,4 @@
}
}
-customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
\ No newline at end of file
+customElements.define(SuggestOwnersTrigger.is, SuggestOwnersTrigger);
diff --git a/ui/suggest-owners.js b/ui/suggest-owners.js
index 9919e54..2e1a487 100644
--- a/ui/suggest-owners.js
+++ b/ui/suggest-owners.js
@@ -24,6 +24,11 @@
return 'owner-group-file-list';
}
+ constructor() {
+ super();
+ this.expandSuggestionStateUnsubscriber = undefined;
+ }
+
static get properties() {
return {
files: Array,
@@ -327,30 +332,40 @@
connectedCallback() {
super.connectedCallback();
- ownerState.onExpandSuggestionChange(expanded => {
- this.hidden = !expanded;
- if (expanded) {
- // this is more of a hack to let reivew input lose focus
- // to avoid suggestion dropdown
- // gr-autocomplete has a internal state for tracking focus
- // that will be canceled if any click happens outside of
- // it's target
- // 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.expandSuggestionStateUnsubscriber = ownerState
+ .onExpandSuggestionChange(expanded => {
+ this.hidden = !expanded;
+ if (expanded) {
+ // this is more of a hack to let reivew input lose focus
+ // to avoid suggestion dropdown
+ // gr-autocomplete has a internal state for tracking focus
+ // that will be canceled if any click happens outside of
+ // it's target
+ // 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);
- // start fetching suggestions
- if (!this._suggestionsTimer) {
- this._suggestionsTimer = setInterval(() => {
- this._pollingSuggestions();
- }, SUGGESTION_POLLING_INTERVAL);
+ // start fetching suggestions
+ if (!this._suggestionsTimer) {
+ this._suggestionsTimer = setInterval(() => {
+ this._pollingSuggestions();
+ }, SUGGESTION_POLLING_INTERVAL);
- // poll immediately to kick start the fetching
- this.reporting.reportLifeCycle('owners-suggestions-fetching-start');
- this._pollingSuggestions();
- }
- }
- });
+ // poll immediately to kick start the fetching
+ this.reporting
+ .reportLifeCycle('owners-suggestions-fetching-start');
+ this._pollingSuggestions();
+ }
+ }
+ });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ if (this.expandSuggestionStateUnsubscriber) {
+ this.expandSuggestionStateUnsubscriber();
+ this.expandSuggestionStateUnsubscriber = undefined;
+ }
}
onInputChanged(restApi, change) {