MinIO support for Admin UI (#2248)

Add support for MinIO creation form and list
This commit is contained in:
Carlos Alcaide Corvo authored and GitHub committed 2024-04-16 14:17:23 -07:00
1 parent 98c16ad422
commit 0870a8f623
26 files changed
+1332 -597

No files matched your search

@@ -73,6 +73,12 @@ export default class GeneratedStorageBucketModel extends BaseModel {
})
region;
@attr('string', {
isNestedAttribute: true,
description: 'The MinIO location server.',
})
endpoint_url;
//this field is not used in the UI, but this is kept here to replicate the API
@attr('string', {
readOnly: true,
+10
View File
@@ -9,8 +9,10 @@ export const TYPE_STORAGE_BUCKET_PLUGIN = 'plugin';
export const TYPES_STORAGE_BUCKET = Object.freeze([TYPE_STORAGE_BUCKET_PLUGIN]);
export const TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 = 'aws';
export const TYPE_STORAGE_BUCKET_PLUGIN_MINIO = 'minio';
export const TYPES_STORAGE_BUCKET_PLUGIN = Object.freeze([
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
]);
export const TYPE_CREDENTIAL_STATIC = 'static';
@@ -75,6 +77,14 @@ export default class StorageBucketModel extends GeneratedStorageBucketModel {
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3;
}
/**
* True if the storage bucket plugin type is MinIO
* @type {boolean}
*/
get isMinIO() {
return this.compositeType === TYPE_STORAGE_BUCKET_PLUGIN_MINIO;
}
/**
* If the storage bucket is a plugin return `plugin.name`,
* otherwise return the storage bucket type.
+37 -6
View File
@@ -4,7 +4,10 @@
*/
import ApplicationSerializer from './application';
import { TYPE_CREDENTIAL_DYNAMIC } from '../models/storage-bucket';
import {
TYPE_CREDENTIAL_DYNAMIC,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
} from '../models/storage-bucket';
const fieldsByCredentialType = {
static: [
@@ -12,27 +15,55 @@ const fieldsByCredentialType = {
'secret_access_key',
'region',
'disable_credential_rotation',
'endpoint_url',
],
dynamic: [
'role_arn',
'role_external_id',
'role_session_name',
'role_tags',
'disable_credential_rotation',
'region',
'disable_credential_rotation',
'endpoint_url',
],
};
export default class StorageBucketSerializer extends ApplicationSerializer {
/**
* Serializes storage-bucket. ATM just AWS needs specific serialization,
* for the other plugin types use default.
* @param {object} snapshot
* @returns
*/
serialize(snapshot) {
const serialized = super.serialize(...arguments);
const { credentialType } = snapshot.record;
const pluginType = snapshot.record.plugin?.name;
const serialized = super.serialize(...arguments);
// Common for all pluginTypes
if (!snapshot.record.isNew) {
delete serialized.bucket_name;
delete serialized.bucket_prefix;
}
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
serialized.attributes.disable_credential_rotation = true;
if (pluginType === TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3) {
return this.serializeAws(serialized, credentialType);
}
return serialized;
}
/**
* Returns a storage bucket serialized object specific for aws
* @param {object} serialized
* @param {string} credentialType
* @returns
*/
serializeAws(serialized, credentialType) {
if (serialized.attributes) {
if (credentialType === TYPE_CREDENTIAL_DYNAMIC) {
serialized.attributes.disable_credential_rotation = true;
}
delete serialized.attributes.endpoint_url;
}
return serialized;
}
@@ -45,7 +76,7 @@ export default class StorageBucketSerializer extends ApplicationSerializer {
if (options.isNestedAttribute && json.attributes) {
// The key must be included in the fieldsByType list above
if (!fieldsByCredentialType[credentialType].includes(key))
//API requires these fields to be null
// API requires these fields to be null
json.attributes[key] = null;
}
@@ -11,6 +11,7 @@ import {
TYPES_STORAGE_BUCKET as types,
TYPES_STORAGE_BUCKET_PLUGIN,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
} from 'api/models/storage-bucket';
// Represents known plugin types, except "foobar" which models the possibility
@@ -50,6 +51,10 @@ export default factory.extend({
)}`,
disable_credential_rotation: faker.datatype.boolean(),
};
case TYPE_STORAGE_BUCKET_PLUGIN_MINIO:
return {
endpoint_url: faker.internet.url(),
};
}
},
});
@@ -7,6 +7,7 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
TYPE_STORAGE_BUCKET_PLUGIN,
TYPE_CREDENTIAL_STATIC,
TYPE_CREDENTIAL_DYNAMIC,
@@ -15,15 +16,6 @@ import {
module('Unit | Serializer | storage bucket', function (hooks) {
setupTest(hooks);
test('it serializes records', function (assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('storage-bucket', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
test('it serializes a new static aws plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
@@ -70,6 +62,47 @@ module('Unit | Serializer | storage bucket', function (hooks) {
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new minio (static by default) plugin as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_MINIO,
credentialType: TYPE_CREDENTIAL_STATIC,
name: 'minio',
description: 'this has a minio plugin',
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
endpoint_url: 'http://hashicorp.com',
region: 'eu-west-1',
access_key_id: 'foobars',
secret_access_key: 'testing',
disable_credential_rotation: true,
});
const expectedResult = {
name: 'minio',
description: 'this has a minio plugin',
type: TYPE_STORAGE_BUCKET_PLUGIN,
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
attributes: {
region: 'eu-west-1',
disable_credential_rotation: true,
endpoint_url: 'http://hashicorp.com',
role_arn: null,
role_external_id: null,
role_session_name: null,
role_tags: null,
secrets_hmac: null,
},
secrets: {
access_key_id: 'foobars',
secret_access_key: 'testing',
},
};
assert.deepEqual(record.serialize(), expectedResult);
});
test('it serializes a new aws plugin with dynamic credentials as expected', async function (assert) {
const store = this.owner.lookup('service:store');
const record = store.createRecord('storage-bucket', {
@@ -126,6 +159,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
plugin: { name: 'aws' },
region: 'eu-west-1',
access_key_id: 'foobars',
secret_access_key: 'testing',
@@ -183,6 +217,7 @@ module('Unit | Serializer | storage bucket', function (hooks) {
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
plugin: { name: 'aws' },
region: 'eu-west-1',
disable_credential_rotation: true,
access_key_id: 'test',
@@ -222,6 +257,61 @@ module('Unit | Serializer | storage bucket', function (hooks) {
assert.deepEqual(serializedRecord, expectedResult);
});
test('it serializes when updating a MinIO (static credential by default) storage bucket correctly', async function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('storage-bucket');
store.push({
data: {
id: '3',
type: 'storage-bucket',
attributes: {
type: TYPE_STORAGE_BUCKET_PLUGIN,
plugin: { name: 'minio' },
name: 'minio',
description: 'this has a minio plugin',
bucket_name: 'bucketname',
bucket_prefix: 'bucketprefix',
worker_filter: 'workerfilter',
endpoint_url: 'http://hashicorp.com',
region: 'eu-west-1',
disable_credential_rotation: true,
access_key_id: 'test',
secret_access_key: 'test',
version: 1,
},
},
});
const record = store.peekRecord('storage-bucket', '3');
record.description = 'This is new description';
record.name = 'This is the new name';
record.endpoint_url = 'http://developer.hashicorp.com';
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
const expectedResult = {
name: 'This is the new name',
description: 'This is new description',
type: TYPE_STORAGE_BUCKET_PLUGIN,
worker_filter: 'workerfilter',
attributes: {
region: 'eu-west-1',
disable_credential_rotation: true,
endpoint_url: 'http://developer.hashicorp.com',
role_arn: null,
role_external_id: null,
role_session_name: null,
role_tags: null,
secrets_hmac: null,
},
secrets: {
access_key_id: 'test',
secret_access_key: 'test',
},
version: 1,
};
assert.deepEqual(serializedRecord, expectedResult);
});
test('it normalizes correctly', function (assert) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('storage-bucket');
+15 -8
View File
@@ -424,6 +424,7 @@ storage-bucket:
titles:
new: New Storage Bucket
amazon_s3: Amazon S3
minio: MinIO
actions:
delete: Delete Storage Bucket
types:
@@ -435,6 +436,9 @@ storage-bucket:
dynamic:
title: Dynamic
description: Specify an Assume Role used to generate credentials to use with this AWS bucket
plugin-types:
aws: Amazon S3
minio: MinIO
form:
scope:
label: Scope
@@ -444,33 +448,36 @@ storage-bucket:
provider:
label: Provider
help: Boundary currently only supports Amazon S3 storage.
endpoint_url:
label: Endpoint URL
help: Cannot be changed later.
bucket_name:
label: Bucket name
help: The name of this bucket in AWS
help: The name of this storage bucket.
bucket_prefix:
label: Bucket prefix
help: The base path where session recordings will be stored.
role_arn:
label: Role ARN
help: The ARN (Assume Role Name) used by this instance that the worker runs on
help: The ARN (Assume Role Name) used by this instance that the worker runs on.
role_external_id:
label: Role external ID
help: Used to provide access to this account if you have this configured
help: Used to provide access to this account if you have this configured.
role_tags:
label: Role tags
help: Attributes to pass to AWS
help: Attributes to pass to AWS.
role_session_name:
label: Role session name
help: Unique identifier for the AWS session to help identify it in your logs
help: Unique identifier for the AWS session to help identify it in your logs.
region:
label: Region
help: The AWS region to use
help: The region that this storage bucket is in.
access_key_id:
label: Access key ID
help: The Access key ID generated by AWS for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
help: The Access key ID generated by your storage provider for the IAM User to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
secret_access_key:
label: Secret access key
help: The secret access key generated by AWS for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
help: The secret access key generated by your storage provider for the IAM user to use with this storage bucket. We will not show this data after it is saved, but you can replace it.
worker_filter:
label: Worker filter
help: Filter for Boundary workers that have network access to your storage. Boolean expression.
@@ -0,0 +1,491 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
{{! name }}
<Hds::Form::TextInput::Field
name='name'
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! description }}
<Hds::Form::Textarea::Field
name='description'
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{! scope }}
{{#if @model.isNew}}
<Hds::Form::Select::Field
name='scope'
@isRequired={{true}}
@width='100%'
disabled={{form.disabled}}
{{on 'change' @updateScope}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{#if @model.scope}}
{{#if @model.scope.isGlobal}}
{{t 'resources.storage-bucket.form.scope.help_global'}}
{{else if @model.scope.isOrg}}
{{t 'resources.storage-bucket.form.scope.help_org'}}
{{/if}}
{{else}}
{{t 'resources.storage-bucket.form.scope.help'}}
{{/if}}
</F.HelperText>
<F.Options>
<option disabled selected={{not @model.scope}} value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @scopes as |scope|}}
<option
value={{scope.model.id}}
selected={{eq scope.model.id @model.scope.scope_id}}
>
{{scope.model.displayName}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
{{else}}
<InfoField
@value={{@model.scopeModel.displayName}}
@icon={{if @model.scopeModel.isGlobal 'globe' 'org'}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{#if @model.scopeModel.isGlobal}}
{{t 'resources.storage-bucket.form.scope.help_global'}}
{{else}}
{{t 'resources.storage-bucket.form.scope.help_org'}}
{{/if}}
</F.HelperText>
</InfoField>
{{/if}}
{{! Provider / plugin.name }}
<Hds::Form::RadioCard::Group @name='plugin_type' @alignment='center' as |G|>
<G.Legend>{{t 'titles.provider'}}</G.Legend>
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
{{#each @pluginTypes as |pluginType|}}
<G.RadioCard
@value={{pluginType}}
@checked={{eq pluginType @model.compositeType}}
@disabled={{(not @model.isNew)}}
{{on 'input' (fn @changePluginType pluginType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.storage-bucket.plugin-types.' pluginType)
}}</R.Label>
{{#if (eq pluginType 'aws')}}
<R.Icon @name='aws-color' />
{{else}}
<R.Icon @name='cloud-upload' />
{{/if}}
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{! bucket_name }}
<Hds::Form::TextInput::Field
name='bucket_name'
@isRequired={{true}}
@value={{@model.bucket_name}}
@isInvalid={{@model.errors.bucket_name}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'bucket_name')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.bucket_name.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.bucket_name.help'
}}</F.HelperText>
{{#if @model.errors.bucket_name}}
<F.Error as |E|>
{{#each @model.errors.bucket_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! bucket_prefix }}
<Hds::Form::TextInput::Field
name='bucket_prefix'
@isOptional={{true}}
@value={{@model.bucket_prefix}}
@isInvalid={{@model.errors.bucket_prefix}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'bucket_prefix')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.bucket_prefix.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.bucket_prefix.help'
}}</F.HelperText>
{{#if @model.errors.bucket_prefix}}
<F.Error as |E|>
{{#each @model.errors.bucket_prefix as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! region }}
<Hds::Form::TextInput::Field
name='region'
@isRequired={{true}}
@value={{@model.region}}
@isInvalid={{@model.errors.region}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'region')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.region.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.region.help'
}}</F.HelperText>
{{#if @model.errors.region}}
<F.Error as |E|>
{{#each @model.errors.region as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! credential_type }}
<Hds::Form::RadioCard::Group name='credential_type' as |G|>
<G.Legend>{{t 'resources.storage-bucket.types.credential'}}</G.Legend>
{{#each this.credentials as |credentialType|}}
<G.RadioCard
@name={{credentialType}}
@value={{credentialType}}
@checked={{eq credentialType this.selectedCredentialType}}
@disabled={{form.disabled}}
{{on 'input' (fn this.updateCredentialTypeSelection credentialType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.storage-bucket.types.' credentialType '.title')
}}</R.Label>
<R.Description>{{t
(concat
'resources.storage-bucket.types.' credentialType '.description'
)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{! static credentials fields }}
{{#unless this.showDynamicCredentials}}
{{! Creation }}
{{#if @model.isNew}}
{{! access_key }}
<Hds::Form::TextInput::Field
name='access_key_id'
@type='password'
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.secrets}}
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.access_key_id.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! secret_access_key }}
<Hds::Form::TextInput::Field
name='secret_access_key'
@type='password'
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secrets}}
disabled={{form.disabled}}
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! Edit }}
{{else}}
{{! access_key }}
<Form::Field::SecretInput
@name='access_key_id'
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.access_key_id}}
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn @rollbackSecretAttrs 'access_key_id'}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.access_key_id.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
</Form::Field::SecretInput>
{{! secret_access_key }}
<Form::Field::SecretInput
@name='secret_access_key'
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secret_access_key}}
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn @rollbackSecretAttrs 'secret_access_key'}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
</Form::Field::SecretInput>
{{/if}}
{{/unless}}
{{! dynamic credentials }}
{{#if this.showDynamicCredentials}}
{{! role_arn }}
<Hds::Form::TextInput::Field
name='role_arn'
@isRequired={{true}}
@value={{@model.role_arn}}
@isInvalid={{@model.errors.role_arn}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_arn')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.role_arn.label'}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_arn.help'}}
</F.HelperText>
{{#if @model.errors.role_arn}}
<F.Error as |E|>
{{#each @model.errors.role_arn as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_external_id }}
<Hds::Form::TextInput::Field
name='role_external_id'
@isOptional={{true}}
@value={{@model.role_external_id}}
@isInvalid={{@model.errors.role_external_id}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_external_id')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_external_id.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_external_id.help'}}
</F.HelperText>
{{#if @model.errors.role_external_id}}
<F.Error as |E|>
{{#each @model.errors.role_external_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_session_name }}
<Hds::Form::TextInput::Field
name='role_session_name'
@isOptional={{true}}
@value={{@model.role_session_name}}
@isInvalid={{@model.errors.role_session_name}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_session_name')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_session_name.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_session_name.help'}}
</F.HelperText>
{{#if @model.errors.role_session_name}}
<F.Error as |E|>
{{#each @model.errors.role_session_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! role_tags }}
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
<F.Error as |E|>
{{#each @model.errors.role_tags as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{/if}}
{{! worker_filter }}
<Hds::Form::TextInput::Field
name='worker_filter'
@isRequired={{true}}
@value={{@model.worker_filter}}
@isInvalid={{@model.errors.worker_filter}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'worker_filter')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.worker_filter.label'}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.worker_filter.help'}}
<Hds::Link::Inline @href={{doc-url 'storage-bucket.worker-filter'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
{{#if @model.errors.worker_filter}}
<F.Error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! JUST for static credentials }}
{{#unless this.showDynamicCredentials}}
{{! disable_credential_rotation }}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
disabled={{form.disabled}}
checked={{@model.disable_credential_rotation}}
{{on 'change' @toggleDisableCredentialRotation}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.disable_credential_rotation.label'
}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.disable_credential_rotation.help'}}
<Hds::Link::Inline
@href={{doc-url 'storage-bucket.disable-credential-rotation'}}
>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::Checkbox::Field>
{{/unless}}
{{! Actions }}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,38 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import {
TYPES_CREDENTIALS,
TYPE_CREDENTIAL_DYNAMIC,
} from 'api/models/storage-bucket';
import { tracked } from '@glimmer/tracking';
export default class FormStorageBucketAwsIndexComponent extends Component {
// =attributes
@tracked selectedCredentialType = this.args.model.credentialType;
@tracked showDynamicCredentials =
this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC;
/**
* returns an array of available credential types
* @type {array}
*/
get credentials() {
return TYPES_CREDENTIALS;
}
// =actions
/**
* Allows to update the credential type
* @param type {string}
*/
@action
updateCredentialTypeSelection(type) {
this.selectedCredentialType = type;
if (type === TYPE_CREDENTIAL_DYNAMIC) {
this.showDynamicCredentials = true;
} else {
this.showDynamicCredentials = false;
}
this.args.changeCredentialType(type);
}
}
@@ -2,455 +2,18 @@
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
@onSubmit={{@submit}}
@cancel={{this.cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
name='name'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::Select::Field
@isRequired={{true}}
@width='100%'
disabled={{form.disabled}}
name='scope'
{{on 'change' this.updateScope}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{this.scopeFieldDescription}}
</F.HelperText>
<F.Options>
<option disabled hidden selected value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @scopes as |scope|}}
<option value={{scope.model.id}}>
{{scope.model.displayName}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
{{else}}
<InfoField
@value={{@model.scopeModel.displayName}}
@icon={{if @model.scopeModel.isGlobal 'globe' 'org'}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{#if @model.scopeModel.isGlobal}}
{{t 'resources.storage-bucket.form.scope.help_global'}}
{{else}}
{{t 'resources.storage-bucket.form.scope.help_org'}}
{{/if}}
</F.HelperText>
</InfoField>
{{/if}}
<InfoField
@value={{t 'resources.storage-bucket.titles.amazon_s3'}}
@icon='aws-color'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'titles.provider'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.provider.help'
}}</F.HelperText>
</InfoField>
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.bucket_name}}
@isInvalid={{@model.errors.bucket_name}}
@type='text'
name='bucket_name'
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'bucket_name')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.bucket_name.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.bucket_name.help'
}}</F.HelperText>
{{#if @model.errors.bucket_name}}
<F.Error as |E|>
{{#each @model.errors.bucket_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.bucket_prefix}}
@isInvalid={{@model.errors.bucket_prefix}}
@type='text'
name='bucket_prefix'
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'bucket_prefix')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.bucket_prefix.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.bucket_prefix.help'
}}</F.HelperText>
{{#if @model.errors.bucket_prefix}}
<F.Error as |E|>
{{#each @model.errors.bucket_prefix as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.region}}
@isInvalid={{@model.errors.region}}
@type='text'
name='region'
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'region')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.region.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.region.help'
}}</F.HelperText>
{{#if @model.errors.region}}
<F.Error as |E|>
{{#each @model.errors.region as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::RadioCard::Group @name='credential_type' as |G|>
<G.Legend>{{t 'resources.storage-bucket.types.credential'}}</G.Legend>
{{#each this.credentials as |credentialType|}}
<G.RadioCard
@name={{credentialType}}
@value={{credentialType}}
@checked={{eq credentialType this.selectedCredentialType}}
@disabled={{form.disabled}}
{{on 'input' (fn this.updateTypeSelection credentialType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.storage-bucket.types.' credentialType '.title')
}}</R.Label>
<R.Description>{{t
(concat
'resources.storage-bucket.types.' credentialType '.description'
)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{#unless this.showDynamicCredentials}}
{{#if @model.isNew}}
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.secrets}}
@type='password'
name='access_key_id'
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.access_key_id.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secrets}}
@type='password'
name='secret_access_key'
disabled={{form.disabled}}
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{else}}
<Form::Field::SecretInput
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.access_key_id}}
@name='access_key_id'
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn this.rollbackSecretAttrs 'access_key_id'}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.access_key_id.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
</Form::Field::SecretInput>
<Form::Field::SecretInput
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secret_access_key}}
@name='secret_access_key'
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn this.rollbackSecretAttrs 'secret_access_key'}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
</Form::Field::SecretInput>
{{/if}}
{{/unless}}
{{#if this.showDynamicCredentials}}
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.role_arn}}
@isInvalid={{@model.errors.role_arn}}
@type='text'
name='role_arn'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_arn')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.role_arn.label'}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_arn.help'}}
</F.HelperText>
{{#if @model.errors.role_arn}}
<F.Error as |E|>
{{#each @model.errors.role_arn as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.role_external_id}}
@isInvalid={{@model.errors.role_external_id}}
@type='text'
name='role_external_id'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_external_id')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_external_id.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_external_id.help'}}
</F.HelperText>
{{#if @model.errors.role_external_id}}
<F.Error as |E|>
{{#each @model.errors.role_external_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.role_session_name}}
@isInvalid={{@model.errors.role_session_name}}
@type='text'
name='role_session_name'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'role_session_name')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.role_session_name.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_session_name.help'}}
</F.HelperText>
{{#if @model.errors.role_session_name}}
<F.Error as |E|>
{{#each @model.errors.role_session_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Form::Field::ListWrapper
@layout='horizontal'
@isOptional={{true}}
@disabled={{form.disabled}}
>
<:fieldset as |F|>
<F.Legend>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.Legend>
<F.HelperText>
{{t 'resources.storage-bucket.form.role_tags.label'}}
</F.HelperText>
{{#if @model.errors.role_tags}}
<F.Error as |E|>
{{#each @model.errors.role_tags as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</:fieldset>
<:field as |F|>
<F.TextInput
@name='role_tags'
@options={{@model.role_tags}}
@model={{@model}}
/>
</:field>
</Form::Field::ListWrapper>
{{/if}}
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.worker_filter}}
@isInvalid={{@model.errors.worker_filter}}
@type='text'
name='worker_filter'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'worker_filter')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.worker_filter.label'}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.worker_filter.help'}}
<Hds::Link::Inline @href={{doc-url 'storage-bucket.worker-filter'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
{{#if @model.errors.worker_filter}}
<F.Error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#unless this.showDynamicCredentials}}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
disabled={{form.disabled}}
checked={{@model.disable_credential_rotation}}
{{on 'change' this.toggleDisableCredentialRotation}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.disable_credential_rotation.label'
}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.disable_credential_rotation.help'}}
<Hds::Link::Inline
@href={{doc-url 'storage-bucket.disable-credential-rotation'}}
>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::Checkbox::Field>
{{/unless}}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
{{#if @model.type}}
{{component
(concat 'form/storage-bucket/' @model.compositeType)
model=@model
submit=@submit
cancel=@cancel
updateScope=this.updateScope
scopes=@scopes
pluginTypes=this.pluginTypes
changePluginType=@changePluginType
changeCredentialType=@changeCredentialType
rollbackSecretAttrs=this.rollbackSecretAttrs
toggleDisableCredentialRotation=this.toggleDisableCredentialRotation
}}
{{/if}}
@@ -5,64 +5,26 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import {
TYPES_CREDENTIALS,
TYPE_CREDENTIAL_DYNAMIC,
TYPES_STORAGE_BUCKET_PLUGIN,
} from 'api/models/storage-bucket';
export default class FormStorageBucketComponent extends Component {
// =attributes
@service intl;
@tracked scopeFieldDescription = this.intl.t(
'resources.storage-bucket.form.scope.help',
);
@tracked selectedCredentialType = this.args.model.credentialType;
@tracked showDynamicCredentials =
this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC;
/**
* returns an array of available credential types
* @type {object}
* returns an array of available storage bucket plugin types
*/
get credentials() {
return TYPES_CREDENTIALS;
get pluginTypes() {
return TYPES_STORAGE_BUCKET_PLUGIN;
}
// =actions
/**
* Allows to update the credential type
* @param type {string}
*/
@action
updateTypeSelection(type) {
this.selectedCredentialType = type;
if (type === TYPE_CREDENTIAL_DYNAMIC) {
this.showDynamicCredentials = true;
} else {
this.showDynamicCredentials = false;
}
this.args.changeType(type);
}
updateScopeFieldDescription() {
if (this.args.model.scopeModel.isGlobal) {
this.scopeFieldDescription = this.intl.t(
'resources.storage-bucket.form.scope.help_global',
);
} else {
this.scopeFieldDescription = this.intl.t(
'resources.storage-bucket.form.scope.help_org',
);
}
}
@action
updateScope(event) {
const selectedScopeId = event.target.value;
@@ -70,7 +32,6 @@ export default class FormStorageBucketComponent extends Component {
(element) => element.model.id === selectedScopeId,
).model;
this.args.model.scopeModel = selectedScopeModel;
this.updateScopeFieldDescription();
}
@action
@@ -0,0 +1,353 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
{{! name }}
<Hds::Form::TextInput::Field
name='name'
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! description }}
<Hds::Form::Textarea::Field
name='description'
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{! scope }}
{{#if @model.isNew}}
<Hds::Form::Select::Field
name='scope'
@isRequired={{true}}
@width='100%'
disabled={{form.disabled}}
{{on 'change' @updateScope}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{#if @model.scope}}
{{#if @model.scope.isGlobal}}
{{t 'resources.storage-bucket.form.scope.help_global'}}
{{else if @model.scope.isOrg}}
{{t 'resources.storage-bucket.form.scope.help_org'}}
{{/if}}
{{else}}
{{t 'resources.storage-bucket.form.scope.help'}}
{{/if}}
</F.HelperText>
<F.Options>
<option disabled selected={{not @model.scope}} value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each @scopes as |scope|}}
<option
value={{scope.model.id}}
selected={{eq scope.model.id @model.scope.scope_id}}
>
{{scope.model.displayName}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
{{else}}
<InfoField
@value={{@model.scopeModel.displayName}}
@icon={{if @model.scopeModel.isGlobal 'globe' 'org'}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.scope.label'}}</F.Label>
<F.HelperText>
{{#if @model.scopeModel.isGlobal}}
{{t 'resources.storage-bucket.form.scope.help_global'}}
{{else}}
{{t 'resources.storage-bucket.form.scope.help_org'}}
{{/if}}
</F.HelperText>
</InfoField>
{{/if}}
{{! Provider / plugin.name }}
<Hds::Form::RadioCard::Group @name='plugin_type' @alignment='center' as |G|>
<G.Legend>{{t 'titles.provider'}}</G.Legend>
<G.HelperText>{{t 'descriptions.choose-a-provider'}}</G.HelperText>
{{#each @pluginTypes as |pluginType|}}
<G.RadioCard
@value={{pluginType}}
@checked={{eq pluginType @model.compositeType}}
@disabled={{(not @model.isNew)}}
{{on 'input' (fn @changePluginType pluginType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.storage-bucket.plugin-types.' pluginType)
}}</R.Label>
{{#if (eq pluginType 'aws')}}
<R.Icon @name='aws-color' />
{{else}}
<R.Icon @name='cloud-upload' />
{{/if}}
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{! endpoint_url }}
<Hds::Form::TextInput::Field
name='endpoint_url'
@isRequired={{true}}
@value={{@model.endpoint_url}}
@isInvalid={{@model.errors.endpoint_url}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'endpoint_url')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.endpoint_url.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.endpoint_url.help'
}}</F.HelperText>
{{#if @model.errors.endpoint_url}}
<F.Error as |E|>
{{#each @model.errors.endpoint_url as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! bucket_name }}
<Hds::Form::TextInput::Field
name='bucket_name'
@isRequired={{true}}
@value={{@model.bucket_name}}
@isInvalid={{@model.errors.bucket_name}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'bucket_name')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.bucket_name.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.bucket_name.help'
}}</F.HelperText>
{{#if @model.errors.bucket_name}}
<F.Error as |E|>
{{#each @model.errors.bucket_name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! region }}
<Hds::Form::TextInput::Field
name='region'
@isOptional={{true}}
@value={{@model.region}}
@isInvalid={{@model.errors.region}}
disabled={{form.disabled}}
readOnly={{not @model.isNew}}
{{on 'input' (set-from-event @model 'region')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.region.label'}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.region.help'
}}</F.HelperText>
{{#if @model.errors.region}}
<F.Error as |E|>
{{#each @model.errors.region as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! Creation }}
{{#if @model.isNew}}
{{! access_key }}
<Hds::Form::TextInput::Field
name='access_key_id'
@type='password'
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.secrets}}
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>
{{t 'resources.storage-bucket.form.access_key_id.label'}}
</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! secret_access_key }}
<Hds::Form::TextInput::Field
name='secret_access_key'
@type='password'
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secrets}}
disabled={{form.disabled}}
@hasVisibilityToggle={{false}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
{{#if @model.errors.secrets}}
<F.Error as |E|>
{{#each @model.errors.secrets as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! Edit }}
{{else}}
{{! access_key }}
<Form::Field::SecretInput
@name='access_key_id'
@isRequired={{true}}
@value={{@model.access_key_id}}
@isInvalid={{@model.errors.access_key_id}}
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn @rollbackSecretAttrs 'access_key_id'}}
{{on 'input' (set-from-event @model 'access_key_id')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.access_key_id.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.access_key_id.help'
}}</F.HelperText>
</Form::Field::SecretInput>
{{! secret_access_key }}
<Form::Field::SecretInput
@name='secret_access_key'
@isRequired={{true}}
@value={{@model.secret_access_key}}
@isInvalid={{@model.errors.secret_access_key}}
@isDisabled={{form.disabled}}
@showEditButton={{true}}
@cancel={{fn @rollbackSecretAttrs 'secret_access_key'}}
{{on 'input' (set-from-event @model 'secret_access_key')}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.secret_access_key.label'
}}</F.Label>
<F.HelperText>{{t
'resources.storage-bucket.form.secret_access_key.help'
}}</F.HelperText>
</Form::Field::SecretInput>
{{/if}}
{{! worker_filter }}
<Hds::Form::TextInput::Field
name='worker_filter'
@isRequired={{true}}
@value={{@model.worker_filter}}
@isInvalid={{@model.errors.worker_filter}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'worker_filter')}}
as |F|
>
<F.Label>{{t 'resources.storage-bucket.form.worker_filter.label'}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.worker_filter.help'}}
<Hds::Link::Inline @href={{doc-url 'storage-bucket.worker-filter'}}>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
{{#if @model.errors.worker_filter}}
<F.Error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{! disable_credential_rotation }}
<Hds::Form::Checkbox::Field
name='disable_credential_rotation'
disabled={{form.disabled}}
checked={{@model.disable_credential_rotation}}
{{on 'change' @toggleDisableCredentialRotation}}
as |F|
>
<F.Label>{{t
'resources.storage-bucket.form.disable_credential_rotation.label'
}}</F.Label>
<F.HelperText>
{{t 'resources.storage-bucket.form.disable_credential_rotation.help'}}
<Hds::Link::Inline
@href={{doc-url 'storage-bucket.disable-credential-rotation'}}
>
{{t 'actions.learn-more'}}
</Hds::Link::Inline>
</F.HelperText>
</Hds::Form::Checkbox::Field>
{{! Actions }}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,78 @@
import Controller from '@ember/controller';
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 ScopesScopeStorageBucketsIndexController extends Controller {
// =services
@service router;
// =actions
/**
* Handle save
* @param {StorageBucketModel} storageBucket
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess('notifications.save-success')
async save(storageBucket) {
await storageBucket.save();
await this.router.transitionTo(
'scopes.scope.storage-buckets.storage-bucket',
storageBucket,
);
await this.router.refresh();
}
/**
* 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();
}
/**
* Rollback changes on storage buckets.
* @param {StorageBucketModel} storageBucket
*/
@action
cancel(storageBucket) {
const { isNew } = storageBucket;
storageBucket.rollbackAttributes();
if (isNew) this.router.transitionTo('scopes.scope.storage-buckets');
}
/**
* Updates credentil type
* @param {object} storageBucket
* @param {string} credentialType
*/
@action
changeCredentialType(storageBucket, credentialType) {
storageBucket.credentialType = credentialType;
}
/**
* Changes the plugin type.
* @param {*} pluginType
*/
@action
async changePluginType(pluginType) {
await this.router.replaceWith({
queryParams: { compositeType: pluginType },
});
}
}
@@ -0,0 +1,13 @@
import Controller, { inject as controller } from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeStorageBucketsNewController extends Controller {
@controller('scopes/scope/storage-buckets/index')
storageBuckets;
// =services
@service router;
// =attributes
queryParams = ['compositeType'];
}
@@ -0,0 +1,6 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeStorageBucketsStorageBucketIndexController extends Controller {
@controller('scopes/scope/storage-buckets/index')
storageBuckets;
}
@@ -0,0 +1,6 @@
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorageBucketController extends Controller {
@controller('scopes/scope/storage-buckets/index')
storageBuckets;
}
@@ -5,10 +5,6 @@
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 {
// =services
@@ -18,8 +14,6 @@ export default class ScopesScopeStorageBucketsRoute extends Route {
@service can;
@service router;
// =attributes
// =methods
/**
@@ -44,59 +38,4 @@ export default class ScopesScopeStorageBucketsRoute extends Route {
return this.store.query('storage-bucket', { scope_id, recursive: true });
}
}
// =actions
/**
* Handle save
* @param {StorageBucketModel} storageBucket
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess('notifications.save-success')
async save(storageBucket) {
await storageBucket.save();
await this.router.transitionTo(
'scopes.scope.storage-buckets.storage-bucket',
storageBucket,
);
this.refresh();
}
/**
* Rollback changes on storage buckets.
* @param {StorageBucketModel} storageBucket
*/
@action
cancel(storageBucket) {
const { isNew } = storageBucket;
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();
}
/**
* Updates type of credential
* @param {string} credentialType
*/
@action
changeType(storageBucket, credentialType) {
storageBucket.credentialType = credentialType;
}
}
@@ -17,6 +17,13 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
@service can;
@service router;
// =attributes
queryParams = {
compositeType: {
refreshModel: true,
},
};
// =methods
/**
@@ -39,8 +46,7 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
* creating another, but reuse name/description/other fields if available.
* @return {StorageBucket}
*/
model() {
const scopeModel = this.store.peekRecord('scope', 'global');
model({ compositeType = TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3 }) {
let name,
description,
scope,
@@ -48,6 +54,7 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
bucket_prefix,
region,
worker_filter;
if (this.currentModel?.isNew) {
({
name,
@@ -60,9 +67,10 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
} = this.currentModel);
this.currentModel.rollbackAttributes();
}
const record = this.store.createRecord('storage-bucket', {
type: TYPE_STORAGE_BUCKET_PLUGIN,
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
compositeType,
name,
description,
scope,
@@ -71,7 +79,6 @@ export default class ScopesScopeStorageBucketsNewRoute extends Route {
region,
worker_filter,
});
record.scopeModel = scopeModel;
return record;
}
@@ -69,6 +69,12 @@
@text={{t 'resources.storage-bucket.titles.amazon_s3'}}
@icon='aws-color'
/>
{{else if B.data.isMinIO}}
<Hds::Badge
@text={{t 'resources.storage-bucket.titles.minio'}}
@icon='cloud-upload'
/>
{{else}}
{{B.data.plugin.name}}
{{/if}}
@@ -113,7 +119,7 @@
<dd.Interactive
@text={{t 'resources.storage-bucket.actions.delete'}}
@color='critical'
{{on 'click' (route-action 'delete' B.data)}}
{{on 'click' (fn this.delete B.data)}}
/>
{{/if}}
</Hds::Dropdown>
@@ -24,10 +24,14 @@
<page.body>
<Form::StorageBucket
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@submit={{fn this.storageBuckets.save @model}}
@cancel={{fn this.storageBuckets.cancel @model}}
@scopes={{this.scopes}}
@changeType={{route-action 'changeType' @model}}
@changeCredentialType={{fn
this.storageBuckets.changeCredentialType
@model
}}
@changePluginType={{this.storageBuckets.changePluginType}}
/>
</page.body>
@@ -27,10 +27,14 @@
<page.body>
<Form::StorageBucket
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@submit={{fn this.storageBuckets.save @model}}
@cancel={{fn this.storageBuckets.cancel @model}}
@scopes={{this.scopes}}
@changeType={{route-action 'changeType' @model}}
@changeCredentialType={{fn
this.storageBuckets.changeCredentialType
@model
}}
@changePluginType={{this.storageBuckets.changePluginType}}
/>
</page.body>
</Rose::Layout::Page>
@@ -27,6 +27,11 @@
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@scopes={{this.scopes}}
@changeCredentialType={{fn
this.storageBuckets.changeCredentialType
@model
}}
@changePluginType={{this.storageBuckets.changePluginType}}
/>
</page.body>
@@ -32,6 +32,10 @@ module('Acceptance | storage-buckets | create', function (hooks) {
const ACCESS_KEY_SELECTOR = '[name="access_key_id"]';
const SECRET_KEY_SELECTOR = '[name="secret_access_key"]';
const SCOPE_SELECTOR = '[name=scope]';
const CREDENTIAL_TYPE_GROUP_SELECTOR = '[name=credential_type]';
const MINIO_PLUGIN_TYPE_SELECTOR = '[value=minio]';
const ENDPOINT_URL_SELECTOR = '[name=endpoint_url]';
const ENDPOINT_URL_TEXT = 'http://www.hashicorp.com';
const instances = {
scopes: {
@@ -63,13 +67,13 @@ module('Acceptance | storage-buckets | create', function (hooks) {
authenticateSession({});
});
test('users can create a new storage bucket with global scope', async function (assert) {
test('users can create a new storage bucket aws plugin type with global scope', async function (assert) {
const storageBucketCount = getStorageBucketCount();
await visit(urls.storageBuckets);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click('[value="global"]');
await select(SCOPE_SELECTOR, 'global');
assert.dom(BUCKET_NAME_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).isNotDisabled();
@@ -86,7 +90,7 @@ module('Acceptance | storage-buckets | create', function (hooks) {
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('users can create a new storage bucket with org scope', async function (assert) {
test('users can create a new storage bucket aws plugin type with org scope', async function (assert) {
const storageBucketCount = getStorageBucketCount();
await visit(urls.storageBuckets);
@@ -110,13 +114,17 @@ module('Acceptance | storage-buckets | create', function (hooks) {
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('users can create a new storage bucket with dynamic credentials', async function (assert) {
test('users can create a new storage bucket aws plugin type with dynamic credentials', async function (assert) {
const storageBucketCount = getStorageBucketCount();
await visit(urls.storageBuckets);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
assert.dom('.hds-form-radio-card').exists({ count: 2 });
// There are 2 credential types
assert
.dom(`${CREDENTIAL_TYPE_GROUP_SELECTOR} .hds-form-radio-card`)
.exists({ count: 2 });
await click(DYNAMIC_CREDENTIAL_SELECTOR);
await fillIn(ROLE_ARN_SELECTOR, 'test-arn-id');
@@ -132,13 +140,15 @@ module('Acceptance | storage-buckets | create', function (hooks) {
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('users can create a new storage bucket with static credentials', async function (assert) {
test('users can create a new storage bucket aws plugin type with static credentials', async function (assert) {
const storageBucketCount = getStorageBucketCount();
await visit(urls.storageBuckets);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
assert.dom('.hds-form-radio-card').exists({ count: 2 });
assert
.dom(`${CREDENTIAL_TYPE_GROUP_SELECTOR} .hds-form-radio-card`)
.exists({ count: 2 });
await click(STATIC_CREDENTIAL_SELECTOR);
await fillIn(ACCESS_KEY_SELECTOR, 'access_key_id');
@@ -156,6 +166,40 @@ module('Acceptance | storage-buckets | create', function (hooks) {
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('users can create a new storage bucket minio plugin type', async function (assert) {
const storageBucketCount = getStorageBucketCount();
// Navigate to new storage bucket
await visit(urls.storageBuckets);
await click(`[href="${urls.newStorageBucket}"]`);
// Fill the form
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click(MINIO_PLUGIN_TYPE_SELECTOR);
await fillIn(ENDPOINT_URL_SELECTOR, ENDPOINT_URL_TEXT);
await fillIn(BUCKET_NAME_FIELD_SELECTOR, 'Test SB');
await fillIn(ACCESS_KEY_SELECTOR, 'access_key_id');
await fillIn(SECRET_KEY_SELECTOR, 'secret_access_key');
await click(SAVE_BTN_SELECTOR);
// Retrieve recently created SB
const storageBucket = await this.server.schema.storageBuckets.findBy({
name: NAME_FIELD_TEXT,
});
// Assertions
assert.strictEqual(storageBucket.name, NAME_FIELD_TEXT);
assert.strictEqual(
storageBucket.attributes.endpoint_url,
ENDPOINT_URL_TEXT,
);
assert.strictEqual(storageBucket.attributes.role_arn, null);
assert.strictEqual(storageBucket.plugin.name, 'minio');
assert.strictEqual(storageBucket.type, 'plugin');
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('user can cancel new storage bucket creation', async function (assert) {
const storageBucketCount = getStorageBucketCount();
await visit(urls.storageBuckets);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/storage-buckets/index',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/storage-buckets/index',
);
assert.ok(controller);
});
},
);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/storage-buckets/new',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/storage-buckets/new',
);
assert.ok(controller);
});
},
);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/storage-buckets/storage-bucket/index',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/storage-buckets/storage-bucket/index',
);
assert.ok(controller);
});
},
);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/targets/target/enable-session-recording/create-storage-bucket',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/targets/target/enable-session-recording/create-storage-bucket',
);
assert.ok(controller);
});
},
);