mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-19 02:49:18 -05:00
* UI: VAULT-39172 VAULT-38567 general settings followup (#8910) * Add unsaved changes fields * Set up default values for TTL and update general-settings * Add form error state * Ass TODO cmment * Move actions back! * Update unsaved changes state * Address comments and add TODOs * UI: VAULT-39264 Lease Duration TTL picker (#9080) * Update default and max ttl to show correct default * Query sys/internal endpoint for ttl values * WIP ttl-picker-v2 * Intialize values and check for if ttl value is unset * Use ttlKey instead of name * Set name to be ttlKey * Show validation for ttl picker * Fix validation bugs * Remove lease duration files * Add copyright headers * Initalize only when its a custom value * Update ttl-picker to not have a dropdown * Validate field before converting to secs * [UI] Fix styling and update version card component (#9214) * Fix styling and update version card component * Update unsaved changes * Code cleanup * More code cleanup! * Add helper function * Remove query for lease duration * Fix outstanding issues * Captialize unsaved changes * Update util name * Remove action helper * [UI]: General Settings design feedback updates (#9257) * Small refactor based on design feedback * More refactoring! * Rename variables so it makes more sense! * Remove unused modal fields Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
parent
5ead15b8f2
commit
91eabbd0db
13 changed files with 460 additions and 177 deletions
|
|
@ -2,11 +2,11 @@
|
|||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-m has-top-bottom-margin" ...attributes>
|
||||
<Hds::Text::Display @size="300" @tag="h2" class="has-bottom-margin-s hds-foreground-strong">Lease Duration</Hds::Text::Display>
|
||||
|
||||
<Hds::Layout::Flex @align="start" @gap="16" class="has-bottom-margin-m">
|
||||
<Hds::Layout::Flex @direction="column" @gap="4">
|
||||
<Hds::Text::Display @size="300" @tag="h5">Lease Duration</Hds::Text::Display>
|
||||
<Hds::Text::Body @size="200" @tag="p">Lease, measured by the “time-to-live” value, defines how long any secret issued
|
||||
by this engine remains valid.</Hds::Text::Body>
|
||||
{{! TODO: Verify what link this is supposed to be }}
|
||||
|
|
@ -20,49 +20,7 @@
|
|||
</Hds::Layout::Flex>
|
||||
|
||||
<Hds::Layout::Flex @direction="column" @gap="24" @isInline="true" class="has-top-padding-s has-bottom-padding-s">
|
||||
<Hds::Form::Select::Field name="TTL-select" @width="150px" {{on "input" this.setTTLType}} as |F|>
|
||||
<F.Label>Time-to-live (TTL)</F.Label>
|
||||
<F.HelperText>Standard expiry deadline.</F.HelperText>
|
||||
<F.Options>
|
||||
<option value="System default" selected>System default</option>
|
||||
<option value="Custom">Custom</option>
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Field>
|
||||
|
||||
{{#if this.enableTTL}}
|
||||
<Hds::SegmentedGroup as |SG|>
|
||||
<SG.TextInput @width="100px" size="32" @value={{this.time}} name="time" {{on "input" this.setTtlTime}} />
|
||||
<SG.Select @width="100px" {{on "input" this.setUnit}} as |S|>
|
||||
<S.Options>
|
||||
<option value="s" selected>seconds</option>
|
||||
<option value="m">minutes</option>
|
||||
<option value="h">hours</option>
|
||||
<option value="d">days</option>
|
||||
</S.Options>
|
||||
</SG.Select>
|
||||
</Hds::SegmentedGroup>
|
||||
{{/if}}
|
||||
<Hds::Form::Select::Field name="max-TTL-select" @width="150px" {{on "input" this.setMaxTTLType}} as |F|>
|
||||
<F.Label>Maximum Time-to-live (TTL)</F.Label>
|
||||
<F.HelperText>Maximum possible extension for expiry.</F.HelperText>
|
||||
<F.Options>
|
||||
<option value="System default" selected>System default</option>
|
||||
<option value="Custom">Custom</option>
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Field>
|
||||
|
||||
{{#if this.enableMaxTTL}}
|
||||
<Hds::SegmentedGroup as |SG|>
|
||||
<SG.TextInput @width="100px" size="32" @value={{this.maxTime}} name="max-time" {{on "input" this.setMaxTtlTime}} />
|
||||
<SG.Select @width="100px" {{on "input" this.setMaxUnit}} as |S|>
|
||||
<S.Options>
|
||||
<option value="s" selected>seconds</option>
|
||||
<option value="m">minutes</option>
|
||||
<option value="h">hours</option>
|
||||
<option value="d">days</option>
|
||||
</S.Options>
|
||||
</SG.Select>
|
||||
</Hds::SegmentedGroup>
|
||||
{{/if}}
|
||||
<SecretEngine::TtlPickerV2 @model={{@model}} @ttlKey="default_lease_ttl" />
|
||||
<SecretEngine::TtlPickerV2 @model={{@model}} @ttlKey="max_lease_ttl" />
|
||||
</Hds::Layout::Flex>
|
||||
</Hds::Card::Container>
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { action } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { HTMLElementEvent } from 'vault/forms';
|
||||
import SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
interface Args {
|
||||
model: SecretsEngineResource;
|
||||
}
|
||||
|
||||
export default class LeaseDuration extends Component<Args> {
|
||||
// TODO: When wiring up to parent, address variable names and usage, update onchange functions etc - reference ttl-picker.js
|
||||
@tracked enableTTL = false;
|
||||
@tracked enableMaxTTL = false;
|
||||
@tracked time = '';
|
||||
@tracked maxTime = '';
|
||||
@tracked unit = 's';
|
||||
@tracked maxUnit = 's';
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
}
|
||||
|
||||
@action
|
||||
setTTLType(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
if (event.target.value === 'Custom') {
|
||||
this.enableTTL = true;
|
||||
} else {
|
||||
this.enableTTL = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setMaxTTLType(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
if (event.target.value === 'Custom') {
|
||||
this.enableMaxTTL = true;
|
||||
} else {
|
||||
this.enableMaxTTL = false;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
setTtlTime(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
this.time = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
setMaxTtlTime(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
this.maxTime = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
setUnit(event: HTMLElementEvent<HTMLSelectElement>) {
|
||||
this.unit = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
setMaxUnit(event: HTMLElementEvent<HTMLSelectElement>) {
|
||||
this.maxUnit = event.target.value;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
|
||||
<Hds::Text::Display @size="300">Metadata</Hds::Text::Display>
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-m has-top-bottom-margin-12" ...attributes>
|
||||
<Hds::Text::Display @size="300" @tag="h2" class="has-bottom-margin-s hds-foreground-strong">Metadata</Hds::Text::Display>
|
||||
|
||||
<div class="flex gap-16 is-flex-column has-top-padding-s">
|
||||
<Hds::Form::Fieldset @layout="vertical" as |F|>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,21 @@
|
|||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-m has-top-bottom-margin" ...attributes>
|
||||
<Hds::Form::Toggle::Group as |G|>
|
||||
<G.Legend>
|
||||
<Hds::Text::Display @size="300">Security</Hds::Text::Display>
|
||||
<Hds::Text::Display
|
||||
@size="300"
|
||||
@tag="h2"
|
||||
class="has-bottom-margin-s hds-foreground-strong"
|
||||
>Security</Hds::Text::Display>
|
||||
</G.Legend>
|
||||
{{! TODO: Toggle fields here are currently hardcoded, will be replaced with actual data from model once wired into parent component }}
|
||||
<G.ToggleField name="local" checked={{@model.secretsEngine.local}} as |F|>
|
||||
{{! TODO: Confirm with design to see if we want these two fields to be disabled }}
|
||||
<G.ToggleField name="local" checked={{@model.secretsEngine.local}} disabled as |F|>
|
||||
<F.Label>Local</F.Label>
|
||||
<F.HelperText>Secrets stay in one cluster and are not replicated.</F.HelperText>
|
||||
</G.ToggleField>
|
||||
<G.ToggleField name="seal-wrap" checked={{@model.secretsEngine.seal_wrap}} as |F|>
|
||||
<G.ToggleField name="seal-wrap" checked={{@model.secretsEngine.seal_wrap}} disabled as |F|>
|
||||
<F.Label>Seal wrap</F.Label>
|
||||
<F.HelperText>Wrap secrets with an additional encryption layer using a seal.</F.HelperText>
|
||||
</G.ToggleField>
|
||||
|
|
|
|||
|
|
@ -2,40 +2,38 @@
|
|||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-s has-top-bottom-margin-12" ...attributes>
|
||||
<Hds::Text::Display @size="300">Version</Hds::Text::Display>
|
||||
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-m has-top-bottom-margin" ...attributes>
|
||||
<Hds::Text::Display @size="300" @tag="h2" class="has-bottom-margin-s hds-foreground-strong">Version</Hds::Text::Display>
|
||||
|
||||
{{! TODO: Having this set up with flex for now, grid might be better? }}
|
||||
<div class="flex gap-36 has-top-margin-l">
|
||||
<Hds::Layout::Flex @direction="column" @gap="8">
|
||||
<Hds::Text::Display>Engine type</Hds::Text::Display>
|
||||
<Hds::Text::Display>Current version</Hds::Text::Display>
|
||||
<Hds::Text::Display>Latest version</Hds::Text::Display>
|
||||
</Hds::Layout::Flex>
|
||||
<Hds::Layout::Grid @columnMinWidth="10%" @gap="12" {{style height="100%" grid-template-rows="min-content"}} as |LG|>
|
||||
<LG.Item @colspan={{2}}>
|
||||
<Hds::Text::Body @tag="h3" class="hds-font-weight-semibold has-bottom-margin-s hds-foreground-strong">Engine type</Hds::Text::Body>
|
||||
<Hds::Text::Body @tag="h3" class="hds-font-weight-semibold has-bottom-margin-s hds-foreground-strong">Current version</Hds::Text::Body>
|
||||
</LG.Item>
|
||||
|
||||
<Hds::Layout::Flex @direction="column" @gap="12" @align="start">
|
||||
<LG.Item @colspan={{3}}>
|
||||
<Hds::Text::Body
|
||||
@tag="p"
|
||||
class="hds-border-strong side-padding-4 border-radius-4"
|
||||
class="hds-border-strong has-side-padding-8 border-radius-4 is-inline-block has-bottom-margin-s"
|
||||
>{{@model.secretsEngine.type}}</Hds::Text::Body>
|
||||
{{! TODO: Verify if we want to display the full version or chop down ie. v0.17.1 vs v0.17.1-0.230942309423094... }}
|
||||
<Hds::Text::Body @tag="p">{{@model.secretsEngine.running_plugin_version}}</Hds::Text::Body>
|
||||
{{! TODO: leaving as is for now to match design, but we might be removing this if we cant get latest version from some source }}
|
||||
<Hds::Text::Body @tag="p">v.12.46</Hds::Text::Body>
|
||||
</LG.Item>
|
||||
</Hds::Layout::Grid>
|
||||
|
||||
{{#if (gt @model.versions.length 1)}}
|
||||
<Hds::Separator />
|
||||
|
||||
<Hds::Layout::Flex @isInline="true">
|
||||
<Hds::Form::Select::Field name="plugin-version" as |F|>
|
||||
<F.Label>Update version to:</F.Label>
|
||||
<F.Options>
|
||||
<option value="">Select version</option>
|
||||
{{#each @model.versions as |version|}}
|
||||
<option value={{version}}>{{version}}</option>
|
||||
{{/each}}
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Field>
|
||||
</Hds::Layout::Flex>
|
||||
</div>
|
||||
|
||||
<Hds::Separator />
|
||||
|
||||
<Hds::Layout::Flex @isInline="true">
|
||||
<Hds::Form::Select::Field name="version-select" as |F|>
|
||||
<F.Label>Update version to:</F.Label>
|
||||
{{! TODO: Update options with available versions from API }}
|
||||
<F.Options>
|
||||
<option value="">Select version</option>
|
||||
<option value="12.4">12.4</option>
|
||||
<option value="12.43">12.43</option>
|
||||
</F.Options>
|
||||
</Hds::Form::Select::Field>
|
||||
</Hds::Layout::Flex>
|
||||
{{/if}}
|
||||
</Hds::Card::Container>
|
||||
|
|
@ -9,13 +9,13 @@
|
|||
<PH.Description>{{get engineData "displayName"}}</PH.Description>
|
||||
<PH.Breadcrumb>
|
||||
<Hds::Breadcrumb>
|
||||
<Hds::Breadcrumb::Item @text="Secrets" />
|
||||
<Hds::Breadcrumb::Item @text="Secrets" @route="vault.cluster.secrets" />
|
||||
<Hds::Breadcrumb::Item
|
||||
@text={{@model.secretsEngine.id}}
|
||||
@route="vault.cluster.secrets.backend.list-root"
|
||||
@model={{@model.secretsEngine.id}}
|
||||
/>
|
||||
<Hds::Breadcrumb::Item @text="Configuration" />
|
||||
<Hds::Breadcrumb::Item @text="Configuration" @current={{true}} />
|
||||
</Hds::Breadcrumb>
|
||||
</PH.Breadcrumb>
|
||||
<PH.IconTile @icon={{get engineData "glyph"}} />
|
||||
|
|
|
|||
|
|
@ -14,39 +14,39 @@
|
|||
<Hds::Text::Display @tag="h2">Saving configuration...</Hds::Text::Display>
|
||||
</Hds::Layout::Flex>
|
||||
{{else}}
|
||||
<form method="POST" {{on "submit" (perform this.saveGeneralSettings)}} aria-label="general settings form">
|
||||
<form
|
||||
method="POST"
|
||||
{{on "submit" (perform this.saveGeneralSettings)}}
|
||||
aria-label="general settings form"
|
||||
id="general-settings-form"
|
||||
>
|
||||
<Hds::Text::Body class="has-top-bottom-margin-xxs">
|
||||
Mount parameters that you can tune to fit required engine behavior.
|
||||
</Hds::Text::Body>
|
||||
|
||||
<Hds::Layout::Flex @direction="row" @gap="32" @align="start">
|
||||
<Hds::Layout::Flex @direction="column" @gap="8">
|
||||
<MessageError @errorMessage={{this.errorMessage}} />
|
||||
|
||||
<Hds::Layout::Grid @columnMinWidth="32%" @gap="32" as |LG|>
|
||||
<LG.Item @colspan={{1}}>
|
||||
<SecretEngine::Card::Version @model={{@model}} class="is-fullwidth" />
|
||||
<SecretEngine::Card::Metadata @model={{@model}} class="is-fullwidth" />
|
||||
</Hds::Layout::Flex>
|
||||
|
||||
<Hds::Layout::Flex @direction="column" @gap="8">
|
||||
</LG.Item>
|
||||
<LG.Item @colspan={{1}}>
|
||||
<SecretEngine::Card::LeaseDuration @model={{@model}} class="is-fullwidth" />
|
||||
<SecretEngine::Card::Security @model={{@model}} class="is-fullwidth" />
|
||||
</Hds::Layout::Flex>
|
||||
</Hds::Layout::Flex>
|
||||
|
||||
<div class="field is-grouped has-top-bottom-margin-12">
|
||||
</LG.Item>
|
||||
</Hds::Layout::Grid>
|
||||
<div class="field is-grouped has-top-bottom-margin">
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button @text="Save changes" type="submit" disabled={{this.saveGeneralSettings.isRunning}} data-test-submit />
|
||||
<Hds::Button
|
||||
@text="Discard"
|
||||
@color="secondary"
|
||||
data-test-cancel
|
||||
{{on "click" (fn (mut this.showUnsavedChangesModal))}}
|
||||
/>
|
||||
<Hds::Button @text="Discard" @color="secondary" data-test-cancel {{on "click" this.openUnsavedChangesModal}} />
|
||||
</Hds::ButtonSet>
|
||||
</div>
|
||||
</form>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.showUnsavedChangesModal}}
|
||||
<Hds::Modal id="unsavedChangesModal" @onClose={{(fn (mut this.showUnsavedChangesModal))}} as |M|>
|
||||
<Hds::Modal id="unsavedChangesModal" @onClose={{this.closeUnsavedChangesModal}} as |M|>
|
||||
<M.Header>
|
||||
Unsaved changes
|
||||
</M.Header>
|
||||
|
|
@ -54,14 +54,19 @@
|
|||
<p class="hds-typography-body-300 hds-foreground-primary">You've made changes to the following
|
||||
<Hds::Text::Display>{{@model.secretsEngine.id}}</Hds::Text::Display>
|
||||
settings:</p>
|
||||
{{! TODO: display what fields were changed }}
|
||||
<br />
|
||||
{{#each this.changedFields as |changedField|}}
|
||||
<Hds::Text::Display>{{capitalize changedField}}</Hds::Text::Display>
|
||||
<br />
|
||||
{{/each}}
|
||||
<br />
|
||||
Would you like to apply them?
|
||||
</M.Body>
|
||||
<M.Footer as |F|>
|
||||
<Hds::Button type="button" @text="Save changes" {{on "click" (action (perform this.saveGeneralSettings))}} />
|
||||
{{! TODO: confirm with design where we want to transition to if Discard changes is clicked }}
|
||||
<Hds::Button type="button" @text="Discard changes" @color="secondary" {{on "click" F.close}} />
|
||||
<M.Footer>
|
||||
<Hds::ButtonSet>
|
||||
<Hds::Button type="button" @text="Save changes" {{on "click" (perform this.saveGeneralSettings)}} />
|
||||
<Hds::Button type="button" @text="Discard changes" @color="secondary" {{on "click" this.discardChanges}} />
|
||||
</Hds::ButtonSet>
|
||||
</M.Footer>
|
||||
</Hds::Modal>
|
||||
{{/if}}
|
||||
|
|
@ -7,12 +7,17 @@ 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.
|
||||
*
|
||||
|
|
@ -24,11 +29,13 @@ import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
|||
* ```
|
||||
*
|
||||
* @param {string} secretsEngine - secrets engine resource
|
||||
* @param {string} versions - list of versions for a given secret engine
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: {
|
||||
secretsEngine: SecretsEngineResource;
|
||||
versions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -40,20 +47,177 @@ export default class GeneralSettingsComponent extends Component<Args> {
|
|||
@tracked errorMessage: string | null = null;
|
||||
@tracked invalidFormAlert: string | null = null;
|
||||
@tracked showUnsavedChangesModal = false;
|
||||
@tracked changedFields: string[] = [];
|
||||
|
||||
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 = !this.showUnsavedChangesModal;
|
||||
this.changedFields = [];
|
||||
}
|
||||
|
||||
@action
|
||||
discardChanges() {
|
||||
this.closeUnsavedChangesModal();
|
||||
this.router.transitionTo(this.args?.model?.secretsEngine?.backendConfigurationLink);
|
||||
}
|
||||
|
||||
saveGeneralSettings = task(async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const { defaultLeaseTime, maxLeaseTime } = this.getFormData();
|
||||
|
||||
if (!this.validateTtl(defaultLeaseTime) || !this.validateTtl(maxLeaseTime)) {
|
||||
this.errorMessage = 'Only use numbers for this setting.';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fd = new FormData(event.target as HTMLFormElement);
|
||||
await this.api.sys.mountsTuneConfigurationParameters(this.args.model.secretsEngine.id, {
|
||||
// TODO: add other params when other card components are made
|
||||
description: fd.get('description') as string,
|
||||
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) {
|
||||
// handle error state
|
||||
const { message } = await this.api.parseError(e);
|
||||
this.errorMessage = message;
|
||||
this.flashMessages.danger(`Try again or check your network connection. ${message}`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
31
ui/app/components/secret-engine/ttl-picker-v2.hbs
Normal file
31
ui/app/components/secret-engine/ttl-picker-v2.hbs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
<Hds::Form::Field @layout="vertical" @isInvalid={{this.errorMessage}} as |F|>
|
||||
<F.Label>{{this.formField.label}}</F.Label>
|
||||
<F.HelperText>{{this.formField.helperText}}</F.HelperText>
|
||||
<F.Control>
|
||||
<Hds::SegmentedGroup as |SG|>
|
||||
<SG.TextInput
|
||||
@width="100px"
|
||||
size="32"
|
||||
@value={{this.time}}
|
||||
{{on "input" this.setTtlTime}}
|
||||
name="{{@ttlKey}}-time"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<SG.Select @width="100px" name="{{@ttlKey}}-unit" {{on "input" this.setUnit}} as |S|>
|
||||
<S.Options>
|
||||
{{#each this.unitOptions as |unit|}}
|
||||
<option value={{unit.value}} selected={{eq unit.value this.selectedUnit}}>{{unit.label}}</option>
|
||||
{{/each}}
|
||||
</S.Options>
|
||||
</SG.Select>
|
||||
</Hds::SegmentedGroup>
|
||||
</F.Control>
|
||||
{{#if this.errorMessage}}
|
||||
<F.Error>{{this.errorMessage}}</F.Error>
|
||||
{{/if}}
|
||||
</Hds::Form::Field>
|
||||
113
ui/app/components/secret-engine/ttl-picker-v2.ts
Normal file
113
ui/app/components/secret-engine/ttl-picker-v2.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { convertFromSeconds, durationToSeconds, largestUnitFromSeconds } from 'core/utils/duration-utils';
|
||||
import { CUSTOM, SYSTEM_DEFAULT } from './page/general-settings';
|
||||
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { HTMLElementEvent } from 'vault/forms';
|
||||
|
||||
/**
|
||||
* @module TtlPickerV2 handles the display of the ttl picker fo the lease duration card in general settings.
|
||||
*
|
||||
* @example
|
||||
* <SecretEngine::TtlPickerV2
|
||||
@model={{this.model}}
|
||||
@isDefaultTtlPicker={{boolean}}
|
||||
/>
|
||||
*
|
||||
* @param {object} model - A model contains a secret engine resource, lease config from the sys/internal endpoint.
|
||||
* @param {boolean} isDefaultTtlPicker - isDefaultTtlPicker is a boolean that determines if the picker is default or max ttl.
|
||||
*/
|
||||
|
||||
interface Args {
|
||||
model: {
|
||||
secretsEngine: SecretsEngineResource;
|
||||
};
|
||||
ttlKey: 'default_lease_ttl' | 'max_lease_ttl';
|
||||
}
|
||||
|
||||
export default class TtlPickerV2 extends Component<Args> {
|
||||
systemDefaultTtl = 0;
|
||||
systemDefault = SYSTEM_DEFAULT;
|
||||
custom = CUSTOM;
|
||||
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly router: RouterService;
|
||||
|
||||
@tracked selectedUnit = 's';
|
||||
@tracked time = '';
|
||||
@tracked errorMessage = '';
|
||||
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
|
||||
this.initializeTtl();
|
||||
}
|
||||
|
||||
initializeTtl() {
|
||||
const ttlValue = this.args?.model?.secretsEngine?.config[this.args.ttlKey];
|
||||
|
||||
let seconds = 0;
|
||||
|
||||
if (typeof ttlValue === 'number') {
|
||||
// if the passed value is a number, assume unit is seconds
|
||||
seconds = ttlValue;
|
||||
} else {
|
||||
const parseDuration = durationToSeconds(ttlValue || '');
|
||||
// if parsing fails leave it empty
|
||||
if (parseDuration === null) return;
|
||||
seconds = parseDuration;
|
||||
}
|
||||
|
||||
const unit = largestUnitFromSeconds(seconds);
|
||||
const time = convertFromSeconds(seconds, unit);
|
||||
this.time = time.toString() || '';
|
||||
this.selectedUnit = unit;
|
||||
}
|
||||
|
||||
get unitOptions() {
|
||||
return [
|
||||
{ label: 'seconds', value: 's' },
|
||||
{ label: 'minutes', value: 'm' },
|
||||
{ label: 'hours', value: 'h' },
|
||||
{ label: 'days', value: 'd' },
|
||||
];
|
||||
}
|
||||
|
||||
get formField() {
|
||||
return {
|
||||
label: this.args?.ttlKey === 'default_lease_ttl' ? 'Time-to-live (TTL)' : 'Maximum Time-to-live (TTL)',
|
||||
helperText:
|
||||
this.args?.ttlKey === 'default_lease_ttl'
|
||||
? 'Standard expiry deadline.'
|
||||
: 'Maximum possible extension for expiry.',
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
setTtlTime(event: HTMLElementEvent<HTMLInputElement>) {
|
||||
this.errorMessage = '';
|
||||
if (isNaN(Number(event.target.value))) {
|
||||
this.errorMessage = 'Only use numbers for this setting.';
|
||||
return;
|
||||
}
|
||||
this.time = event.target.value;
|
||||
this.args.model.secretsEngine.config[this.args.ttlKey] = event.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
setUnit(event: HTMLElementEvent<HTMLSelectElement>) {
|
||||
this.selectedUnit = event.target.value;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,13 +4,23 @@
|
|||
*/
|
||||
|
||||
import Route from '@ember/routing/route';
|
||||
import { service } from '@ember/service';
|
||||
import { getPluginVersionsFromEngineType } from 'vault/utils/plugin-catalog-helpers';
|
||||
|
||||
import type SecretsEngineResource from 'vault/resources/secrets/engine';
|
||||
import type ApiService from 'vault/services/api';
|
||||
import type PluginCatalogService from 'vault/services/plugin-catalog';
|
||||
|
||||
export default class SecretsBackendConfigurationGeneralSettingsRoute extends Route {
|
||||
@service declare readonly api: ApiService;
|
||||
@service('plugin-catalog') declare readonly pluginCatalog: PluginCatalogService;
|
||||
|
||||
async model() {
|
||||
const secretsEngine = this.modelFor('vault.cluster.secrets.backend') as SecretsEngineResource;
|
||||
// TODO: get list of versions using the sys/plugins/catalog endpoint.
|
||||
return { secretsEngine };
|
||||
const { data } = await this.pluginCatalog.getRawPluginCatalogData();
|
||||
|
||||
const versions = getPluginVersionsFromEngineType(data?.secret, secretsEngine.type);
|
||||
|
||||
return { secretsEngine, versions };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ export interface PluginCatalogResponse {
|
|||
error: boolean;
|
||||
}
|
||||
|
||||
export interface PluginCatalogRawDataResponse {
|
||||
data: {
|
||||
secret: Array<PluginCatalogPlugin>;
|
||||
auth: Array<PluginCatalogPlugin>;
|
||||
database: Array<PluginCatalogPlugin>;
|
||||
detailed: Array<PluginCatalogPlugin>;
|
||||
} | null;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
export default class PluginCatalogService extends Service {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly auth: AuthService;
|
||||
|
|
@ -93,6 +103,52 @@ export default class PluginCatalogService extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the plugin catalog from the Vault API and groups the data by plugin type
|
||||
* Uses the API service middleware for authentication, namespacing, and error handling
|
||||
* @returns Promise resolving to plugin catalog data and error state
|
||||
*/
|
||||
async getRawPluginCatalogData(): Promise<PluginCatalogRawDataResponse> {
|
||||
try {
|
||||
const response = await this.api.sys.pluginsCatalogListPlugins({
|
||||
headers: {
|
||||
token: this.auth.currentToken,
|
||||
namespace: sanitizePath(this.namespace.path),
|
||||
},
|
||||
});
|
||||
|
||||
if (response && response.detailed && Array.isArray(response.detailed) && response.detailed.length > 0) {
|
||||
const detailedPlugins = response.detailed as PluginCatalogPlugin[];
|
||||
|
||||
const secretPlugins = detailedPlugins.filter((plugin) => plugin.type === 'secret');
|
||||
|
||||
const authPlugins = detailedPlugins.filter((plugin) => plugin.type === 'auth');
|
||||
|
||||
const databasePlugins = detailedPlugins.filter((plugin) => plugin.type === 'database');
|
||||
|
||||
return {
|
||||
data: {
|
||||
secret: secretPlugins,
|
||||
auth: authPlugins,
|
||||
database: databasePlugins,
|
||||
detailed: detailedPlugins,
|
||||
},
|
||||
error: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: null,
|
||||
error: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
error: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets plugins of a specific type from the catalog
|
||||
* @param type - The plugin type ('secret', 'auth', 'database')
|
||||
|
|
|
|||
|
|
@ -203,3 +203,12 @@ export function categorizeEnginesByStatus(engines: EnhancedEngineDisplayData[]):
|
|||
|
||||
return { enabled, disabled };
|
||||
}
|
||||
|
||||
export function getPluginVersionsFromEngineType(list: PluginCatalogPlugin[] | undefined, name: string) {
|
||||
if (!list) return [];
|
||||
|
||||
return list.reduce((acc: string[], item: PluginCatalogPlugin) => {
|
||||
if (item.name === name) acc.push(item.version);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue