vault/ui/app/components/secret-engine/page/general-settings.ts
Vault Automation 06948c7588
[UI][Bugfix]: General Settings Bugfix Pt.1 (#9820) (#9837)
* VAULT-39898 page scroll locked after discarding

* VAULT-39904 update lease duration card text

* Ensure ttl fields dont get reset if there are errors

* Fix failing tests

* VAULT-39900 close the unsaved changes modal if it errors on save

* Switch max and lease duration

* Refactor modal save/close

* Remove promise!

* Code cleanup!

Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
2025-10-03 12:33:36 -07:00

239 lines
7.7 KiB
TypeScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { task } from 'ember-concurrency';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { convertToSeconds } from 'core/utils/duration-utils';
import type Router from '@ember/routing/router';
import type FlashMessageService from 'vault/services/flash-messages';
import type ApiService from 'vault/services/api';
import type SecretsEngineResource from 'vault/resources/secrets/engine';
export const CUSTOM = 'Custom';
export const SYSTEM_DEFAULT = 'System default';
/**
* @module GeneralSettingsComponent is used to configure the SSH secret engine.
*
* @example
* ```js
* <Secrets:Page:GeneralSettings
* @model={{this.model}}
* />
* ```
*
* @param {string} secretsEngine - secrets engine resource
* @param {string} versions - list of versions for a given secret engine
*/
interface Args {
model: {
secretsEngine: SecretsEngineResource;
versions: string[];
};
}
export default class GeneralSettingsComponent extends Component<Args> {
@service declare readonly router: Router;
@service declare readonly api: ApiService;
@service declare readonly flashMessages: FlashMessageService;
@tracked errorMessage: string | null = null;
@tracked invalidFormAlert: string | null = null;
@tracked showUnsavedChangesModal = false;
@tracked changedFields: string[] = [];
@tracked defaultLeaseUnit = '';
@tracked maxLeaseUnit = '';
originalModel = JSON.parse(JSON.stringify(this.args.model));
getUnsavedChanges(newModel: SecretsEngineResource, originalModel: SecretsEngineResource) {
for (const key in this.args.model.secretsEngine) {
const secretsEngineKeyType = key as keyof typeof this.args.model.secretsEngine;
if (secretsEngineKeyType === 'options') {
return;
}
if (secretsEngineKeyType === 'config') {
const { defaultLeaseTime, defaultLeaseUnit, maxLeaseTime, maxLeaseUnit } = this.getFormData();
const hasDefaultTtlValueChanged = this.hasTtlValueChanged(
defaultLeaseTime,
defaultLeaseUnit,
'default_lease_ttl'
);
const hasMaxTtlValueChanged = this.hasTtlValueChanged(maxLeaseTime, maxLeaseUnit, 'max_lease_ttl');
if (
(hasDefaultTtlValueChanged || hasMaxTtlValueChanged) &&
!this.changedFields.includes('Lease Duration')
) {
this.changedFields.push('Lease Duration');
}
} else {
if (newModel[secretsEngineKeyType] !== originalModel[secretsEngineKeyType]) {
this.changedFields.push(key);
}
}
}
}
validateTtl(ttlValue: FormDataEntryValue | number | null) {
if (isNaN(Number(ttlValue))) {
this.errorMessage = 'Only use numbers for this setting.';
return false;
}
return true;
}
hasTtlValueChanged(ttlTime: number, ttlUnit: string, ttlKey: 'max_lease_ttl' | 'default_lease_ttl') {
const defaultLeaseInSecs = convertToSeconds(ttlTime, ttlUnit);
if (defaultLeaseInSecs === this?.originalModel?.secretsEngine?.config[ttlKey]) {
return false;
}
return true;
}
hasDescriptionChanged(description: FormDataEntryValue | null) {
return description !== this?.originalModel?.secretsEngine?.description;
}
hasPluginVersionChanged(version: FormDataEntryValue | null) {
return version && version !== this?.originalModel?.secretsEngine?.running_plugin_version;
}
getFormData() {
const form = document.getElementById('general-settings-form');
const fd = new FormData(form as HTMLFormElement);
const fdDefaultLeaseTime = Number(fd.get('default_lease_ttl-time'));
const fdDefaultLeaseUnit = fd.get('default_lease_ttl-unit')?.toString() || 's';
const fdMaxLeaseTime = Number(fd.get('max_lease_ttl-time'));
const fdMaxLeaseUnit = fd.get('max_lease_ttl-unit')?.toString() || 's';
return {
defaultLeaseTime: fdDefaultLeaseTime,
defaultLeaseUnit: fdDefaultLeaseUnit,
maxLeaseTime: fdMaxLeaseTime,
maxLeaseUnit: fdMaxLeaseUnit,
description: fd.get('description'),
version: fd.get('plugin-version'),
};
}
hasUnsavedChanges() {
const { defaultLeaseTime, defaultLeaseUnit, maxLeaseTime, maxLeaseUnit, description, version } =
this.getFormData();
const hasDefaultTtlValueChanged = this.hasTtlValueChanged(
defaultLeaseTime,
defaultLeaseUnit,
'default_lease_ttl'
);
const hasMaxTtlValueChanged = this.hasTtlValueChanged(maxLeaseTime, maxLeaseUnit, 'max_lease_ttl');
return (
hasDefaultTtlValueChanged ||
hasMaxTtlValueChanged ||
this.hasDescriptionChanged(description) ||
this.hasPluginVersionChanged(version)
);
}
formatTuneParams() {
const { defaultLeaseTime, defaultLeaseUnit, maxLeaseTime, maxLeaseUnit, description, version } =
this.getFormData();
const hasDefaultTtlValueChanged = this.hasTtlValueChanged(
defaultLeaseTime,
defaultLeaseUnit,
'default_lease_ttl'
);
const hasMaxTtlValueChanged = this.hasTtlValueChanged(maxLeaseTime, maxLeaseUnit, 'max_lease_ttl');
const defaultLeaseTtl = hasDefaultTtlValueChanged ? `${defaultLeaseTime}${defaultLeaseUnit}` : undefined;
const maxLeaseTtl = hasMaxTtlValueChanged ? `${maxLeaseTime}${maxLeaseUnit}` : undefined;
const pluginVersion = this.hasPluginVersionChanged(version) ? version : undefined;
const pluginDescription = this.hasDescriptionChanged(description) ? description : undefined;
return {
defaultLeaseTtl,
maxLeaseTtl,
pluginVersion,
pluginDescription,
};
}
@action
openUnsavedChangesModal() {
if (this.hasUnsavedChanges()) {
this.getUnsavedChanges(this.args?.model?.secretsEngine, this?.originalModel?.secretsEngine);
this.showUnsavedChangesModal = true;
} else {
this.showUnsavedChangesModal = false;
}
}
@action
closeUnsavedChangesModal() {
this.showUnsavedChangesModal = false;
this.changedFields = [];
}
@action
closeAndHandle(close: () => void, action: 'save' | 'discard') {
close();
if (action === 'save') {
this.saveGeneralSettings.perform();
}
if (action === 'discard') {
this.router.transitionTo(this.args?.model?.secretsEngine?.backendConfigurationLink);
}
}
saveGeneralSettings = task(async (event?) => {
// event is an optional arg because saveGeneralSettings can be called in the closeAndHandle function.
// There are instances where we will save in the modal where that doesn't require an event.
if (event) event.preventDefault();
const { defaultLeaseTime, defaultLeaseUnit, maxLeaseTime, maxLeaseUnit } = this.getFormData();
this.defaultLeaseUnit = defaultLeaseUnit;
this.maxLeaseUnit = maxLeaseUnit;
if (!this.validateTtl(defaultLeaseTime) || !this.validateTtl(maxLeaseTime)) {
this.errorMessage = 'Only use numbers for this setting.';
return;
}
try {
const { defaultLeaseTtl, maxLeaseTtl, pluginVersion, pluginDescription } = this.formatTuneParams();
await this.api.sys.mountsTuneConfigurationParameters(this.args?.model?.secretsEngine?.id, {
description: pluginDescription as string | undefined,
default_lease_ttl: defaultLeaseTtl,
max_lease_ttl: maxLeaseTtl,
plugin_version: pluginVersion as string | undefined,
});
this.flashMessages.success('Engine settings successfully updated.');
this.router.transitionTo(this.args?.model?.secretsEngine?.backendConfigurationLink);
} catch (e) {
const { message } = await this.api.parseError(e);
this.errorMessage = message;
}
});
}