diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 4396ac2dc..0fd5949a4 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -290,6 +290,10 @@ storage-bucket:
worker_filter:
label: Worker filter
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
+ questions:
+ delete-storage-bucket:
+ title: Delete associated recordings?
+ message: This storage bucket may have session recordings associated with it. Deleting this storage bucket will delete these recordings when you save your changes.
project:
title: Project
title_plural: Projects
diff --git a/ui/admin/app/routes/scopes/scope/storage-buckets.js b/ui/admin/app/routes/scopes/scope/storage-buckets.js
index 8185c65f8..eee836051 100644
--- a/ui/admin/app/routes/scopes/scope/storage-buckets.js
+++ b/ui/admin/app/routes/scopes/scope/storage-buckets.js
@@ -7,6 +7,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
+import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeStorageBucketsRoute extends Route {
@@ -71,4 +72,20 @@ export default class ScopesScopeStorageBucketsRoute extends Route {
storageBucket.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope.storage-buckets');
}
+
+ /**
+ * Deletes the storage bucket.
+ * @param {StorageBucketModel} storageBucket
+ */
+ @action
+ @loading
+ @confirm('resources.storage-bucket.questions.delete-storage-bucket.message', {
+ title: 'resources.storage-bucket.questions.delete-storage-bucket.title',
+ confirm: 'resources.storage-bucket.actions.delete',
+ })
+ @notifyError(({ message }) => message, { catch: true })
+ @notifySuccess('notifications.delete-success')
+ async delete(storageBucket) {
+ await storageBucket.destroyRecord();
+ }
}
diff --git a/ui/admin/app/templates/scopes/scope/storage-buckets/index.hbs b/ui/admin/app/templates/scopes/scope/storage-buckets/index.hbs
index f29ed6f90..4b3f46253 100644
--- a/ui/admin/app/templates/scopes/scope/storage-buckets/index.hbs
+++ b/ui/admin/app/templates/scopes/scope/storage-buckets/index.hbs
@@ -102,6 +102,14 @@
@model={{B.data.id}}
@text={{t 'actions.edit'}}
/>
+ {{#if (can 'delete model' B.data)}}
+
+
+ {{/if}}
diff --git a/ui/admin/tests/acceptance/storage-buckets/delete-test.js b/ui/admin/tests/acceptance/storage-buckets/delete-test.js
new file mode 100644
index 000000000..fac18ebe0
--- /dev/null
+++ b/ui/admin/tests/acceptance/storage-buckets/delete-test.js
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import { module, test } from 'qunit';
+import { visit, click, currentURL } from '@ember/test-helpers';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { Response } from 'miragejs';
+import { authenticateSession } from 'ember-simple-auth/test-support';
+
+module('Acceptance | storage-buckets | delete', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+
+ let intl;
+ let features;
+ let getStorageBucketCount;
+
+ const DROPDOWN_BUTTON_SELECTOR = '.hds-dropdown-toggle-icon';
+ const DELETE_DROPDOWN_SELECTOR = '.hds-dropdown-list [type="button"]';
+ const DIALOG_DELETE_BTN_SELECTOR = '.rose-dialog .rose-button-primary';
+ const DIALOG_CANCEL_BTN_SELECTOR = '.rose-dialog .rose-button-secondary';
+ const DIALOG_MESSAGE_SELECTOR = '.rose-dialog-body';
+ const DIALOG_TITLE_SELECTOR = '.rose-dialog-header';
+ const NOTIFICATION_MSG_SELECTOR = '.rose-notification-body';
+ const NOTIFICATION_MSG_TEXT = 'Deleted successfully.';
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ },
+ storageBucket: null,
+ };
+
+ const urls = {
+ globalScope: null,
+ storageBuckets: null,
+ };
+
+ hooks.beforeEach(function () {
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.storageBucket = this.server.create('storage-bucket', {
+ scope: instances.scopes.global,
+ });
+ urls.globalScope = `/scopes/global`;
+ urls.storageBuckets = `${urls.globalScope}/storage-buckets`;
+ getStorageBucketCount = () =>
+ this.server.schema.storageBuckets.all().models.length;
+
+ authenticateSession({});
+ intl = this.owner.lookup('service:intl');
+ features = this.owner.lookup('service:features');
+ features.enable('session-recording');
+ });
+
+ test('user can delete a storage bucket', async function (assert) {
+ assert.expect(2);
+ const storageBucketCount = getStorageBucketCount();
+ await visit(urls.globalScope);
+
+ await click(`[href="${urls.storageBuckets}"]`);
+ await click(DROPDOWN_BUTTON_SELECTOR);
+ await click(DELETE_DROPDOWN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.storageBuckets);
+ assert.strictEqual(getStorageBucketCount(), storageBucketCount - 1);
+ });
+
+ test('user can accept delete storage bucket via dialog', async function (assert) {
+ assert.expect(4);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ const storageBucketCount = getStorageBucketCount();
+ await visit(urls.globalScope);
+
+ await click(`[href="${urls.storageBuckets}"]`);
+ await click(DROPDOWN_BUTTON_SELECTOR);
+ await click(DELETE_DROPDOWN_SELECTOR);
+
+ assert
+ .dom(DIALOG_DELETE_BTN_SELECTOR)
+ .hasText(intl.t('resources.storage-bucket.actions.delete'));
+
+ await click(DIALOG_DELETE_BTN_SELECTOR);
+
+ assert.dom(NOTIFICATION_MSG_SELECTOR).hasText(NOTIFICATION_MSG_TEXT);
+ assert.strictEqual(currentURL(), urls.storageBuckets);
+ assert.strictEqual(getStorageBucketCount(), storageBucketCount - 1);
+ });
+
+ test('user can cancel delete storage bucket via dialog', async function (assert) {
+ assert.expect(4);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ const storageBucketCount = getStorageBucketCount();
+ await visit(urls.globalScope);
+
+ await click(`[href="${urls.storageBuckets}"]`);
+ await click(DROPDOWN_BUTTON_SELECTOR);
+ await click(DELETE_DROPDOWN_SELECTOR);
+
+ assert
+ .dom(DIALOG_TITLE_SELECTOR)
+ .hasText(
+ intl.t('resources.storage-bucket.questions.delete-storage-bucket.title')
+ );
+ assert
+ .dom(DIALOG_MESSAGE_SELECTOR)
+ .hasText(
+ intl.t(
+ 'resources.storage-bucket.questions.delete-storage-bucket.message'
+ )
+ );
+
+ await click(DIALOG_CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.storageBuckets);
+ assert.strictEqual(getStorageBucketCount(), storageBucketCount);
+ });
+
+ test('user cannot delete storage bucket without proper authorization', async function (assert) {
+ assert.expect(1);
+ await visit(urls.globalScope);
+ instances.storageBucket.authorized_actions =
+ instances.storageBucket.authorized_actions.filter(
+ (item) => item !== 'delete'
+ );
+
+ await click(`[href="${urls.storageBuckets}"]`);
+ await click(DROPDOWN_BUTTON_SELECTOR);
+
+ assert.dom(DELETE_DROPDOWN_SELECTOR).doesNotExist();
+ });
+
+ test('deleting a storage bucket which errors displays error messages', async function (assert) {
+ assert.expect(1);
+ await visit(urls.globalScope);
+ this.server.del('/storage-buckets/:id', () => {
+ return new Response(
+ 490,
+ {},
+ {
+ status: 490,
+ code: 'error',
+ message: 'Oops.',
+ }
+ );
+ });
+
+ await click(`[href="${urls.storageBuckets}"]`);
+ await click(DROPDOWN_BUTTON_SELECTOR);
+ await click(DELETE_DROPDOWN_SELECTOR);
+
+ assert.dom(NOTIFICATION_MSG_SELECTOR).hasText('Oops.');
+ });
+});