blob: 1c5aab53aba1467de85fddd575f58594adb9ef20 [file] [log] [blame]
/**
* @license
* Copyright (C) 2020 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 './gr-repo-chip';
import {customElement, property, state} from 'lit/decorators';
import {css, CSSResult, html, LitElement, PropertyValues} from 'lit';
import {HttpMethod, RestPluginApi} from '@gerritcodereview/typescript-api/rest';
import {Checker} from './types';
import {checked, fire, value} from './util';
const REPOS_PER_PAGE = 6;
const CREATE_CHECKER_URL = '/plugins/checks/checkers/';
const SCHEME_PATTERN = /^[\w-_.]*$/;
declare global {
interface HTMLElementTagNameMap {
'gr-create-checkers-dialog': GrCreateCheckersDialog;
}
}
export type CancelEvent = CustomEvent<CancelEventDetail>;
export interface CancelEventDetail {
reload: boolean;
}
enum Status {
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
}
const statuses = [
{
text: Status.ENABLED,
value: Status.ENABLED,
},
{
text: Status.DISABLED,
value: Status.DISABLED,
},
];
@customElement('gr-create-checkers-dialog')
export class GrCreateCheckersDialog extends LitElement {
@property()
checker?: Checker;
@property()
pluginRestApi!: RestPluginApi;
@state()
name = '';
@state()
scheme = '';
@state()
checkerId = '';
@state()
url = '';
@state()
description = '';
@state()
repo = '';
@state()
errorMsg = '';
@state()
required = false;
@state()
query = '';
@state()
status = Status.ENABLED;
/** True when the checker prop is set. */
@state()
editing = false;
override connectedCallback() {
super.connectedCallback();
this.initCheckerValues();
}
update(changedProperties: PropertyValues) {
if (changedProperties.has('checker')) {
this.initCheckerValues();
}
super.update(changedProperties);
}
static styles = [
window.Gerrit.styles.form as CSSResult,
css`
:host {
display: inline-block;
}
input {
width: 20em;
}
input[disabled] {
background-color: var(--table-subheader-background-color);
}
gr-autocomplete {
border: none;
--gr-autocomplete: {
border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
padding: 0 0.15em;
width: 20em;
}
}
.error {
color: red;
}
`,
];
override render() {
return html`
<div class="gr-form-styles">
<section ?hidden="${this.errorMsg !== ''}">
<span class="error">${this.errorMsg}</span>
</section>
<section>
<span class="title">Name*</span>
<input
.value="${this.name}"
@input="${(e: InputEvent) => (this.name = value(e))}"
autocomplete="on"
/>
</section>
<section>
<span class="title">Description</span>
<input
.value="${this.description}"
@input="${(e: InputEvent) => (this.description = value(e))}"
autocomplete="on"
/>
</section>
<section>
<span class="title">Repository*</span>
<div ?hidden="${this.repo === ''}">
<gr-repo-chip
.repo="${this.repo}"
@remove="${() => (this.repo = '')}"
tabindex="-1"
></gr-repo-chip>
</div>
<div ?hidden="${this.repo !== ''}">
<gr-autocomplete
.query="${(input: string) => this.repoSuggestions(input)}"
@commit="${(e: CustomEvent<{value: string}>) => {
this.repo = e.detail.value;
}}"
clearOnCommit
warnUncommitted
>
</gr-autocomplete>
</div>
</section>
<section>
<span class="title">Scheme*</span>
<input
.value="${this.scheme}"
@input="${(e: InputEvent) => (this.scheme = value(e))}"
?disabled="${this.editing}"
autocomplete="on"
/>
</section>
<section>
<span class="title">ID*</span>
<input
.value="${this.checkerId}"
@input="${(e: InputEvent) => (this.checkerId = value(e))}"
?disabled="${this.editing}"
autocomplete="on"
/>
</section>
<section>
<span class="title">Url</span>
<input
.value="${this.url}"
@input="${(e: InputEvent) => (this.url = value(e))}"
autocomplete="on"
/>
</section>
<section>
<span class="title">UUID</span>
<span class="title uuid">${this.getUuid()}</span>
</section>
<section>
<span class="title">Status</span>
<gr-dropdown-list
text="Status"
.items="${statuses}"
.value="${this.status}"
@value-change="${(e: CustomEvent<{value: Status}>) =>
(this.status = e.detail.value)}"
>
</gr-dropdown-list>
</section>
<section>
<span class="title">Required</span>
<input
type="checkbox"
?checked="${this.required}"
@input="${(e: InputEvent) => (this.required = checked(e))}"
/>
</section>
<section>
<span class="title">Query</span>
<input
.value="${this.query}"
@input="${(e: InputEvent) => (this.query = value(e))}"
autocomplete="on"
/>
</section>
</div>
`;
}
initCheckerValues() {
if (!this.checker) return;
// If a checker was set by the parent component, then that means that we are
// in editing mode of an existing checker.
this.editing = true;
this.scheme = this.checker.uuid.split(':')[0];
this.checkerId = this.checker.uuid.split(':')[1];
this.name = this.checker.name ?? '';
this.description = this.checker.description ?? '';
this.url = this.checker.url ?? '';
this.query = this.checker.query ?? '';
this.required = !!this.checker.blocking?.length;
this.repo = this.checker.repository ?? '';
this.status = (this.checker.status as Status) ?? Status.ENABLED;
}
cleanUp() {
this.name = '';
this.scheme = '';
this.checkerId = '';
this.url = '';
this.description = '';
this.repo = '';
this.errorMsg = '';
this.required = false;
this.status = Status.ENABLED;
this.editing = false;
this.query = '';
fire(this, 'cancel', {reload: true});
}
getUuid(): string {
return `${this.scheme}:${this.checkerId}`;
}
getCheckerRequestObject(): Checker {
return {
name: this.name ?? '',
description: this.description ?? '',
uuid: this.getUuid(),
repository: this.repo,
url: this.url,
status: this.status,
blocking: this.required ? ['STATE_NOT_PASSING'] : [],
query: this.query,
};
}
validateRequest() {
if (!this.name) {
this.errorMsg = 'Name cannot be empty';
return false;
}
if (this.description.length > 1000) {
this.errorMsg = 'Description should be less than 1000 characters';
return false;
}
if (!this.repo) {
this.errorMsg = 'Select a repository';
return false;
}
if (!this.scheme) {
this.errorMsg = 'Scheme cannot be empty.';
return false;
}
if (this.scheme.match(SCHEME_PATTERN) === null) {
this.errorMsg =
'Scheme must contain [A-Z], [a-z], [0-9] or {"-" , "_" , "."}';
return false;
}
if (this.scheme.length > 100) {
this.errorMsg = 'Scheme must be shorter than 100 characters';
return false;
}
if (!this.checkerId) {
this.errorMsg = 'ID cannot be empty.';
return false;
}
if (this.checkerId.match(SCHEME_PATTERN) === null) {
this.errorMsg =
'ID must contain [A-Z], [a-z], [0-9] or {"-" , "_" , "."}';
return false;
}
return true;
}
handleEditChecker() {
if (!this.validateRequest()) return;
this.pluginRestApi
.send(
HttpMethod.POST,
CREATE_CHECKER_URL + this.getCheckerRequestObject().uuid,
this.getCheckerRequestObject()
)
.then(
res => {
if (res) {
this.errorMsg = '';
fire<CancelEventDetail>(this, 'cancel', {reload: true});
}
},
error => {
this.errorMsg = error;
}
);
}
handleCreateChecker() {
if (!this.validateRequest()) return;
// Currently after creating checker there is no reload happening (as
// this would result in the user exiting the screen).
this.pluginRestApi
.send(HttpMethod.POST, CREATE_CHECKER_URL, this.getCheckerRequestObject())
.then(
res => {
if (res) this.cleanUp();
},
error => {
this.errorMsg = error;
}
);
}
repoSuggestions(filter: string) {
return this.pluginRestApi.getRepos(filter, REPOS_PER_PAGE).then(repos =>
((repos ?? []) as {name: string}[]).map(repo => {
return {name: repo.name, value: repo.name};
})
);
}
}