blob: 9760dfd09823099af590d56c529d0705bb1a17bc [file] [log] [blame]
/**
* @license
* Copyright 2016 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 {SshKeyInfo} from '../../../types/common';
import {GrButton} from '../../shared/gr-button/gr-button';
import {IronAutogrowTextareaElement} from '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
import {getAppContext} from '../../../services/app-context';
import {LitElement, css, html, PropertyValues} 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 {fire} from '../../../utils/event-util';
import {BindValueChangeEvent} from '../../../types/events';
import {modalStyles} from '../../../styles/gr-modal-styles';
declare global {
interface HTMLElementTagNameMap {
'gr-ssh-editor': GrSshEditor;
}
}
@customElement('gr-ssh-editor')
export class GrSshEditor extends LitElement {
@property({type: Boolean})
hasUnsavedChanges = false;
@property({type: Array})
keys: SshKeyInfo[] = [];
@property({type: Object})
keyToView?: SshKeyInfo;
@property({type: String})
newKey = '';
@property({type: Array})
keysToRemove: SshKeyInfo[] = [];
@state() prevHasUnsavedChanges = false;
@query('#addButton') addButton!: GrButton;
@query('#newKey') newKeyEditor!: IronAutogrowTextareaElement;
@query('#viewKeyModal') viewKeyModal!: HTMLDialogElement;
private readonly restApiService = getAppContext().restApiService;
static override get styles() {
return [
grFormStyles,
sharedStyles,
modalStyles,
css`
.statusHeader {
width: 4em;
}
.keyHeader {
width: 7.5em;
}
#viewKeyModal {
padding: var(--spacing-xxl);
width: 50em;
}
.publicKey {
font-family: var(--monospace-font-family);
font-size: var(--font-size-mono);
line-height: var(--line-height-mono);
overflow-x: scroll;
overflow-wrap: break-word;
width: 30em;
}
.closeButton {
bottom: 2em;
position: absolute;
right: 2em;
}
#existing {
margin-bottom: var(--spacing-l);
}
#existing .commentColumn {
min-width: 27em;
width: auto;
}
iron-autogrow-textarea {
background-color: var(--view-background-color);
}
`,
];
}
override updated(changedProperties: PropertyValues) {
if (changedProperties.has('hasUnsavedChanges')) {
if (this.prevHasUnsavedChanges === this.hasUnsavedChanges) return;
this.prevHasUnsavedChanges = this.hasUnsavedChanges;
fire(this, 'has-unsaved-changes-changed', {
value: this.hasUnsavedChanges,
});
}
}
override render() {
return html`
<div class="gr-form-styles">
<fieldset id="existing">
<table>
<thead>
<tr>
<th class="commentColumn">Comment</th>
<th class="statusHeader">Status</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">Algorithm</span>
<span class="value">${this.keyToView?.algorithm}</span>
</section>
<section>
<span class="title">Public key</span>
<span class="value publicKey"
>${this.keyToView?.encoded_key}</span
>
</section>
<section>
<span class="title">Comment</span>
<span class="value">${this.keyToView?.comment}</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 SSH key</span>
<span class="value">
<iron-autogrow-textarea
id="newKey"
autocomplete="on"
placeholder="New SSH Key"
.bindValue=${this.newKey}
@bind-value-changed=${(e: BindValueChangeEvent) => {
this.newKey = e.detail.value ?? '';
}}
></iron-autogrow-textarea>
</span>
</section>
<gr-button
id="addButton"
link=""
?disabled=${!this.newKey.length}
@click=${() => this.handleAddKey()}
>Add new SSH key</gr-button
>
</fieldset>
</div>
`;
}
private renderKey(key: SshKeyInfo, index: number) {
return html` <tr>
<td class="commentColumn">${key.comment}</td>
<td>${key.valid ? 'Valid' : 'Invalid'}</td>
<td>
<gr-button
link=""
@click=${(e: Event) => this.showKey(e)}
data-index=${index}
>Click to View</gr-button
>
</td>
<td>
<gr-copy-clipboard
hasTooltip=""
.buttonTitle=${'Copy SSH public key to clipboard'}
hideInput=""
.text=${key.ssh_public_key}
>
</gr-copy-clipboard>
</td>
<td>
<gr-button
link=""
data-index=${index}
@click=${(e: Event) => this.handleDeleteKey(e)}
>Delete</gr-button
>
</td>
</tr>`;
}
loadData() {
return this.restApiService.getAccountSSHKeys().then(keys => {
if (!keys) return;
this.keys = keys;
});
}
// private but used in tests
save() {
const promises = this.keysToRemove.map(key =>
this.restApiService.deleteAccountSSHKey(`${key.seq}`)
);
return Promise.all(promises).then(() => {
this.keysToRemove = [];
this.hasUnsavedChanges = false;
});
}
private showKey(e: Event) {
const el = e.target as GrButton;
const index = Number(el.getAttribute('data-index')!);
this.keyToView = this.keys[index];
this.viewKeyModal.showModal();
}
private handleDeleteKey(e: Event) {
const el = e.target as GrButton;
const index = Number(el.getAttribute('data-index')!);
this.keysToRemove.push(this.keys[index]);
this.keys.splice(index, 1);
this.requestUpdate();
this.hasUnsavedChanges = true;
}
// private but used in tests
handleAddKey() {
this.addButton.disabled = true;
this.newKeyEditor.disabled = true;
return this.restApiService
.addAccountSSHKey(this.newKey.trim())
.then(key => {
this.newKeyEditor.disabled = false;
this.newKey = '';
this.keys.push(key);
this.requestUpdate();
})
.catch(() => {
this.addButton.disabled = false;
this.newKeyEditor.disabled = false;
});
}
}