diff --git a/ui/app/app.js b/ui/app/app.js index 94614b2aa7fb..d0fde1b6c239 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -34,6 +34,7 @@ export default class App extends Application { dependencies: { services: [ 'auth', + 'download', 'flash-messages', 'namespace', 'path-help', @@ -57,10 +58,10 @@ export default class App extends Application { 'namespace', 'path-help', 'router', + 'secret-mount-path', 'store', 'version', 'wizard', - 'secret-mount-path', ], externalRoutes: { secrets: 'vault.cluster.secrets.backends', diff --git a/ui/app/services/download.ts b/ui/app/services/download.ts index a608ea32c108..7cfbb19b41e2 100644 --- a/ui/app/services/download.ts +++ b/ui/app/services/download.ts @@ -1,19 +1,45 @@ import Service from '@ember/service'; +interface Extensions { + csv: string; + hcl: string; + sentinel: string; + json: string; + pem: string; + txt: string; +} + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types +const EXTENSION_TO_MIME: Extensions = { + csv: 'txt/csv', + hcl: 'text/plain', + sentinel: 'text/plain', + json: 'application/json', + pem: 'application/x-pem-file', + txt: 'text/plain', +}; + export default class DownloadService extends Service { - download(filename: string, mimetype: string, content: string) { + download(filename: string, content: string, extension: string) { + // replace spaces with hyphens, append extension to filename + const formattedFilename = + `${filename?.replace(/\s+/g, '-')}.${extension}` || + `vault-data-${new Date().toISOString()}.${extension}`; + + // map extension to MIME type or use default + const mimetype = EXTENSION_TO_MIME[extension as keyof Extensions] || 'text/plain'; + + // commence download const { document, URL } = window; const downloadElement = document.createElement('a'); - downloadElement.download = filename; - downloadElement.href = URL.createObjectURL( - new Blob([content], { - type: mimetype, - }) - ); + const data = new File([content], formattedFilename, { type: mimetype }); + downloadElement.download = formattedFilename; + downloadElement.href = URL.createObjectURL(data); document.body.appendChild(downloadElement); downloadElement.click(); URL.revokeObjectURL(downloadElement.href); downloadElement.remove(); + return formattedFilename; } // SAMPLE CSV FORMAT ('content' argument) @@ -22,15 +48,14 @@ export default class DownloadService extends Service { // namespacelonglonglong4/,,191,171,20\n // namespacelonglonglong4/,auth/method/uMGBU,35,20,15\n' csv(filename: string, content: string) { - // even though Blob type 'text/csv' is specified below, some browsers (ex. Firefox) require the filename has an explicit extension - const formattedFilename = `${filename?.replace(/\s+/g, '-')}.csv` || 'vault-data.csv'; - this.download(formattedFilename, 'text/csv', content); - return formattedFilename; + this.download(filename, content, 'csv'); } pem(filename: string, content: string) { - const formattedFilename = `${filename?.replace(/\s+/g, '-')}.pem` || 'vault-cert.pem'; - this.download(formattedFilename, 'application/x-pem-file', content); - return formattedFilename; + this.download(filename, content, 'pem'); + } + + miscExtension(filename: string, content: string, extension: string) { + this.download(filename, content, extension); } } diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index a998a3df30d0..4b15cc81edd7 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -91,9 +91,8 @@ {{/if}} diff --git a/ui/app/templates/vault/cluster/policy/show.hbs b/ui/app/templates/vault/cluster/policy/show.hbs index 7ea4179e246f..7606b4af9626 100644 --- a/ui/app/templates/vault/cluster/policy/show.hbs +++ b/ui/app/templates/vault/cluster/policy/show.hbs @@ -25,9 +25,9 @@ Download policy diff --git a/ui/lib/core/addon/components/download-button.js b/ui/lib/core/addon/components/download-button.js index 39737574f4e1..365b7f6253de 100644 --- a/ui/lib/core/addon/components/download-button.js +++ b/ui/lib/core/addon/components/download-button.js @@ -1,9 +1,12 @@ -import { action } from '@ember/object'; import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import errorMessage from 'vault/utils/error-message'; /** * @module DownloadButton * DownloadButton components are an action button used to download data. Both the action text and icon are yielded. - * + * * NOTE: when using in an engine, remember to add the 'download' service to its dependencies (in /engine.js) and map to it in /app.js + * [ember-docs](https://ember-engines.com/docs/services) * @example * ```js * * ``` - * @param {string} data - data to download + * @param {string} [filename] - name of file that prefixes the ISO timestamp generated at download + * @param {string} [data] - data to download + * @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype * @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor - * @param {string} [filename] - name of file that prefixes the ISO timestamp generated when download - * @param {string} [mime='text/plain'] - media type to be downloaded - * @param {string} [extension='txt'] - file extension */ export default class DownloadButton extends Component { - get extension() { - return this.args.extension || 'txt'; - } - - get mime() { - return this.args.mime || 'text/plain'; - } + @service download; + @service flashMessages; get filename() { - const defaultFilename = `${new Date().toISOString()}.${this.extension}`; - return this.args.filename ? this.args.filename + '-' + defaultFilename : defaultFilename; + const timestamp = new Date().toISOString(); + return this.args.filename ? this.args.filename + '-' + timestamp : timestamp; } - get data() { + get content() { if (this.args.stringify) { return JSON.stringify(this.args.data, null, 2); } return this.args.data; } - // TODO refactor and call service instead + get extension() { + return this.args.extension || 'txt'; + } + @action handleDownload() { - const { document, URL } = window; - const downloadElement = document.createElement('a'); - const content = new File([this.data], this.filename, { type: this.mime }); - downloadElement.download = this.filename; - downloadElement.href = URL.createObjectURL(content); - document.body.appendChild(downloadElement); - downloadElement.click(); - URL.revokeObjectURL(downloadElement.href); - downloadElement.remove(); + try { + this.download.miscExtension(this.filename, this.content, this.extension); + this.flashMessages.info(`Downloading ${this.filename}`); + } catch (error) { + this.flashMessages.danger(errorMessage(error, 'There was a problem downloading. Please try again.')); + } } } diff --git a/ui/lib/kmip/addon/engine.js b/ui/lib/kmip/addon/engine.js index f8f9070a585b..64bb85f4dc94 100644 --- a/ui/lib/kmip/addon/engine.js +++ b/ui/lib/kmip/addon/engine.js @@ -11,6 +11,7 @@ const Eng = Engine.extend({ dependencies: { services: [ 'auth', + 'download', 'flash-messages', 'namespace', 'path-help', diff --git a/ui/lib/kmip/addon/templates/configuration.hbs b/ui/lib/kmip/addon/templates/configuration.hbs index cf88f5084d08..cf134fb710dc 100644 --- a/ui/lib/kmip/addon/templates/configuration.hbs +++ b/ui/lib/kmip/addon/templates/configuration.hbs @@ -4,9 +4,9 @@ {{#if this.model}} Download CA cert diff --git a/ui/lib/pki/addon/components/page/pki-key-details.hbs b/ui/lib/pki/addon/components/page/pki-key-details.hbs index 3a85f73a6ddd..2552c91c77e5 100644 --- a/ui/lib/pki/addon/components/page/pki-key-details.hbs +++ b/ui/lib/pki/addon/components/page/pki-key-details.hbs @@ -10,6 +10,12 @@ Delete
+ {{#if @key.privateKey}} + + Download private key + + + {{/if}} Edit key diff --git a/ui/lib/pki/addon/engine.js b/ui/lib/pki/addon/engine.js index 8531866cfc40..0c7e0c40f20b 100644 --- a/ui/lib/pki/addon/engine.js +++ b/ui/lib/pki/addon/engine.js @@ -18,10 +18,10 @@ export default class PkiEngine extends Engine { 'namespace', 'path-help', 'router', + 'secret-mount-path', 'store', 'version', 'wizard', - 'secret-mount-path', ], externalRoutes: ['secrets'], }; diff --git a/ui/lib/pki/addon/routes/keys/create.js b/ui/lib/pki/addon/routes/keys/create.js index 94849704e2a4..6874caa48a09 100644 --- a/ui/lib/pki/addon/routes/keys/create.js +++ b/ui/lib/pki/addon/routes/keys/create.js @@ -5,7 +5,6 @@ import { withConfirmLeave } from 'core/decorators/confirm-leave'; @withConfirmLeave() export default class PkiKeysCreateRoute extends PkiKeysIndexRoute { @service store; - @service secretMountPath; model() { return this.store.createRecord('pki/key');