blob: befa4ddb1a556f048c6ed57326dfe38c1e352cc3 [file] [log] [blame] [edit]
/**
* @license
* Copyright (C) 2019 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, property, query, state} from 'lit/decorators';
import {css, CSSResult, html, LitElement} from 'lit';
import {PluginApi} from '@gerritcodereview/typescript-api/plugin';
import {RestPluginApi} from '@gerritcodereview/typescript-api/rest';
import {GroupInfo} from '@gerritcodereview/typescript-api/rest-api';
import {AccountCapabilityInfo} from './plugin';
import {ConfigInfo, ServiceUserInfo} from './gr-serviceuser-create';
import {GrServiceUserSshPanel} from './gr-serviceuser-ssh-panel';
import {GrServiceUserHttpPassword} from './gr-serviceuser-http-password';
import './gr-serviceuser-ssh-panel';
import './gr-serviceuser-http-password';
const NOT_FOUND_MESSAGE = 'Not Found';
@customElement('gr-serviceuser-detail')
export class GrServiceUserDetail extends LitElement {
@query('#sshEditor')
sshEditor!: GrServiceUserSshPanel;
@query('#httpPass')
httpPass!: GrServiceUserHttpPassword;
@query('#serviceUserFullNameInput')
serviceUserFullNameInput!: HTMLInputElement;
@query('#serviceUserEmailInput')
serviceUserEmailInput!: HTMLInputElement;
@query('#serviceUserOwnerInput')
serviceUserOwnerInput!: HTMLInputElement;
@property({type: Object})
plugin!: PluginApi;
@property()
pluginRestApi!: RestPluginApi;
@property({type: String})
serviceUserId?: String;
@property({type: Object})
serviceUser!: ServiceUserInfo;
@state()
loading = true;
@state()
statusButtonText = 'Activate';
@state()
prefsChanged = false;
@state()
changingPrefs = false;
@state()
isAdmin = false;
@state()
emailAllowed = false;
@state()
ownerAllowed = false;
@state()
httpPasswordAllowed = false;
@property({type: String})
fullName?: String;
@property({type: String})
email = '';
@property({type: Array})
availableOwners?: Array<GroupInfo>;
@property({type: String})
owner = NOT_FOUND_MESSAGE;
@property({type: String})
ownerChangeWarning?: String;
override connectedCallback() {
super.connectedCallback();
this.pluginRestApi = this.plugin.restApi();
}
override firstUpdated() {
this.extractUserId();
this.loadServiceUser();
}
static override get styles() {
return [
window.Gerrit.styles.font as CSSResult,
window.Gerrit.styles.form as CSSResult,
window.Gerrit.styles.subPage as CSSResult,
css`
main {
margin: 2em auto;
max-width: 50em;
}
.heading {
font-size: x-large;
font-weight: 500;
}
h1#Title {
margin-bottom: 1em;
}
p#ownerChangeWarning {
margin-top: 1em;
margin-bottom: 1em;
}
span#gr_serviceuser_activity {
border-radius: 1em;
width: 10em;
padding: 0.3em;
font-weight: bold;
text-align: center;
}
span.value {
width: 50%;
}
input.wide {
width: var(--paper-input-container-shared-input-style_-_width);
}
span.Active {
background-color: #9fcc6b;
}
span.Inactive {
background-color: #f7a1ad;
}
`,
];
}
override render() {
return html`
<main class="gr-form-styles read-only">
<div id="loading" class="${this.computeLoadingClass()}">Loading...</div>
<div id="loadedContent" class="${this.computeLoadingClass()}">
<h1 id="Title" class="heading">
Service User "${this.serviceUser?.name}"
</h1>
<div id="form">
<fieldset>
<fieldset>
<h2 id="accountState" class="heading-2">Account State</h2>
<section>
<span class="title">Current State</span>
<span
id="gr_serviceuser_activity"
class="value ${this.active()}"
>
${this.active()}
</span>
</section>
<gr-button
id="statusToggleButton"
@click="${this.toggleStatus}"
?disabled="${this.loading}"
>
${this.statusButtonText}
</gr-button>
</fieldset>
<fieldset>
<h2 id="userDataHeader" class="heading-2">User Data</h2>
<section>
<span class="title">Username</span>
<span class="value">${this.serviceUser?.username}</span>
</section>
${this.renderFullNameFormSection()}
<section>
<span class="title">Email Address</span>
<span class="value"> ${this.renderEmailFormContent()} </span>
</section>
<section>
<span class="title">Owner Group</span>
<span class="value">
${this.renderOwnerGroupFormContent()}
</span>
</section>
<p id="ownerChangeWarning" class="style-scope gr-settings-view">
${this.ownerChangeWarning}
</p>
<gr-button
id="savePrefs"
@click="${this.handleSavePreferences}"
?disabled="${!this.prefsChanged}"
>
Save changes
</gr-button>
</fieldset>
<fieldset>
<h2 id="creationHeader" class="heading-2">Creation</h2>
<section>
<span class="title">Created By</span>
<span class="value">${this.getCreator()}</span>
</section>
<section>
<span class="title">Created At</span>
<span class="value">${this.serviceUser?.created_at}</span>
</section>
</fieldset>
<fieldset>
<h2 id="credentialsHeader" class="heading-2">Credentials</h2>
${this.renderHttpCredentialsForm()}
<fieldset>
<h3 id="SSHKeys">SSH keys</h3>
<gr-serviceuser-ssh-panel
id="sshEditor"
></gr-serviceuser-ssh-panel>
</fieldset>
</fieldset>
</fieldset>
</div>
</div>
</main>
`;
}
private renderFullNameFormSection() {
return html`
<section>
<span class="title">Full Name</span>
<span class="value">
<input
id="serviceUserFullNameInput"
type="text"
class="wide"
.value="${this.fullName}"
.placeholder="${this.serviceUser?.name}"
?disabled="${this.changingPrefs}"
@input="${this.fullNameChanged}"
/>
</span>
</section>
`;
}
private renderEmailFormContent() {
if (this.emailAllowed) {
return html`
<input
id="serviceUserEmailInput"
type="text"
class="wide"
.value="${this.email}"
.placeholder="${this.serviceUser.email ?? ''}"
?disabled="${this.changingPrefs}"
@input="${this.emailChanged}"
/>
`;
}
return html`${this.serviceUser?.email}`;
}
private renderOwnerGroupFormContent() {
if (this.ownerAllowed) {
return html`
<gr-autocomplete
id="serviceUserOwnerInput"
.text="${this.owner}"
.value="${this.owner}"
.query="${(input: string) => this.getGroupSuggestions(input)}"
?disabled="${this.changingPrefs}"
@value-changed="${this.ownerChanged}"
@text-changed="${this.ownerChanged}"
>
${this.getCurrentOwnerGroup()}
</gr-autocomplete>
`;
}
return html`${this.getCurrentOwnerGroup()}`;
}
private renderHttpCredentialsForm() {
if (this.httpPasswordAllowed) {
return html`
<fieldset>
<h3 id="HTTPCredentials">HTTP Credentials</h3>
<fieldset>
<gr-serviceuser-http-password id="httpPass">
</gr-http-password>
</fieldset>
</fieldset>
`;
}
return html``;
}
private loadServiceUser() {
if (!this.serviceUserId) {
return;
}
const promises = [];
promises.push(this.getPluginConfig());
promises.push(this.getServiceUser());
Promise.all(promises).then(() => {
this.sshEditor.loadData(this.pluginRestApi);
if (this.httpPasswordAllowed) {
this.httpPass.loadData(this.pluginRestApi);
}
this.dispatchEvent(
new CustomEvent('title-change', {
detail: {title: this.serviceUser?.name},
bubbles: true,
composed: true,
})
);
this.computeStatusButtonText();
this.loading = false;
this.fullName = this.serviceUser?.name;
this.email = this.serviceUser.email ?? '';
this.owner = this.getCurrentOwnerGroup() ?? NOT_FOUND_MESSAGE;
});
}
private computeLoadingClass() {
return this.loading ? 'loading' : '';
}
private extractUserId() {
this.serviceUserId = this.baseURI.split('/').pop();
}
private getPermissions() {
return this.pluginRestApi
.get<AccountCapabilityInfo>('/accounts/self/capabilities/')
.then(capabilities => {
if (!capabilities) {
this.isAdmin = false;
} else {
this.isAdmin =
capabilities.administrateServer === undefined ? false : true;
}
});
}
private getPluginConfig() {
return this.getPermissions()
.then(() =>
this.pluginRestApi.get<ConfigInfo>('/config/server/serviceuser~config/')
)
.then(config => {
if (!config) {
return;
}
this.emailAllowed = config.allow_email || this.isAdmin;
this.ownerAllowed = config.allow_owner || this.isAdmin;
this.httpPasswordAllowed = config.allow_http_password || this.isAdmin;
});
}
private getServiceUser() {
return this.pluginRestApi
.get(`/a/config/server/serviceuser~serviceusers/${this.serviceUserId}`)
.then(serviceUser => {
if (!serviceUser) {
this.serviceUser = {};
return;
}
this.serviceUser = serviceUser;
});
}
private active() {
if (!this.serviceUser) {
return NOT_FOUND_MESSAGE;
}
return this.serviceUser?.inactive === true ? 'Inactive' : 'Active';
}
private computeStatusButtonText() {
if (!this.serviceUser) {
return;
}
this.statusButtonText =
this.serviceUser?.inactive === true ? 'Activate' : 'Deactivate';
}
private toggleStatus() {
if (this.serviceUser?.inactive === true) {
this.pluginRestApi
.put(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/active`
)
.then(() => {
this.loadServiceUser();
});
} else {
this.pluginRestApi
.delete(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/active`
)
.then(() => {
this.loadServiceUser();
});
}
}
private getCreator() {
if (!this.serviceUser || !this.serviceUser?.created_by) {
return NOT_FOUND_MESSAGE;
}
if (this.serviceUser?.created_by.username !== undefined) {
return this.serviceUser?.created_by.username;
}
if (this.serviceUser?.created_by._account_id !== -1) {
return this.serviceUser?.created_by._account_id;
}
return NOT_FOUND_MESSAGE;
}
private getCurrentOwnerGroup() {
return this.serviceUser && this.serviceUser?.owner
? this.serviceUser?.owner.name
: NOT_FOUND_MESSAGE;
}
private isNewValidEmail(email: String) {
if (!this.serviceUser.email) {
return email.includes('@');
}
return email !== this.serviceUser.email && (email.includes('@') || email.length === 0);
}
private getGroupSuggestions(input: String) {
return this.pluginRestApi
.get<Object>(`/a/groups/?n=10&suggest=${input}`)
.then(response => {
this.availableOwners = Object.values(response);
return Object.keys(response).map(name => {
return {name, value: name};
});
});
}
private isNewOwner() {
if (this.owner === NOT_FOUND_MESSAGE) {
return false;
}
return this.owner !== this.getCurrentOwnerGroup();
}
private computeOwnerWarning() {
let message = 'If ';
message +=
this.getCurrentOwnerGroup() !== NOT_FOUND_MESSAGE
? 'the owner group is changed'
: 'an owner group is set';
message += ' only members of the ';
message += this.getCurrentOwnerGroup() !== NOT_FOUND_MESSAGE ? 'new ' : '';
message += 'owner group can see and administrate the service user.';
message +=
this.getCurrentOwnerGroup() !== NOT_FOUND_MESSAGE
? ''
: ' The creator of the service user can no' +
' longer see and administrate the service user if she/he' +
' is not member of the owner group.';
this.ownerChangeWarning = message;
}
private fullNameChanged() {
this.fullName = this.serviceUserFullNameInput.value;
this.computePrefsChanged();
}
private emailChanged() {
this.email = this.serviceUserEmailInput.value;
this.computePrefsChanged();
}
private ownerChanged() {
this.owner = this.serviceUserOwnerInput.value;
if (this.isNewOwner()) {
this.computeOwnerWarning();
}
this.computePrefsChanged();
}
private computePrefsChanged() {
if (this.loading || this.changingPrefs) {
return;
}
if (
this.owner === this.getCurrentOwnerGroup() &&
!this.isNewValidEmail(this.email) &&
this.fullName === this.serviceUser.name
) {
this.prefsChanged = false;
return;
}
this.prefsChanged = true;
}
private applyNewFullName() {
return this.pluginRestApi.put(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/name`,
{name: this.fullName}
);
}
private applyNewEmail() {
if (!this.isNewValidEmail(this.email)) {
return;
}
return this.pluginRestApi.put(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/email`,
{email: this.email}
);
}
private applyNewOwner() {
if (!this.isNewOwner()) {
return;
}
if (this.owner === '') {
return this.pluginRestApi.delete(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/owner`
);
}
return this.pluginRestApi.put(
`/config/server/serviceuser~serviceusers/${this.serviceUser?._account_id}/owner`,
{group: this.owner}
);
}
private handleSavePreferences() {
const promises = [];
this.changingPrefs = true;
if (this.fullName !== this.serviceUser.name) {
promises.push(this.applyNewFullName());
}
if (this.email !== this.serviceUser.email) {
promises.push(this.applyNewEmail());
}
if (this.owner !== this.serviceUser.owner?.name) {
promises.push(this.applyNewOwner());
}
Promise.all(promises)
.then(() => {
this.prefsChanged = false;
this.ownerChangeWarning = '';
this.loadServiceUser();
})
.catch(error => {
this.dispatchEvent(
new CustomEvent('show-error', {
detail: {message: error},
composed: true,
bubbles: true,
})
);
})
.finally(() => {
this.changingPrefs = false;
});
}
}