/** * 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 { waitFor } from '@ember/test-waiters'; import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; import errorMessage from 'vault/utils/error-message'; import type PkiActionModel from 'vault/models/pki/action'; import type PkiConfigUrlsModel from 'vault/models/pki/config/urls'; import type FlashMessageService from 'vault/services/flash-messages'; import type RouterService from '@ember/routing/router-service'; import type { ValidationMap } from 'vault/vault/app-types'; interface AdapterOptions { actionType: string; useIssuer: boolean | undefined; } interface Args { model: PkiActionModel; urls: PkiConfigUrlsModel; onCancel: CallableFunction; onComplete: CallableFunction; onSave?: CallableFunction; adapterOptions: AdapterOptions; hideAlertBanner: boolean; } /** * @module PkiGenerateRoot * PkiGenerateRoot shows only the fields valid for the generate root endpoint. * This component renders the form, handles the model save and rollback actions, * and shows the resulting data on success. onCancel is required for the cancel * transition, and if onSave is provided it will call that after save for any * side effects in the parent. * * @example * ```js * * ``` * * @param {Object} model - pki/action model. * @callback onCancel - Callback triggered when cancel button is clicked, after model is unloaded * @callback onSave - Optional - Callback triggered after model is saved, as a side effect. Results are shown on the same component * @callback onComplete - Callback triggered when "Done" button clicked, on results view * @param {Object} adapterOptions - object passed as adapterOptions on the model.save method */ export default class PkiGenerateRootComponent extends Component { @service declare readonly flashMessages: FlashMessageService; @service declare readonly router: RouterService; @tracked modelValidations: ValidationMap | null = null; @tracked errorBanner = ''; @tracked invalidFormAlert = ''; get defaultFields() { return [ 'type', 'commonName', 'issuerName', 'customTtl', 'notBeforeDuration', 'format', 'permittedDnsDomains', 'maxPathLength', ]; } get returnedFields() { return [ 'certificate', 'commonName', 'issuerId', 'issuerName', 'issuingCa', 'keyName', 'keyId', 'serialNumber', ]; } @action cancel() { // Generate root form will always have a new model this.args.model.unloadRecord(); this.args.onCancel(); } @action checkFormValidity() { if (this.args.model.validate) { const { isValid, state, invalidFormMessage } = this.args.model.validate(); this.modelValidations = state; this.invalidFormAlert = invalidFormMessage; return isValid; } return true; } @task @waitFor *save(event: Event) { event.preventDefault(); const continueSave = this.checkFormValidity(); if (!continueSave) return; try { yield this.args.model.save({ adapterOptions: this.args.adapterOptions }); // root generation must occur first in case templates are used for URL fields // this way an issuer_id exists for backend to interpolate into the template yield this.setUrls(); this.flashMessages.success('Successfully generated root.'); // This component shows the results, but call `onSave` for any side effects on parent if (this.args.onSave) { this.args.onSave(); } window?.scrollTo(0, 0); } catch (e) { this.errorBanner = errorMessage(e); this.invalidFormAlert = 'There was a problem generating the root.'; } } async setUrls() { if (!this.args.urls || !this.args.urls.canSet || !this.args.urls.hasDirtyAttributes) return; return this.args.urls.save(); } }