Merge "Add ChecksApi types and interface"
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api-types.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api-types.ts
new file mode 100644
index 0000000..f587da1
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api-types.ts
@@ -0,0 +1,370 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Settings
+ *
+ * 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.
+ */
+
+// IMPORTANT !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+// The entire API is currently in DRAFT state.
+// Changes to all type and interfaces are expected.
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+export interface GrChecksApiInterface {
+  /**
+   * Must only be called once. You cannot register twice. You cannot unregister.
+   */
+  register(provider: ChecksProvider, config?: ChecksApiConfig): void;
+
+  /**
+   * Forces Gerrit to call fetch() on the registered provider. Can be called
+   * when the provider has gotten an update and does not want wait for the next
+   * polling interval to pass.
+   */
+  announceUpdate(): void;
+}
+
+export interface ChecksApiConfig {
+  /**
+   * How often should the provider be called for new CheckData while the user
+   * navigates change related pages and the browser tab remains visible?
+   * Set to 0 to disable polling. Default is 60 seconds.
+   */
+  fetchPollingIntervalSeconds: number;
+}
+
+export interface ChecksProvider {
+  /**
+   * Gerrit calls this method when ...
+   * - ... the change or diff page is loaded.
+   * - ... the user switches back to a Gerrit tab with a change or diff page.
+   * - ... while the tab is visible in a regular polling interval, see
+   *       ChecksApiConfig.
+   */
+  fetch(change: number, patchset: number): Promise<FetchResponse>;
+}
+
+export interface FetchResponse {
+  responseCode: ResponseCode;
+
+  /** Only relevant when the responseCode is ERROR. */
+  errorMessage?: string;
+
+  /**
+   * Only relevant when the responseCode is NOT_LOGGED_IN.
+   * Gerrit displays a "Login" button and calls this callback when the user
+   * clicks on the button.
+   */
+  loginCallback?: () => void;
+
+  runs: CheckRun[];
+  results: CheckResult[];
+}
+
+export enum ResponseCode {
+  OK,
+  ERROR,
+  NOT_LOGGED_IN,
+}
+
+/**
+ * A CheckRun models an entity that has start/end timestamps and can be in
+ * either of the states RUNNABLE, RUNNING, COMPLETED. By itself it cannot model
+ * an error, neither can it be failed or successful by itself. A run can be
+ * associated with 0 to n results (see below). So until runs are completed the
+ * runs are more interesting for the user: What is going on at the moment? When
+ * runs are completed the users' interest shifts to results: What do I have to
+ * fix? The only actions that can be associated with runs are RUN and CANCEL.
+ */
+export interface CheckRun {
+  /**
+   * Gerrit requests check runs and results from the plugin by change number and
+   * patchset number. So these two properties can as well be left empty when
+   * returning results to the Gerrit UI and are thus optional.
+   */
+  change?: number;
+  /**
+   * Typically only runs for the latest patchset are requested and presented.
+   * Older runs and their results are only available on request, e.g. by
+   * switching to another patchset in a dropdown
+   *
+   * TBD: CI data providers may decide that runs and results are applicable to a
+   * newer patchset, even if they were produced for an older, e.g. because only
+   * the commit message was changed. Maybe that warrants the addition of another
+   * optional field, e.g. `original_patchset`.
+   */
+  patchset?: number;
+  /**
+   * The UI will focus on just the latest attempt per run. Former attempts are
+   * accessible, but initially collapsed/hidden. Lower number means older
+   * attempt. Every run has its own attempt numbering, so attempt 3 of run A is
+   * not directly related to attempt 3 of run B.
+   *
+   * RUNNABLE runs must use `undefined` as attempt.
+   * COMPLETED and RUNNING runs must use an attempt number >=0.
+   *
+   * TBD: Optionally providing aggregate information about former attempts will
+   * probably be a useful feature, but we are deferring the exact data modeling
+   * of that to later.
+   */
+  attempt?: number;
+
+  /**
+   * An optional opaque identifier not used by Gerrit directly, but might be
+   * used by plugin extensions and callbacks.
+   */
+  externalId?: string;
+
+  // The following 3 properties are independent of this run *instance*. They
+  // just describe what the check is about and will be identical for other
+  // attempts or patchsets or changes.
+
+  /**
+   * The unique name of the check. There can’t be two runs with the same
+   * change/patchset/attempt/checkName combination.
+   * Multiple attempts of the same run must have the same checkName.
+   * It should be expected that this string is cut off at ~30 chars in the UI.
+   * The full name will then be shown in a tooltip.
+   */
+  checkName: string;
+  /**
+   * Optional description of the check. Only shown as a tooltip or in a
+   * hovercard.
+   */
+  checkDescription?: string;
+  /**
+   * Optional http link to an external page with more detailed information about
+   * this run. Must begin with 'http'.
+   */
+  checkLink?: string;
+
+  /**
+   * RUNNABLE:  Not run (yet). Mostly useful for runs that the user can trigger
+   *            (see actions). Cannot contain results.
+   * RUNNING:   Subsumes "scheduled".
+   * COMPLETED: The attempt of the run has finished. Does not indicate at all
+   *            whether the run was successful or not. Outcomes can and should
+   *            be modeled using the CheckResult entity.
+   */
+  status: RunStatus;
+  /**
+   * Optional short description of the run status. This is a plain string
+   * without styling or formatting options. It will only be shown as a tooltip
+   * or in a hovercard.
+   *
+   * Examples:
+   * "40 tests running, 30 completed: 0 failing so far",
+   * "Scheduled 5 minutes ago, not running yet".
+   */
+  statusDescription?: string;
+  /**
+   * Optional http link to an external page with more detailed information about
+   * the run status. Must begin with 'http'.
+   */
+  statusLink?: string;
+
+  /**
+   * Optional reference to a Gerrit label (e.g. "Verified") that this result
+   * influences. Allows the user to understand and navigate the relationship
+   * between CI results and submit requirements,
+   * see also https://gerrit-review.googlesource.com/c/homepage/+/279176.
+   */
+  labelName?: string;
+
+  /**
+   * Optional callbacks to the CI plugin. Must be implemented individually by
+   * each plugin. The most important actions (which get special UI treatment)
+   * are:
+   * "Run" for RUNNABLE and COMPLETED runs.
+   * "Cancel" for RUNNING runs.
+   */
+  actions: Action[];
+
+  scheduledTimestamp?: Date;
+  startedTimestamp?: Date;
+  finishedTimestamp?: Date;
+
+  /**
+   * List of results produced by this run.
+   * RUNNABLE runs must not have results.
+   * RUNNING runs can contain (intermediate) results.
+   * Nesting the results in runs enforces that:
+   * - A run can have 0-n results.
+   * - A result is associated with exactly one run.
+   */
+  results: CheckResult[];
+}
+
+export interface Action {
+  name: string;
+  tooltip?: string;
+  /**
+   * Primary actions will get a more prominent treatment in the UI. For example
+   * primary actions might be rendered as buttons versus just menu entries in
+   * an overflow menu.
+   */
+  primary: boolean;
+  callback: ActionCallback;
+}
+
+export type ActionCallback = (
+  change: number,
+  patchset: number,
+  attempt: number,
+  externalId: string,
+  /** Identical to 'checkName' property of CheckRun. */
+  checkName: string,
+  /** Identical to 'name' property of Action entity. */
+  actionName: string
+) => Promise<ActionResult>;
+
+export interface ActionResult {
+  /** An empty errorMessage means success. */
+  errorMessage?: string;
+}
+
+export enum RunStatus {
+  RUNNABLE,
+  RUNNING,
+  COMPLETED,
+}
+
+export interface CheckResult {
+  /**
+   * An optional opaque identifier not used by Gerrit directly, but might be
+   * used by plugin extensions and callbacks.
+   */
+  externalId?: string;
+
+  /**
+   * INFO:    The user will typically not bother to look into this category,
+   *          only for looking up something that they are searching for. Can be
+   *          used for reporting secondary metrics and analysis, or a wider
+   *          range of artifacts produced by the CI system.
+   * WARNING: A warning is something that should be read before submitting the
+   *          change. The user should not ignore it, but it is also not blocking
+   *          submit. It has a similar level of importance as an unresolved
+   *          comment.
+   * ERROR:   An error indicates that the change must not or cannot be submitted
+   *          without fixing the problem. Errors will be visualized very
+   *          prominently to the user.
+   *
+   * The ‘tags’ field below can be used for further categorization, e.g. for
+   * distinguishing FAILED vs TIMED_OUT.
+   */
+  category: Category;
+
+  /**
+   * Short description of the check result.
+   *
+   * It should be expected that this string might be cut off at ~80 chars in the
+   * UI. The full description will then be shown in a tooltip.
+   * This is a plain string without styling or formatting options.
+   *
+   * Examples:
+   * MessageConverterTest failed with: 'kermit' expected, but got 'ernie'.
+   * Binary size of javascript bundle has increased by 27%.
+   */
+  summary: string;
+
+  /**
+   * Exhaustive optional message describing the check result.
+   * Will be initially collapsed. Might potentially be very long, e.g. a log of
+   * MB size. The UI is not limiting this. CI data providers are responsible for
+   * not killing the browser. :-)
+   *
+   * For now this is just a plain unformatted string. The only formatting
+   * applied is the one that Gerrit also applies to human comments. TBD: Both
+   * human comments and check result messages should get richer formatting
+   * options.
+   */
+  message?: string;
+
+  /**
+   * Tags allow a CI System to further categorize a result, e.g. making a list
+   * of results filterable by the end-user.
+   * The name is free-form, but there is a predefined set of TagColors to
+   * choose from with a recommendation of color for common tags, see below.
+   *
+   * Examples:
+   * PASS, FAIL, SCHEDULED, OBSOLETE, SKIPPED, TIMED_OUT, INFRA_ERROR, FLAKY
+   * WIN, MAC, LINUX
+   * BUILD, TEST, LINT
+   * INTEGRATION, E2E, SCREENSHOT
+   */
+  tags: Tag[];
+
+  /**
+   * Links provide an opportunity for the end-user to easily access details and
+   * artifacts. Links are displayed by an icon+tooltip only. They don’t have a
+   * name, making them clearly distinguishable from tags and actions.
+   *
+   * There is a fixed set of LinkIcons to choose from, see below.
+   *
+   * Examples:
+   * Link to test log.
+   * Link to result artifacts such as images and screenshots.
+   * Link to downloadable artifacts such as ZIP or APK files.
+   */
+  links: Link[];
+
+  /**
+   * Callbacks to the CI plugin. Must be implemented individually by each
+   * plugin. Actions are rendered as buttons. If there are more than two actions
+   * per result, then further actions are put into an overflow menu. Sort order
+   * is defined by the data provider.
+   *
+   * Examples:
+   * Acknowledge/Dismiss, Delete, Report a bug, Report as not useful,
+   * Make blocking, Downgrade severity.
+   */
+  actions: Action[];
+}
+
+export enum Category {
+  INFO,
+  WARNING,
+  ERROR,
+}
+
+export interface Tag {
+  name: string;
+  tooltip?: string;
+  color?: TagColor;
+}
+
+// TBD: Add more ...
+// TBD: Clarify standard colors for common tags.
+export enum TagColor {
+  GRAY,
+  GREEN,
+}
+
+export interface Link {
+  /** Must begin with 'http'. */
+  url: string;
+  tooltip?: string;
+  /**
+   * Primary links will get a more prominent treatment in the UI, e.g. being
+   * always visible in the results table or also showing up in the change page
+   * summary of checks.
+   */
+  primary: boolean;
+  icon: LinkIcon;
+}
+
+// TBD: Add more ...
+export enum LinkIcon {
+  EXTERNAL,
+  DOWNLOAD,
+}
diff --git a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
index bfcff33..50b9222 100644
--- a/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
+++ b/polygerrit-ui/app/elements/plugins/gr-checks-api/gr-checks-api.ts
@@ -15,7 +15,60 @@
  * limitations under the License.
  */
 import {PluginApi} from '../gr-plugin-types';
+import {
+  ChecksApiConfig,
+  ChecksProvider,
+  GrChecksApiInterface,
+  ResponseCode,
+} from './gr-checks-api-types';
 
-export class GrChecksApi {
+const DEFAULT_CONFIG: ChecksApiConfig = {
+  fetchPollingIntervalSeconds: 60,
+};
+
+enum State {
+  NOT_REGISTERED,
+  REGISTERED,
+  FETCHING,
+}
+
+/**
+ * Plugin API for checks.
+ *
+ * This object is created/returned to plugins that want to provide check data.
+ * Plugins normally just call register() once at startup and then wait for
+ * fetch() being called on the provider interface.
+ */
+export class GrChecksApi implements GrChecksApiInterface {
+  private provider?: ChecksProvider;
+
+  config?: ChecksApiConfig;
+
+  private state = State.NOT_REGISTERED;
+
   constructor(readonly plugin: PluginApi) {}
+
+  announceUpdate() {
+    // TODO(brohlfs): Implement!
+  }
+
+  register(provider: ChecksProvider, config?: ChecksApiConfig): void {
+    if (this.state !== State.NOT_REGISTERED || this.provider)
+      throw new Error('Only one provider can be registered per plugin.');
+    this.state = State.REGISTERED;
+    this.provider = provider;
+    this.config = config ?? DEFAULT_CONFIG;
+  }
+
+  async fetch(change: number, patchset: number) {
+    if (this.state === State.NOT_REGISTERED || !this.provider)
+      throw new Error('Cannot fetch checks without a registered provider.');
+    if (this.state === State.FETCHING) return;
+    this.state = State.FETCHING;
+    const response = await this.provider.fetch(change, patchset);
+    this.state = State.REGISTERED;
+    if (response.responseCode === ResponseCode.OK) {
+      // TODO(brohlfs): Do something with the response.
+    }
+  }
 }