blob: 61cc9edb0c17b02c70d797d59a12808ff50c1999 [file] [log] [blame]
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import '../../shared/gr-button/gr-button';
import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
import {GpgKeyInfo, GpgKeyId} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea';
import {getAppContext} from '../../../services/app-context';
import {css, html, LitElement} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {grFormStyles} from '../../../styles/gr-form-styles';
import {sharedStyles} from '../../../styles/shared-styles';
import {assertIsDefined} from '../../../utils/common-util';
import {BindValueChangeEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
import {modalStyles} from '../../../styles/gr-modal-styles';
import {formStyles} from '../../../styles/form-styles';
declare global {
interface HTMLElementTagNameMap {
'gr-gpg-editor': GrGpgEditor;
}
}
@customElement('gr-gpg-editor')
export class GrGpgEditor extends LitElement {
@query('#viewKeyModal') viewKeyModal?: HTMLDialogElement;
@query('#addButton') addButton?: GrButton;
@query('#newKey') newKeyTextarea?: IronAutogrowTextareaElement;
@property({type: Boolean})
hasUnsavedChanges = false;
// private but used in test
@state() keys: GpgKeyInfo[] = [];
// private but used in test
@state() keyToView?: GpgKeyInfo;
// private but used in test
@state() newKey = '';
// private but used in test
@state() keysToRemove: GpgKeyInfo[] = [];
private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
grFormStyles,
formStyles,
sharedStyles,
modalStyles,
css`
.keyHeader {
width: 9em;
}
.userIdHeader {
width: 15em;
}
#viewKeyModal {
padding: var(--spacing-xxl);
width: 50em;
}
.closeButton {
bottom: 2em;
position: absolute;
right: 2em;
}
#existing {
margin-bottom: var(--spacing-l);
}
iron-autogrow-textarea {
background-color: var(--view-background-color);
}
`,
];
}
override render() {
return html`
<div class="gr-form-styles">
<fieldset id="existing">
<table>
<thead>
<tr>
<th class="idColumn">ID</th>
<th class="fingerPrintColumn">Fingerprint</th>
<th class="userIdHeader">User IDs</th>
<th class="keyHeader">Public Key</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
${this.keys.map((key, index) => this.renderKey(key, index))}
</tbody>
</table>
<dialog id="viewKeyModal" tabindex="-1">
<fieldset>
<section>
<span class="title">Status</span>
<span class="value">${this.keyToView?.status}</span>
</section>
<section>
<span class="title">Key</span>
<span class="value">${this.keyToView?.key}</span>
</section>
</fieldset>
<gr-button
class="closeButton"
@click=${() => {
this.viewKeyModal?.close();
}}
>Close</gr-button
>
</dialog>
<gr-button @click=${this.save} ?disabled=${!this.hasUnsavedChanges}
>Save changes</gr-button
>
</fieldset>
<fieldset>
<section>
<span class="title">New GPG key</span>
<span class="value">
<iron-autogrow-textarea
id="newKey"
autocomplete="on"
.bindValue=${this.newKey}
@bind-value-changed=${(e: BindValueChangeEvent) =>
this.handleNewKeyChanged(e)}
placeholder="New GPG Key"
></iron-autogrow-textarea>
</span>
</section>
<gr-button
id="addButton"
?disabled=${!this.newKey?.length}
@click=${this.handleAddKey}
>Add new GPG key</gr-button
>
</fieldset>
</div>
`;
}
private renderKey(key: GpgKeyInfo, index: number) {
return html`<tr>
<td class="idColumn">${key.id}</td>
<td class="fingerPrintColumn">${key.fingerprint}</td>
<td class="userIdHeader">${key.user_ids?.map(id => html`${id}`)}</td>
<td class="keyHeader">
<gr-button @click=${() => this.showKey(key)} link=""
>Click to View</gr-button
>
</td>
<td>
<gr-copy-clipboard
hasTooltip
buttonTitle="Copy GPG public key to clipboard"
hideInput
.text=${key.key}
>
</gr-copy-clipboard>
</td>
<td>
<gr-button @click=${() => this.handleDeleteKey(index)}
>Delete</gr-button
>
</td>
</tr>`;
}
// private but used in test
loadData() {
this.keys = [];
return this.restApiService.getAccountGPGKeys().then(keys => {
if (!keys) {
return;
}
this.keys = Object.keys(keys).map(key => {
const gpgKey = keys[key];
gpgKey.id = key as GpgKeyId;
return gpgKey;
});
});
}
// private but used in test
save() {
const promises = this.keysToRemove.map(key =>
this.restApiService.deleteAccountGPGKey(key.id!)
);
return Promise.all(promises).then(() => {
this.keysToRemove = [];
this.setHasUnsavedChanges(false);
});
}
private showKey(key: GpgKeyInfo) {
this.keyToView = key;
this.viewKeyModal?.showModal();
}
private handleNewKeyChanged(e: BindValueChangeEvent) {
this.newKey = e.detail.value ?? '';
}
private handleDeleteKey(index: number) {
this.keysToRemove.push(this.keys[index]);
this.keys.splice(index, 1);
this.requestUpdate();
this.setHasUnsavedChanges(true);
}
// private but used in test
handleAddKey() {
assertIsDefined(this.newKeyTextarea);
assertIsDefined(this.addButton);
this.addButton.disabled = true;
this.newKeyTextarea.disabled = true;
return this.restApiService
.addAccountGPGKey({add: [this.newKey.trim()]})
.then(() => {
assertIsDefined(this.newKeyTextarea);
this.newKeyTextarea.disabled = false;
this.newKey = '';
this.loadData();
})
.catch(() => {
assertIsDefined(this.newKeyTextarea);
assertIsDefined(this.addButton);
this.addButton.disabled = false;
this.newKeyTextarea.disabled = false;
});
}
private setHasUnsavedChanges(value: boolean) {
this.hasUnsavedChanges = value;
fire(this, 'has-unsaved-changes-changed', {value});
}
}