Add a basic screen to invoke the Check Code Owner REST endpoint
It happens frequently that users have questions about code owners, e.g.
why a certain user is or isn't a code owner of a certain file. In this
case admins can use the Check Code Owner REST endpoint to get insights
into the code owner computation.
Invoking the Check Code Owner REST endpoint is a bit cumbersome though
and hence takes more time than it should:
* one needs to lookup the URL from the documentation and then assemble
the URL manually
* identifiers in the URL (e.g. project names and files names) need to be
URL-encoded
* to make the JSON output readable pretty-printing (pp=1) should be
requested
* one needs to know how to call the REST endpoint with authentication
(e.g. through curl or by opening the URL in the browser)
To simplify this we add a very basic screen with a form that allows to
call the Check Code Owner REST endpoint from the UI. This form takes
care of the pain-points above and also provides links to the
documentation.
The new screen is available at:
<GERRIT_URL>/x/code-owners/check-code-owner
Screenshot: https://i.imgur.com/6CFDFpH.png
The implementation of the screen followed the example of the service
user creation screen from the serviceuser plugin:
https://gerrit.googlesource.com/plugins/serviceuser/+/master/web/gr-serviceuser-create.ts
Bug: Google b/345161989
Change-Id: I25cff9f08ee3c8eb3a11dc4cd2d262fc8be1a6e3
Signed-off-by: Edwin Kempin <ekempin@google.com>
Reviewed-on: https://gerrit-review.googlesource.com/c/plugins/code-owners/+/434457
Tested-by: Zuul <zuul-63@gerritcodereview-ci.iam.gserviceaccount.com>
Reviewed-by: Kamil Musin <kamilm@google.com>
diff --git a/web/gr-check-code-owner.ts b/web/gr-check-code-owner.ts
new file mode 100644
index 0000000..cba304f
--- /dev/null
+++ b/web/gr-check-code-owner.ts
@@ -0,0 +1,266 @@
+/**
+ * @license
+ * Copyright (C) 2024 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 {customElement, query, property, state} from 'lit/decorators';
+import {css, CSSResult, html, LitElement} from 'lit';
+import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
+
+declare global {
+ interface Window {
+ CANONICAL_PATH?: string;
+ }
+}
+
+@customElement('gr-check-code-owner')
+export class GrCheckCodeOwner extends LitElement {
+ @query('#projectInput')
+ projectInput!: HTMLInputElement;
+
+ @query('#branchInput')
+ branchInput!: HTMLInputElement;
+
+ @query('#emailInput')
+ emailInput!: HTMLInputElement;
+
+ @query('#pathInput')
+ pathInput!: HTMLInputElement;
+
+ @query('#userInput')
+ userInput!: HTMLInputElement;
+
+ @query('#resultOutput')
+ resultOutput!: HTMLInputElement;
+
+ @property()
+ plugin!: PluginApi;
+
+ @state()
+ dataValid = false;
+
+ @state()
+ isChecking = false;
+
+ static override get styles() {
+ return [
+ window.Gerrit.styles.font as CSSResult,
+ window.Gerrit.styles.form as CSSResult,
+ css`
+ main {
+ margin: 2em auto;
+ max-width: 50em;
+ }
+ .heading {
+ font-size: x-large;
+ font-weight: 500;
+ }
+ .output {
+ min-width: 50em;
+ }
+ `,
+ ];
+ }
+
+ override render() {
+ return html`
+ <main class="gr-form-styles read-only">
+ <div>
+ <h1 class="heading">Check Code Owner</h1>
+ </div>
+ <p>
+ Checks the code ownership of a user for a path in a branch, see
+ <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#check-code-owner" target="_blank">documentation<a/>.
+ </p>
+ <p>
+ Requires that the caller has the
+ <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#checkCodeOwner" target="_blank">Check Code Owner</a>
+ or the
+ <a href="${window.CANONICAL_PATH || ''}/Documentation/access-control.html#capability_administrateServer" target="_blank">Administrate Server</a>
+ global capability.
+ </p>
+ <p>All fields, except the 'Calling User' field, are required.</p>
+ <fieldset>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="The project from which the code owner configuration should be read."
+ >
+ Project* <gr-icon icon="info"></gr-icon>:
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <input
+ id="projectInput"
+ type="text"
+ @input=${this.validateData}
+ />
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="The branch from which the code owner configuration should be read."
+ >
+ Branch* <gr-icon icon="info"></gr-icon>:
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <input
+ id="branchInput"
+ type="text"
+ @input=${this.validateData}
+ />
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="Email for which the code ownership should be checked."
+ >
+ Email* <gr-icon icon="info"></gr-icon>:
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <input
+ id="emailInput"
+ type="text"
+ @input=${this.validateData}
+ />
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="Path for which the code ownership should be checked."
+ >
+ Path* <gr-icon icon="info"></gr-icon>:
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <input
+ id="pathInput"
+ type="text"
+ placeholder="/path/to/file.ext"
+ @input=${this.validateData}
+ />
+ </span>
+ </section>
+ <section>
+ <span class="title">
+ <gr-tooltip-content
+ has-tooltip
+ title="User for which the code owner visibility should be checked.\nCan be any account identifier (e.g. email, account ID).\nIf not specified the code owner visibility is not checked.\nCan be used to investigate why a code owner is not shown/suggested to this user."
+ >
+ Calling User (optional) <gr-icon icon="info"></gr-icon>:
+ </gr-tooltip-content>
+ </span>
+ <span class="value">
+ <input
+ id="userInput"
+ type="text"
+ @input=${this.validateData}
+ />
+ </span>
+ </section>
+ </fieldset>
+ <gr-button
+ @click=${this.handleCheckCodeOwner}
+ ?disabled="${!this.dataValid || this.isChecking}"
+ >
+ Check Code Owner
+ </gr-button>
+ <p>
+ See
+ <a href="${window.CANONICAL_PATH || ''}/plugins/code-owners/Documentation/rest-api.html#code-owner-check-info" target="_blank">CheckCodeOwnerInfo</a>
+ for an explanation of the JSON fields.
+ </p>
+ <section>
+ <span class="value">
+ <iron-autogrow-textarea
+ class="output"
+ id="resultOutput"
+ readonly
+ >
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ </main>
+ `;
+ }
+
+ private validateData() {
+ this.dataValid =
+ this.validateHasValue(this.projectInput.value) &&
+ this.validateHasValue(this.branchInput.value) &&
+ this.validateEmail(this.emailInput.value) &&
+ this.validateHasValue(this.pathInput.value);
+ }
+
+ private validateHasValue(value: string) {
+ if (value && value.trim().length > 0) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private validateEmail(email: string) {
+ if (email && email.includes('@')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private async handleCheckCodeOwner() {
+ this.isChecking = true;
+
+ var project = this.projectInput.value.trim();
+ var branch = this.branchInput.value.trim();
+ var email = this.emailInput.value.trim();
+ var path = this.pathInput.value.trim();
+
+ var url = `/a/projects/${encodeURIComponent(project)}/branches/${encodeURIComponent(branch)}/code_owners.check/?email=${encodeURIComponent(email)}&path=${encodeURIComponent(path)}`;
+ if (this.userInput.value) {
+ url = url + `&user=${encodeURIComponent(this.userInput.value.trim())}`
+ }
+ url = url + "&pp=1";
+
+ try {
+ await this.plugin
+ .restApi()
+ .get<String>(url)
+ .then(response => {
+ this.resultOutput.value = JSON.stringify(response, null, 2);
+ })
+ .catch(error => {
+ this.dispatchEvent(
+ new CustomEvent('show-error', {
+ detail: {message: error},
+ bubbles: true,
+ composed: true,
+ })
+ );
+ });
+ } finally {
+ this.isChecking = false;
+ }
+ }
+}
diff --git a/web/plugin.ts b/web/plugin.ts
index 9270544..2e26843 100644
--- a/web/plugin.ts
+++ b/web/plugin.ts
@@ -28,6 +28,7 @@
OWNER_STATUS_COLUMN_CONTENT,
} from './owner-status-column';
import {CodeOwnersModelMixinInterface} from './code-owners-model-mixin';
+import './gr-check-code-owner';
window.Gerrit.install(plugin => {
const restApi = plugin.restApi();
@@ -105,4 +106,5 @@
(view as unknown as CodeOwnersModelMixinInterface).restApi = restApi;
(view as unknown as CodeOwnersModelMixinInterface).reporting = reporting;
});
+ plugin.screen('check-code-owner', 'gr-check-code-owner');
});