Llb/worker filter improvements (#2614)

* test: 💍 clean up worker filter component tests (#2559)

* Icu 15741 add show filter generator toggle to worker filter component (#2562)

* feat: 🎸 add filter generator to worker filter component

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15741

* feat: 🎸 show add worker filter when none set for targets (#2564)

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15760

* Icu 15743 tag workflow (#2565)

* feat: 🎸 add tag generator workflow

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15743

* test: 💍 add test cases

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15743

* refactor: 💡 update selected type name to selectedGeneratorType

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15743

* Apply suggestions from code review

minor translation change

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>

* refactor: 💡 update id for generated-value

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15743

---------

Co-authored-by: Lisbet Alvarez <lisbet.alvarez@hashicorp.com>
Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>

* Icu 15744 name workflow (#2566)

* feat: 🎸 add name workflow

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15744

* test: 💍 add "name" workflow tests for filter generator

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15744

* refactor: 💡 use const test selectors for filter generator

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15744

* refactor: 💡 add default option

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15744

* docs: ✏️ fix comment

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15744

---------

Co-authored-by: cameronperera <cameron.perera@hashicorp.com>

* refactor: 💡 move worker filter generator outside of rose::form (#2574)

* refactor: 💡 move worker filter generator outside of rose::form

* [ICU-15718] Add worker filter route, template for empty state (#2576)

* feat: 🎸 Vault Credential Store Worker Filter

The Worker Filters tab will display the associated worker filter and
have an empty state for when there is no worker filter assigned.

* add template

* Add missing copyright headers

* update styling, remove Rose component

* color primary is already default, remove

* add rose component, remove custom css

* fix test

* singularize lang

---------

Co-authored-by: laurenolivia <laurenolivia@users.noreply.github.com>

* feat: 🎸 update worker filter field to use code editor (#2578)

* feat: 🎸 update worker filter field to use code editor

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15737

* test: 💍 fix failing storage bucket tests

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15737

* test: 💍 update tests for new worker filter field (#2580)

* test: 💍 update tests for new worker filter field

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15739

* Update ui/admin/tests/acceptance/storage-buckets/update-test.js

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>

---------

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>

* [ICU-15719] Create edit worker filter route and template (#2582)

* add codeblock to template

* add cancel btn fn

* add controller test

* add save fn

* display formatted worker filter text inside codeblock

* move codeblock component outside rose::layout::centered

* remove console.log

* remove router.refresh() and refactor translations yaml

* refactor translations, add breadcrumb::item

* replace html elements with hds

* move component, delete file

* [ICU-15720] Add edit worker filter action to manage dropdown (#2593)

* add acceptance test file

* delete test file

* add edit option to manage dropdown

* update route in dropdown

* add check for model isVault

* add acceptance test file

* delete test file

* remove data test selectors, fix syntax

* [ICU-15721] Remove worker filter input field (#2596)

* remove worker filter input field from vault credential store form

* revert yaml

* [ICU-15838] Add vault credential store worker filter tests (#2595)

* add data test elements

* add subtests to update-test.js

* add subtext to check for .hasText()

* add subtest when worker filter exists

* update subtest name

* remove console.log

* clean up

* add new selectors

* refactor: 💡 worker filter clean-up (#2592)

* refactor: 💡 worker filter clean-up

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15816

* refactor: 💡 add aria labels & missing can save check

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15816

* refactor: 💡 pr feedback of using uuid

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15816

* Add missing copyright headers

* refactor: 💡 use unique-id helper for aria labels (pr feedback)

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-15816

---------

Co-authored-by: lisbet-alvarez <lisbet-alvarez@users.noreply.github.com>

* refactor: 💡 update icon logic for egress worker filter (#2612)

* refactor: 💡 update worker_filter help translation

* refactor: 💡 add maxHeight to worker filter code blocks

* refactor: 💡 use existing default option for dropdowns

* refactor: 💡 hide copy button when no result to copy

and remove unused styles

---------

Co-authored-by: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com>
Co-authored-by: Lisbet Alvarez <lisbet.alvarez@hashicorp.com>
Co-authored-by: Lauren <lauren.burwell@hashicorp.com>
Co-authored-by: laurenolivia <laurenolivia@users.noreply.github.com>
Co-authored-by: lisbet-alvarez <lisbet-alvarez@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2024-12-11 14:00:27 -06:00
1 parent 40c8ceeca6
commit 2869020e65
40 files changed
+1045 -212

No files matched your search

+24
View File
@@ -133,3 +133,27 @@ settings:
alerts:
cache-daemon: There may be a problem with the cache daemon
client-agent: There may be a problem with the client agent
worker-filter-generator:
toggle:
title: Show filter generator
description: The generator will automatically generate a pre-formatted filter for you. You can copy the result to the editor above.
filter-generator:
title: Filter generator
description: Choose what you want to format into a filter.
link: Learn more about formatting a worker filter.
input-values:
title: Input values
description: Next, input what you want to format.
formatted-result:
title: Formatted result
description: Copy and paste this into the editor above.
label: Generated result
tag:
label: Tag
helper: Format a worker tag
name:
label: Name
helper: Format a worker name
operator:
matches: matches
contains: contains
+2
View File
@@ -136,6 +136,8 @@ key:
label: Key
value:
label: Value
operator:
label: Operator
callback_url:
label: Callback URL
help: The URL to which the third-party provider sends the authentication response. Paste this into the provider configuration.
+14 -1
View File
@@ -502,7 +502,7 @@ storage-bucket:
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.
help: Filters to the worker(s) that can handle requests for this storage bucket.
disable_credential_rotation:
label: Disable credential rotation
help: Disabling credential rotation could allow entities outside of Boundary to know the client secret in use.
@@ -819,6 +819,7 @@ target:
ingress-workers: Ingress workers
actions:
edit-worker-filter: Edit worker filter
add-worker-filter: Add worker filter
credential-store:
title: Credential Store
title_plural: Credential Stores
@@ -828,8 +829,20 @@ credential-store:
title: Welcome to Credential Stores
titles:
new: New Credential Store
worker-filter:
title: Worker Filter
description: The worker filter format for this credential store.
messages:
none:
title: No worker filter added
description: You haven't added a worker filter yet.
edit:
title: Edit Worker Filter
description: Specify workers that have access to the credential store.
actions:
add: Add Worker Filter
delete: Delete Credential Store
edit: Edit Worker Filter
types:
vault: Vault
static: Static
@@ -18,6 +18,12 @@
{{t 'resources.credential.actions.create'}}
</dd.Interactive>
{{/if}}
{{#if (and (feature-flag 'vault-worker-filter') @model.isVault)}}
<dd.Interactive
@route='scopes.scope.credential-stores.credential-store.edit-worker-filter'
@text={{t 'resources.credential-store.actions.edit'}}
/>
{{/if}}
{{#if (can 'delete model' @model)}}
<dd.Separator />
<dd.Interactive @color='critical' {{on 'click' @delete}}>
@@ -8,6 +8,13 @@
{{t 'titles.details'}}
</nav.link>
{{#if @model.isVault}}
{{#if (feature-flag 'vault-worker-filter')}}
<nav.link
@route='scopes.scope.credential-stores.credential-store.worker-filter'
>
{{t 'resources.credential-store.worker-filter.title'}}
</nav.link>
{{/if}}
{{#if (can 'navigate model' @model collection='credential-libraries')}}
<nav.link
@route='scopes.scope.credential-stores.credential-store.credential-libraries'
@@ -115,35 +115,6 @@
{{/if}}
</Hds::Form::TextInput::Field>
{{#if (feature-flag 'vault-worker-filter')}}
<Hds::Form::TextInput::Field
name='worker_filter'
@value={{@model.worker_filter}}
@isOptional={{true}}
@isInvalid={{@model.errors.worker_filter}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'worker_filter')}}
as |F|
>
<F.Label>
{{t 'resources.credential-store.form.worker_filter.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.credential-store.form.worker_filter.help'}}
<Hds::Link::Inline @href={{doc-url 'worker-filters'}}>
{{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>
{{/if}}
{{#if (or @model.isNew form.isEditable)}}
<Hds::Form::TextInput::Field
name='token'
@@ -456,30 +456,41 @@
{{/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 data-test-worker-filter-error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#let (unique-id) (unique-id) as |labelId helpId|}}
<Hds::Form::Fieldset
aria-labelledby={{labelId}}
aria-describedby={{helpId}}
class='storage-bucket-worker-filter'
@isRequired={{true}}
as |F|
>
<F.Legend id={{labelId}}>{{t
'resources.storage-bucket.form.worker_filter.label'
}}</F.Legend>
<F.HelperText id={{helpId}}>
{{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>
<F.Control>
{{#if form.disabled}}
<Hds::CodeBlock
@language='bash'
@value={{@model.worker_filter}}
@hasCopyButton={{true}}
data-test-worker-filter
/>
{{else}}
<WorkerFilterGenerator
@model={{@model}}
@name='worker_filter'
@hideToolbar={{true}}
/>
{{/if}}
</F.Control>
</Hds::Form::Fieldset>
{{/let}}
{{! JUST for static credentials }}
{{#unless this.showDynamicCredentials}}
@@ -13,10 +13,11 @@ import { tracked } from '@glimmer/tracking';
export default class FormStorageBucketAwsIndexComponent extends Component {
// =attributes
@tracked selectedCredentialType = this.args.model.credentialType;
@tracked selectedCredentialType = this.args.model.credentialType;
@tracked showDynamicCredentials =
this.args.model.credentialType === TYPE_CREDENTIAL_DYNAMIC;
/**
* returns an array of available credential types
* @type {array}
@@ -28,7 +29,7 @@ export default class FormStorageBucketAwsIndexComponent extends Component {
// =actions
/**
* Allows to update the credential type
* @param type {string}
* @param {string} type
*/
@action
updateCredentialTypeSelection(type) {
@@ -315,30 +315,42 @@
{{/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 data-test-worker-filter-error as |E|>
{{#each @model.errors.worker_filter as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#let (unique-id) (unique-id) as |labelId helpId|}}
<Hds::Form::Fieldset
aria-labelledby={{labelId}}
aria-describedby={{helpId}}
class='storage-bucket-worker-filter'
@isRequired={{true}}
as |F|
>
<F.Legend id={{labelId}}>{{t
'resources.storage-bucket.form.worker_filter.label'
}}</F.Legend>
<F.HelperText {{helpId}}>
{{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>
<F.Control>
{{#if form.disabled}}
<Hds::CodeBlock
@maxHeight='300px'
@language='bash'
@value={{@model.worker_filter}}
@hasCopyButton={{true}}
data-test-worker-filter
/>
{{else}}
<WorkerFilterGenerator
@model={{@model}}
@name='worker_filter'
@hideToolbar={{true}}
/>
{{/if}}
</F.Control>
</Hds::Form::Fieldset>
{{/let}}
{{! disable_credential_rotation }}
<Hds::Form::Checkbox::Field
@@ -0,0 +1,21 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
class='full-width'
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
as |form|
>
<WorkerFilterGenerator @model={{@model}} @name={{@name}} />
{{#if (can 'save model' @model)}}
<form.actions
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,198 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::Field as |F|>
<F.Control>
<Rose::CodeEditor as |c|>
{{#unless @hideToolbar}}
<c.toolbar @copyText={{get @model @name}} />
{{/unless}}
<c.fieldEditor
@onInput={{fn this.setWorkerFilter @model @name}}
@value={{or (get @model @name) ''}}
@options={{hash
mode='text/x-sh'
gutters=(array 'CodeMirror-lint-markers')
lint='true'
}}
/>
</Rose::CodeEditor>
</F.Control>
{{#if (get @model.errors @name)}}
<F.Error data-test-worker-filter-error as |E|>
{{#each (get @model.errors @name) as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Field>
<Hds::Separator />
<Hds::Form::Toggle::Field
name='show_filter_generator'
{{on 'change' this.toggleFilterGenerator}}
as |F|
>
<F.Label>{{t 'worker-filter-generator.toggle.title'}}</F.Label>
<F.HelperText>{{t
'worker-filter-generator.toggle.description'
}}</F.HelperText>
</Hds::Form::Toggle::Field>
<Hds::Separator />
{{#if this.showFilterGenerator}}
<Hds::Form::Radio::Group
class='filter-generator-selection'
@name='filter_generator'
{{on 'change' this.setGeneratorType}}
as |G|
>
<G.Legend>{{t 'worker-filter-generator.filter-generator.title'}}</G.Legend>
<G.HelperText>
{{t 'worker-filter-generator.filter-generator.description'}}
<br />
<Hds::Link::Inline
@href={{doc-url 'worker-filters-format'}}
@color='secondary'
>
{{t 'worker-filter-generator.filter-generator.link'}}
</Hds::Link::Inline>
</G.HelperText>
<G.RadioField
@value={{this.generatorTagType}}
checked={{eq this.selectedGeneratorType this.generatorTagType}}
as |F|
>
<F.Label>{{t
'worker-filter-generator.filter-generator.tag.label'
}}</F.Label>
<F.HelperText>{{t
'worker-filter-generator.filter-generator.tag.helper'
}}</F.HelperText>
</G.RadioField>
<G.RadioField
@value={{this.generatorNameType}}
checked={{eq this.selectedGeneratorType this.generatorNameType}}
as |F|
>
<F.Label>{{t
'worker-filter-generator.filter-generator.name.label'
}}</F.Label>
<F.HelperText>{{t
'worker-filter-generator.filter-generator.name.helper'
}}</F.HelperText>
</G.RadioField>
</Hds::Form::Radio::Group>
{{#let (unique-id) (unique-id) as |labelId helpId|}}
<Hds::Form::Fieldset
aria-labelledby={{labelId}}
aria-describedby={{helpId}}
class='input-values'
as |F|
>
<F.Legend id={{labelId}}>{{t
'worker-filter-generator.filter-generator.input-values.title'
}}</F.Legend>
<F.HelperText id={{helpId}}>{{t
'worker-filter-generator.filter-generator.input-values.description'
}}</F.HelperText>
<F.Control>
{{#if (eq this.selectedGeneratorType this.generatorTagType)}}
<Hds::Form::TextInput::Field
@value={{this.key}}
name='tag_key'
@width='320px'
{{on 'input' (set-from-event this 'key')}}
as |F|
>
<F.Label>{{t 'form.key.label'}}</F.Label>
</Hds::Form::TextInput::Field>
{{else}}
<Hds::Form::Select::Field
name='name_operator'
@value={{this.operator}}
@width='320px'
{{on 'change' (set-from-event this 'operator')}}
as |F|
>
<F.Label>{{t 'form.operator.label'}}</F.Label>
<F.Options>
<option value=''>
{{t 'titles.choose-an-option'}}
</option>
{{#each this.operatorOptions as |operator|}}
<option
value={{operator}}
selected={{eq operator this.operator}}
>
{{#if (eq operator '==')}}
{{operator}}
{{else}}
{{t
(concat
'worker-filter-generator.filter-generator.operator.'
operator
)
}}
{{/if}}
</option>
{{/each}}
</F.Options>
</Hds::Form::Select::Field>
{{/if}}
</F.Control>
<F.Control>
<Hds::Form::TextInput::Field
@value={{this.value}}
name='tag_value'
@width='320px'
{{on 'input' (set-from-event this 'value')}}
as |F|
>
<F.Label>{{t 'form.value.label'}}</F.Label>
</Hds::Form::TextInput::Field>
</F.Control>
</Hds::Form::Fieldset>
{{/let}}
{{#let (unique-id) (unique-id) as |labelId helpId|}}
<Hds::Form::Fieldset
aria-labelledby={{labelId}}
aria-describedby={{helpId}}
class='formatted-results'
@layout='vertical'
as |F|
>
<F.Legend id={{labelId}}>{{t
'worker-filter-generator.filter-generator.formatted-result.title'
}}</F.Legend>
<F.HelperText id={{helpId}}>{{t
'worker-filter-generator.filter-generator.formatted-result.description'
}}</F.HelperText>
<F.Control>
<Hds::Text::Body @tag='p' @weight='semibold'>{{t
'worker-filter-generator.filter-generator.formatted-result.label'
}}</Hds::Text::Body>
<div class='generated-results-container'>
<Hds::Form::TextInput::Base
readonly
name='generated_value'
@value={{this.generatedResult}}
@width='320px'
/>
{{#if this.generatedResult}}
<Hds::Copy::Button
@text='Copy'
@textToCopy={{this.generatedResult}}
/>
{{/if}}
</div>
</F.Control>
</Hds::Form::Fieldset>
{{/let}}
{{/if}}
@@ -0,0 +1,70 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class WorkerFilterGeneratorIndexComponent extends Component {
// =attributes
generatorTagType = 'tag';
generatorNameType = 'name';
operatorOptions = ['==', 'matches', 'contains'];
@tracked showFilterGenerator;
@tracked selectedGeneratorType = this.generatorTagType;
@tracked key = '';
@tracked value = '';
@tracked operator = '';
/**
* @return {string}
*/
get generatedResult() {
let valueResult = this.value ? `"${this.value}"` : '';
if (this.selectedGeneratorType === 'tag') {
let keyResult = this.key ? `"/tags/${this.key}"` : '';
return keyResult || valueResult ? `${valueResult} in ${keyResult}` : '';
} else {
return valueResult || this.operator
? `"/name" ${this.operator} ${valueResult}`
: '';
}
}
// =actions
/**
* Sets the model filter to the passed in value.
* @param {Model} model
* @param {string} filter
* @param {string} value
*/
@action
setWorkerFilter(model, filter, value) {
model[filter] = value;
}
/**
* Toggles showFilterGenerator attribute to true or false.
*/
@action
toggleFilterGenerator() {
this.showFilterGenerator = !this.showFilterGenerator;
}
/**
* Clears out key, value, and operator.
* Sets selectedGeneratorType to a value.
* @param {object} event
*/
@action
setGeneratorType(event) {
this.key = '';
this.value = '';
this.operator = '';
this.selectedGeneratorType = event.target.value;
}
}
@@ -1,41 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
class='full-width'
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
as |form|
>
<Hds::Form::Field as |F|>
<F.Control>
<Rose::CodeEditor as |c|>
<c.toolbar @copyText={{get @model @name}} />
<c.fieldEditor
@onInput={{fn this.setWorkerFilter @model @name}}
@value={{or (get @model @name) ''}}
@options={{hash
mode='text/x-sh'
gutters=(array 'CodeMirror-lint-markers')
lint='true'
}}
/>
</Rose::CodeEditor>
</F.Control>
{{#if (get @model.errors @name)}}
<F.Error as |E|>
{{#each (get @model.errors @name) as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Field>
<form.actions
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
</Rose::Form>
@@ -1,22 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class WorkerFilterComponent extends Component {
// =actions
/**
* Sets the model filter to the passed in value.
* @param {Model} model
* @param {string} filter
* @param {string} value
*/
@action
setWorkerFilter(model, filter, value) {
model[filter] = value;
}
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreEditWorkerFilterController extends Controller {
@controller('scopes/scope/credential-stores/index') credentialStores;
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Controller, { inject as controller } from '@ember/controller';
export default class ScopesScopeCredentialStoresCredentialStoreWorkerFilterController extends Controller {
@controller('scopes/scope/credential-stores/index') credentialStores;
}
@@ -126,4 +126,31 @@ export default class ScopesScopeCredentialStoresIndexController extends Controll
this[paramKey] = [...selectedItems];
this.page = 1;
}
/**
* Save worker filter
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.add-success')
async saveWorkerFilter(credentialStore) {
await credentialStore.save();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.worker-filter',
);
}
/**
* Cancel adding or editing a worker filter
* @param {CredentialStoreModel} credentialStore
*/
@action
async cancelWorkerFilter(credentialStore) {
credentialStore.rollbackAttributes();
await this.router.replaceWith(
'scopes.scope.credential-stores.credential-store.worker-filter',
);
}
}
+2
View File
@@ -150,6 +150,8 @@ Router.map(function () {
function () {},
);
});
this.route('worker-filter');
this.route('edit-worker-filter');
},
);
this.route('new');
@@ -0,0 +1,8 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
export default class ScopesScopeCredentialStoresCredentialStoreEditWorkerFilterRoute extends Route {}
@@ -0,0 +1,8 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Route from '@ember/routing/route';
export default class ScopesScopeCredentialStoresCredentialStoreWorkerFilterRoute extends Route {}
+27
View File
@@ -1056,3 +1056,30 @@
padding: 0.25rem 0.75rem;
}
}
// worker filter generator
.filter-generator-selection {
.hds-form-group__control-field {
margin-bottom: 0;
}
}
.formatted-results {
p {
margin-bottom: 0.5rem;
}
}
.generated-results-container {
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: flex-start;
}
// storage buckets
.storage-bucket-worker-filter {
.hds-form-field--layout-flag {
margin-bottom: 0;
}
}
@@ -0,0 +1,37 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{page-title
(t 'resources.credential-store.worker-filter.messages.edit.title')
}}
<Breadcrumbs::Item
@text={{t 'resources.credential-store.worker-filter.messages.edit.title'}}
@route='scopes.scope.credential-stores.credential-store.edit-worker-filter'
/>
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
<page.header>
<Hds::Text::Display @tag='h2' @size='300' class='h2'>
{{t 'resources.credential-store.worker-filter.messages.edit.title'}}
<DocLink @doc='worker-filters-format' @iconSize='large' />
</Hds::Text::Display>
<Hds::Text::Body @tag='p' @size='300'>
{{t 'resources.credential-store.worker-filter.messages.edit.description'}}
</Hds::Text::Body>
</page.header>
<page.body>
<Form::WorkerFilter
@name='worker_filter'
@model={{@model}}
@submit={{fn this.credentialStores.saveWorkerFilter @model}}
@cancel={{fn this.credentialStores.cancelWorkerFilter @model}}
/>
</page.body>
</Rose::Layout::Page>
@@ -0,0 +1,72 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{page-title (t 'resources.credential-store.worker-filter.title')}}
<Breadcrumbs::Item
@text={{t 'resources.credential-store.worker-filter.title'}}
@route='scopes.scope.credential-stores.credential-store.worker-filter'
/>
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<Breadcrumbs::Container />
</page.breadcrumbs>
<page.header>
<CredentialStores::CredentialStore::Header @model={{@model}} />
</page.header>
<page.navigation>
<CredentialStores::CredentialStore::Nav @model={{@model}} />
</page.navigation>
<page.actions>
<CredentialStores::CredentialStore::Actions
@model={{@model}}
@delete={{fn this.credentialStores.delete @model}}
/>
</page.actions>
<page.body>
{{#if @model.worker_filter}}
<Hds::CodeBlock
@maxHeight='300px'
@language='bash'
@value={{@model.worker_filter}}
@hasCopyButton={{true}}
as |CB|
>
<CB.Title>
{{t 'resources.credential-store.worker-filter.title'}}
</CB.Title>
<CB.Description>
{{t 'resources.credential-store.worker-filter.description'}}
</CB.Description>
</Hds::CodeBlock>
{{else}}
<Rose::Layout::Centered>
<Hds::ApplicationState as |A|>
<A.Header
@title={{t
'resources.credential-store.worker-filter.messages.none.title'
}}
/>
<A.Body
@text={{t
'resources.credential-store.worker-filter.messages.none.description'
}}
/>
<A.Footer as |F|>
<F.LinkStandalone
@icon='plus-circle'
@text={{t 'resources.credential-store.actions.add'}}
@route='scopes.scope.credential-stores.credential-store.edit-worker-filter'
/>
</A.Footer>
</Hds::ApplicationState>
</Rose::Layout::Centered>
{{/if}}
</page.body>
</Rose::Layout::Page>
@@ -17,7 +17,7 @@
<page.header>
<h2>
{{t 'resources.target.workers.edit-egress-worker-filter.title'}}
<DocLink @doc='target.worker-filters' @iconSize='large' />
<DocLink @doc='worker-filters-format' @iconSize='large' />
</h2>
<p>{{t
'resources.target.workers.edit-egress-worker-filter.description'
@@ -25,7 +25,7 @@
</page.header>
<page.body class='target-workers'>
<WorkerFilter
<Form::WorkerFilter
@name='egress_worker_filter'
@model={{@model}}
@submit={{fn this.targets.saveWorkerFilter @model}}
@@ -18,7 +18,7 @@
<page.header>
<h2>
{{t 'resources.target.workers.edit-ingress-worker-filter.title'}}
<DocLink @doc='target.worker-filters' @iconSize='large' />
<DocLink @doc='worker-filters-format' @iconSize='large' />
</h2>
<p>{{t
'resources.target.workers.edit-ingress-worker-filter.description'
@@ -26,7 +26,7 @@
</page.header>
<page.body class='target-workers'>
<WorkerFilter
<Form::WorkerFilter
@name='ingress_worker_filter'
@model={{@model}}
@submit={{fn this.targets.saveWorkerFilter @model}}
@@ -51,9 +51,13 @@
'resources.target.workers.accordion-label.ingress-workers'
}}</span>
<Hds::Link::Standalone
@icon='edit'
@icon={{if @model.ingress_worker_filter 'edit' 'plus-circle'}}
@text={{t
'resources.target.workers.actions.edit-worker-filter'
(if
@model.ingress_worker_filter
'resources.target.workers.actions.edit-worker-filter'
'resources.target.workers.actions.add-worker-filter'
)
}}
@route='scopes.scope.targets.target.edit-ingress-worker-filter'
/>
@@ -62,6 +66,7 @@
<:content>
{{#if @model.ingress_worker_filter}}
<Hds::CodeBlock
@maxHeight='300px'
@language='bash'
@hasCopyButton={{true}}
@value={{@model.ingress_worker_filter}}
@@ -101,8 +106,14 @@
'resources.target.workers.accordion-label.egress-workers'
}}</span>
<Hds::Link::Standalone
@icon='edit'
@text={{t 'resources.target.workers.actions.edit-worker-filter'}}
@icon={{if @model.egress_worker_filter 'edit' 'plus-circle'}}
@text={{t
(if
@model.egress_worker_filter
'resources.target.workers.actions.edit-worker-filter'
'resources.target.workers.actions.add-worker-filter'
)
}}
@route='scopes.scope.targets.target.edit-egress-worker-filter'
/>
</div>
@@ -110,6 +121,7 @@
<:content>
{{#if @model.egress_worker_filter}}
<Hds::CodeBlock
@maxHeight='300px'
@language='bash'
@hasCopyButton={{true}}
@value={{@model.egress_worker_filter}}
+2 -2
View File
@@ -101,8 +101,8 @@ module.exports = function (environment) {
'target.add-host-sources': '/docs/concepts/domain-model/host-sets',
'target.enable-session-recording':
'/docs/configuration/session-recording/enable-session-recording',
'target.worker-filters':
'/docs/concepts/filtering/worker-tags#worker-filtering',
'worker-filters-format':
'/docs/concepts/filtering/worker-tags#filter-examples',
user: '/docs/concepts/domain-model/users',
downloads: '/install',
'getting-started.desktop':
@@ -0,0 +1,13 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// Manage actions dropdown selectors
export const MANAGE_DROPDOWN =
'[data-test-manage-credential-stores-dropdown] button:first-child';
export const EDIT_ACTION =
'[data-test-manage-credential-stores-dropdown] ul li:nth-child(2) a';
export const CODE_EDITOR_BODY = '[data-test-code-editor-field-editor] textarea';
export const CODE_BLOCK_BODY = '.hds-code-block__code';
@@ -10,6 +10,8 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { Response } from 'miragejs';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
module('Acceptance | credential-stores | update', function (hooks) {
setupApplicationTest(hooks);
@@ -59,7 +61,13 @@ module('Acceptance | credential-stores | update', function (hooks) {
urls.credentialStores = `${urls.projectScope}/credential-stores`;
urls.vaultCredentialStore = `${urls.credentialStores}/${instances.vaultCredentialStore.id}`;
urls.staticCredentialStore = `${urls.credentialStores}/${instances.staticCredentialStore.id}`;
urls.workerFilter = `${urls.credentialStores}/${instances.vaultCredentialStore.id}/worker-filter`;
urls.editWorkerFilter = `${urls.credentialStores}/${instances.vaultCredentialStore.id}/edit-worker-filter`;
await authenticateSession({});
// Enable feature flag
const featuresService = this.owner.lookup('service:features');
featuresService.enable('vault-worker-filter');
});
test('can save changes to existing static credential store', async function (assert) {
@@ -295,4 +303,63 @@ module('Acceptance | credential-stores | update', function (hooks) {
);
}
});
test('visiting static credential store does not show worker filter tab', async function (assert) {
await visit(urls.staticCredentialStore);
assert.dom(commonSelectors.HREF(urls.workerFilter)).doesNotExist();
});
test('user can click vault credential store worker filter tab and be rerouted to correct url', async function (assert) {
await visit(urls.vaultCredentialStore);
assert.dom(commonSelectors.HREF(urls.workerFilter)).exists();
await click(commonSelectors.HREF(urls.workerFilter));
assert.strictEqual(currentURL(), urls.workerFilter);
});
test('manage actions dropdown displays edit option and routes to correct url', async function (assert) {
await visit(urls.vaultCredentialStore);
await click(commonSelectors.HREF(urls.workerFilter));
await click(selectors.MANAGE_DROPDOWN);
await click(selectors.EDIT_ACTION);
assert.strictEqual(currentURL(), urls.editWorkerFilter);
});
test('when work filters code editor is empty, save btn reroutes to empty state template', async function (assert) {
instances.vaultCredentialStore.update({
attributes: { worker_filter: null },
});
await visit(urls.vaultCredentialStore);
await click(commonSelectors.HREF(urls.workerFilter));
assert.dom('.hds-application-state').exists();
assert
.dom('.hds-application-state')
.hasText(
`No worker filter added You haven't added a worker filter yet. Add Worker Filter`,
);
});
test('when worker filter exists, readonly code block displays the filter text', async function (assert) {
instances.vaultCredentialStore.update({
attributes: { worker_filter: null },
});
await visit(urls.vaultCredentialStore);
await click(commonSelectors.HREF(urls.workerFilter));
await click(selectors.MANAGE_DROPDOWN);
await click(selectors.EDIT_ACTION);
await fillIn(selectors.CODE_EDITOR_BODY, '"bar" in "/tags/foo"');
await click(commonSelectors.SAVE_BTN);
assert.dom(selectors.CODE_BLOCK_BODY).exists();
assert.dom(selectors.CODE_BLOCK_BODY).includesText('"bar" in "/tags/foo"');
});
});
@@ -7,7 +7,6 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn, select } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { authenticateSession } from 'ember-simple-auth/test-support';
import * as selectors from './selectors';
@@ -58,6 +57,11 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await select(selectors.FIELD_SCOPE, 'global');
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
assert.dom(selectors.FIELD_BUCKET_NAME).isNotDisabled();
assert.dom(selectors.FIELD_BUCKET_PREFIX).isNotDisabled();
assert.dom(selectors.FIELD_BUCKET_NAME).doesNotHaveAttribute('readOnly');
@@ -70,6 +74,9 @@ module('Acceptance | storage-buckets | create', function (hooks) {
assert.strictEqual(storageBucket.name, commonSelectors.FIELD_NAME_VALUE);
assert.strictEqual(storageBucket.scopeId, 'global');
assert
.dom(selectors.READONLY_WORKER_FILTER)
.hasText(selectors.EDITOR_WORKER_FILTER_VALUE);
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
@@ -81,6 +88,11 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await select(selectors.FIELD_SCOPE, instances.scopes.org.id);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
assert.dom(selectors.FIELD_BUCKET_NAME).isNotDisabled();
assert.dom(selectors.FIELD_BUCKET_PREFIX).isNotDisabled();
assert.dom(selectors.FIELD_BUCKET_NAME).doesNotHaveAttribute('readOnly');
@@ -120,6 +132,11 @@ module('Acceptance | storage-buckets | create', function (hooks) {
);
await fillIn(selectors.FIELD_ACCESS_KEY, selectors.FIELD_ACCESS_KEY_VALUE);
await fillIn(selectors.FIELD_SECRET_KEY, selectors.FIELD_SECRET_KEY_VALUE);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
// Assertions
@@ -152,6 +169,10 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await click(selectors.FIELD_DYNAMIC_CREDENTIAL);
await fillIn(selectors.FIELD_ROLE_ARN, selectors.FIELD_ROLE_ARN_VALUE);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
const storageBucket = this.server.schema.storageBuckets.findBy({
@@ -175,6 +196,10 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await click(selectors.FIELD_STATIC_CREDENTIAL);
await fillIn(selectors.FIELD_ACCESS_KEY, selectors.FIELD_ACCESS_KEY_VALUE);
await fillIn(selectors.FIELD_SECRET_KEY, selectors.FIELD_SECRET_KEY_VALUE);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
@@ -208,6 +233,10 @@ module('Acceptance | storage-buckets | create', function (hooks) {
);
await fillIn(selectors.FIELD_ACCESS_KEY, selectors.FIELD_ACCESS_KEY_VALUE);
await fillIn(selectors.FIELD_SECRET_KEY, selectors.FIELD_SECRET_KEY_VALUE);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
@@ -264,8 +293,11 @@ module('Acceptance | storage-buckets | create', function (hooks) {
await visit(urls.storageBuckets);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
await a11yAudit();
assert
.dom(commonSelectors.ALERT_TOAST_BODY)
@@ -29,6 +29,11 @@ export const FIELD_SECRET_KEY_EDIT_BTN =
'[data-test-secret-access-key] [type=button]';
export const FIELD_WORKER_FILTER = '[name=worker_filter]';
export const FIELD_WORKER_FILTER_ERROR = '[data-test-worker-filter-error]';
export const READONLY_WORKER_FILTER = '[data-test-worker-filter]';
export const EDITOR_WORKER_FILTER =
'[data-test-code-editor-field-editor] textarea';
export const EDITOR_WORKER_FILTER_VALUE = '"dev" in "/tags/env"';
export const TOAST = '[role=alert] div';
export const TOAST_SUCCESSFUL_VALUE = 'Saved successfully.';
export const TABLE_FIRST_ROW_ACTION_DROPDOWN =
@@ -22,12 +22,13 @@ module('Acceptance | storage-buckets | update', function (hooks) {
global: null,
},
storageBucket: null,
storageBucketMinio: null,
};
const urls = {
globalScope: null,
storageBuckets: null,
storageBucket: null,
storageBucketMinio: null,
};
hooks.beforeEach(async function () {
@@ -39,8 +40,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
scope: instances.scopes.global,
plugin: { name: 'minio' },
});
urls.globalScope = `/scopes/global`;
urls.storageBuckets = `${urls.globalScope}/storage-buckets`;
urls.storageBuckets = `/scopes/global/storage-buckets`;
urls.storageBucket = `${urls.storageBuckets}/${instances.storageBucket.id}`;
urls.storageBucketMinio = `${urls.storageBuckets}/${instances.storageBucketMinio.id}`;
@@ -51,7 +51,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
test('can save changes to an existing storage-bucket', async function (assert) {
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.HREF(urls.storageBucket));
await click(commonSelectors.EDIT_BTN, 'Click edit mode');
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await click(commonSelectors.SAVE_BTN, 'Click save');
@@ -70,7 +70,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
const name = instances.storageBucket.name;
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.HREF(urls.storageBucket));
await click(commonSelectors.EDIT_BTN, 'Click edit mode');
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await click(commonSelectors.CANCEL_BTN, 'Click cancel');
@@ -82,7 +82,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
test('can save changes to access key fields', async function (assert) {
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.HREF(urls.storageBucket));
assert.dom(selectors.FIELD_ACCESS_KEY_EDIT_BTN).exists();
assert.dom(selectors.FIELD_SECRET_KEY_EDIT_BTN).exists();
@@ -117,7 +117,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
test('can cancel changes to access key fields', async function (assert) {
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.HREF(urls.storageBucket));
await click(commonSelectors.EDIT_BTN, 'Click edit mode');
await click(selectors.FIELD_ACCESS_KEY_EDIT_BTN);
await click(selectors.FIELD_SECRET_KEY_EDIT_BTN);
@@ -165,7 +165,10 @@ module('Acceptance | storage-buckets | update', function (hooks) {
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
await fillIn(selectors.FIELD_WORKER_FILTER, 'random string');
await fillIn(
selectors.EDITOR_WORKER_FILTER,
selectors.EDITOR_WORKER_FILTER_VALUE,
);
await click(commonSelectors.SAVE_BTN);
assert
@@ -182,7 +185,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
test('user cannot edit scope, provider, bucket name, bucket prefix and region fields in a Amazon S3 storage bucket form', async function (assert) {
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucket}"]`);
await click(commonSelectors.HREF(urls.storageBucket));
assert.dom(selectors.FIELD_BUCKET_NAME).isDisabled();
assert.dom(selectors.FIELD_BUCKET_PREFIX).isDisabled();
@@ -202,7 +205,7 @@ module('Acceptance | storage-buckets | update', function (hooks) {
test('user cannot edit scope, provider, endpoint_url, bucket name or region fields in a MinIO storage bucket', async function (assert) {
await visit(urls.storageBuckets);
await click(`[href="${urls.storageBucketMinio}"]`);
await click(commonSelectors.HREF(urls.storageBucketMinio));
assert.dom(selectors.FIELD_BUCKET_NAME).isDisabled();
@@ -214,4 +217,17 @@ module('Acceptance | storage-buckets | update', function (hooks) {
assert.dom(selectors.FIELD_BUCKET_NAME).hasAttribute('readOnly');
assert.dom(selectors.FIELD_REGION).hasAttribute('readOnly');
});
test('user sees an editable code editor while updating and readonly code block before/after', async function (assert) {
await visit(urls.storageBuckets);
await click(commonSelectors.HREF(urls.storageBucketMinio));
assert.dom(selectors.READONLY_WORKER_FILTER).isVisible();
assert.dom(selectors.EDITOR_WORKER_FILTER).doesNotExist();
await click(commonSelectors.EDIT_BTN, 'Click edit mode');
assert.dom(selectors.EDITOR_WORKER_FILTER).isVisible();
assert.dom(selectors.READONLY_WORKER_FILTER).doesNotExist();
});
});
@@ -7,7 +7,6 @@ import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_TARGET_SSH } from 'api/models/target';
@@ -30,6 +29,9 @@ module(
const NAME_FIELD_TEXT = 'random string';
const BUCKET_NAME_FIELD_SELECTOR = '[name="bucket_name"]';
const BUCKET_PREFIX_FIELD_SELECTOR = '[name="bucket_prefix"]';
const EDITOR_WORKER_FILTER =
'[data-test-code-editor-field-editor] textarea';
const EDITOR_WORKER_FILTER_VALUE = '"dev" in "/tags/env"';
const instances = {
scopes: {
@@ -84,6 +86,7 @@ module(
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click('[value="global"]');
await fillIn(EDITOR_WORKER_FILTER, EDITOR_WORKER_FILTER_VALUE);
assert.dom(BUCKET_NAME_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).isNotDisabled();
@@ -107,6 +110,7 @@ module(
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click(`[value="${instances.scopes.org.scope.id}"]`);
await fillIn(EDITOR_WORKER_FILTER, EDITOR_WORKER_FILTER_VALUE);
assert.dom(BUCKET_NAME_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).isNotDisabled();
@@ -158,8 +162,8 @@ module(
await visit(urls.enableSessionRecording);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(EDITOR_WORKER_FILTER, EDITOR_WORKER_FILTER_VALUE);
await click(SAVE_BTN_SELECTOR);
await a11yAudit();
assert.dom(ALERT_TEXT_SELECTOR).hasText('The request was invalid.');
assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.');
@@ -17,6 +17,8 @@ module('Acceptance | targets | workers', function (hooks) {
let featuresService;
let featureEdition;
const ACCORDION_DROPDOWN_TEXT_SELECTOR = (name) =>
`[data-test-target-${name}-workers-accordion-item] a`;
const ACCORDION_DROPDOWN_SELECTOR = (name) =>
`[data-test-target-${name}-workers-accordion-item] .hds-accordion-item__button`;
const CODE_BLOCK_SELECTOR = (name) =>
@@ -244,6 +246,38 @@ module('Acceptance | targets | workers', function (hooks) {
assert.dom(CODE_BLOCK_SELECTOR('egress')).hasText(egressWorkerFilter);
});
test('user will see "Add worker filter" if no filter set', async function (assert) {
featuresService.enable('target-worker-filters-v2-ingress');
instances.target.update({
egress_worker_filter: '',
ingress_worker_filter: '',
});
await visit(urls.target);
await click(`[href="${urls.targetWorkers}"]`);
assert
.dom(ACCORDION_DROPDOWN_TEXT_SELECTOR('egress'))
.hasText('Add worker filter');
assert
.dom(ACCORDION_DROPDOWN_TEXT_SELECTOR('ingress'))
.hasText('Add worker filter');
});
test('user will see "Edit worker filter" if filter is set', async function (assert) {
featuresService.enable('target-worker-filters-v2-ingress');
await visit(urls.target);
await click(`[href="${urls.targetWorkers}"]`);
assert
.dom(ACCORDION_DROPDOWN_TEXT_SELECTOR('egress'))
.hasText('Edit worker filter');
assert
.dom(ACCORDION_DROPDOWN_TEXT_SELECTOR('ingress'))
.hasText('Edit worker filter');
});
test('user can cancel changes to egress worker filter in a target', async function (assert) {
const egressWorkerFilter = '"random" in "/worker/filters"';
await visit(urls.target);
@@ -0,0 +1,135 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click, fillIn, select } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module(
'Integration | Component | worker-filter-generator/index',
function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
const CODE_EDITOR = '[data-test-code-editor-field-editor]';
const CODE_EDITOR_LINE =
'[data-test-code-editor-field-editor] .CodeMirror-line';
const FILTER_GENERATOR = '[name="filter_generator"]';
const SHOW_FILTER_GENERATOR = '[name="show_filter_generator"]';
const TAG_TYPE_OPTION = '[value="tag"]';
const NAME_TYPE_OPTION = '[value="name"]';
const TAG_KEY = '[name="tag_key"]';
const TAG_VALUE = '[name="tag_value"]';
const NAME_OPERATOR = '[name="name_operator"]';
const GENERATED_VALUE = '[name="generated_value"]';
test('it renders correct content when egress_worker_filter is passed in', async function (assert) {
this.model = { egress_worker_filter: 'egress filter' };
await render(
hbs`<WorkerFilterGenerator @name='egress_worker_filter' @model={{this.model}} />`,
);
assert.dom(CODE_EDITOR).isVisible();
assert.dom(CODE_EDITOR_LINE).hasText(this.model.egress_worker_filter);
});
test('it renders correct content when ingress_worker_filter is passed in', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
assert.dom(CODE_EDITOR).isVisible();
assert.dom(CODE_EDITOR_LINE).hasText(this.model.ingress_worker_filter);
});
test('toggleFilterGenerator shows filter generator when toggled on', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
assert.dom(FILTER_GENERATOR).isNotVisible();
await click(SHOW_FILTER_GENERATOR);
assert.dom(FILTER_GENERATOR).isVisible();
});
test('filter generator tag type shows key and value input boxes', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
await click(SHOW_FILTER_GENERATOR);
await click(TAG_TYPE_OPTION);
assert.dom(TAG_KEY).isVisible();
assert.dom(TAG_VALUE).isVisible();
assert.dom(NAME_OPERATOR).isNotVisible();
});
test('filter generator tag type generates correctly formatted filter', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
await click(SHOW_FILTER_GENERATOR);
await click(TAG_TYPE_OPTION);
await fillIn(TAG_KEY, 'key1');
await fillIn(TAG_VALUE, 'val1');
assert.dom(GENERATED_VALUE).hasValue('"val1" in "/tags/key1"');
});
test('filter generator name type shows operator and value fields', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
await click(SHOW_FILTER_GENERATOR);
await click(NAME_TYPE_OPTION);
assert.dom(NAME_OPERATOR).isVisible();
assert.dom(TAG_VALUE).isVisible();
assert.dom(TAG_KEY).isNotVisible();
});
test('filter generator name type generates correctly formatted filter', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
await click(SHOW_FILTER_GENERATOR);
await click(NAME_TYPE_OPTION);
await fillIn(TAG_VALUE, 'val1');
await select(NAME_OPERATOR, '==');
assert.dom(GENERATED_VALUE).hasValue('"/name" == "val1"');
});
test('generated result is cleared when switching filter types', async function (assert) {
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilterGenerator @name='ingress_worker_filter' @model={{this.model}} />`,
);
await click(SHOW_FILTER_GENERATOR);
await click(NAME_TYPE_OPTION);
await fillIn(TAG_VALUE, 'val1');
await select(NAME_OPERATOR, '==');
assert.dom(GENERATED_VALUE).hasValue('"/name" == "val1"');
await click(TAG_TYPE_OPTION);
assert.dom(GENERATED_VALUE).hasNoValue();
});
},
);
@@ -1,45 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | worker-filter/index', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
test('it renders correct content when egress_worker_filter is passed in', async function (assert) {
this.onInput = () => {};
this.submit = () => {};
this.cancel = () => {};
this.model = { egress_worker_filter: 'egress filter' };
await render(
hbs`<WorkerFilter @name='egress_worker_filter' @model={{this.model}} @onInput={{this.onInput}} @submit={{this.submit}} @cancel={{this.cancel}} />`,
);
assert.dom('[data-test-code-editor-field-editor]').isVisible();
assert
.dom('[data-test-code-editor-field-editor] .CodeMirror-line')
.hasText(this.model.egress_worker_filter);
});
test('it renders correct content when ingress_worker_filter is passed in', async function (assert) {
this.onInput = () => {};
this.submit = () => {};
this.cancel = () => {};
this.model = { ingress_worker_filter: 'ingress filter' };
await render(
hbs`<WorkerFilter @name='ingress_worker_filter' @model={{this.model}} @onInput={{this.onInput}} @submit={{this.submit}} @cancel={{this.cancel}}/>`,
);
assert.dom('[data-test-code-editor-field-editor]').isVisible();
assert
.dom('[data-test-code-editor-field-editor] .CodeMirror-line')
.hasText(this.model.ingress_worker_filter);
});
});
@@ -0,0 +1,22 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Controller | scopes/scope/credential-stores/credential-store/edit-worker-filter',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/credential-stores/credential-store/edit-worker-filter',
);
assert.ok(controller);
assert.ok(controller.credentialStores);
});
},
);
@@ -0,0 +1,22 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Controller | scopes/scope/credential-stores/credential-store/worker-filter',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/credential-stores/credential-store/worker-filter',
);
assert.ok(controller);
assert.ok(controller.credentialStores);
});
},
);
@@ -0,0 +1,21 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Route | scopes/scope/credential-stores/credential-store/edit-worker-filter',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/credential-stores/credential-store/edit-worker-filter',
);
assert.ok(route);
});
},
);
@@ -0,0 +1,21 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Route | scopes/scope/credential-stores/credential-store/worker-filter',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/credential-stores/credential-store/worker-filter',
);
assert.ok(route);
});
},
);