mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-06 04:46:25 +02:00
Sync Destinations List Name Filter Updates (#24695)
* updates destination name filter to use FilterInput component * simplifies destinations list redirect condition * fixes issue with sync destination type filter and issue filtering by both name and type * unsets page query param in sync destination secrets route
This commit is contained in:
parent
87ab7497fa
commit
3153673894
@ -110,6 +110,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.opacity-050 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.opacity-060 {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<div>
|
||||
<div class="field">
|
||||
<p class="control has-icons-left has-icons-right">
|
||||
<span class="input has-text-grey-light">{{or @placeholder "Search"}}</span>
|
||||
<span class="input opacity-050">{{or @placeholder "Search"}}</span>
|
||||
<Icon @name="search" class="search-icon has-text-grey-light" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -28,18 +28,17 @@
|
||||
class="is-marginless"
|
||||
data-test-filter="type"
|
||||
/>
|
||||
<SearchSelect
|
||||
@options={{this.destinationNames}}
|
||||
@objectKeys={{array "id" "name"}}
|
||||
@passObject={{true}}
|
||||
@selectLimit={{1}}
|
||||
@disallowNewItems={{true}}
|
||||
@placeholder="Filter by name"
|
||||
@inputValue={{if @nameFilter (array @nameFilter)}}
|
||||
@onChange={{fn this.onFilterChange "name"}}
|
||||
class="is-marginless has-left-padding-s"
|
||||
data-test-filter="name"
|
||||
/>
|
||||
<div class="has-left-margin-s">
|
||||
<FilterInput
|
||||
id="name-filter"
|
||||
aria-label="Filter by name"
|
||||
placeholder="Filter by name"
|
||||
value={{@nameFilter}}
|
||||
data-test-filter="name"
|
||||
@autofocus={{true}}
|
||||
@onInput={{fn this.onFilterChange "name"}}
|
||||
/>
|
||||
</div>
|
||||
</ToolbarFilters>
|
||||
<ToolbarActions>
|
||||
<ToolbarLink @route="secrets.destinations.create" @type="add" data-test-create-destination>
|
||||
|
||||
@ -9,6 +9,7 @@ import { action } from '@ember/object';
|
||||
import { getOwner } from '@ember/application';
|
||||
import errorMessage from 'vault/utils/error-message';
|
||||
import { findDestination, syncDestinations } from 'core/helpers/sync-destinations';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
@ -16,6 +17,7 @@ import type StoreService from 'vault/services/store';
|
||||
import type FlashMessageService from 'vault/services/flash-messages';
|
||||
import type { EngineOwner } from 'vault/vault/app-types';
|
||||
import type { SyncDestinationName, SyncDestinationType } from 'vault/vault/helpers/sync-destinations';
|
||||
import type Transition from '@ember/routing/transition';
|
||||
|
||||
interface Args {
|
||||
destinations: Array<SyncDestinationModel>;
|
||||
@ -28,15 +30,31 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly flashMessages: FlashMessageService;
|
||||
|
||||
// for some reason there isn't a full page refresh happening when transitioning on filter change
|
||||
// when the transition happens it causes the FilterInput component to lose focus since it can only focus on didInsert
|
||||
// to work around this, verify that a transition from this route was completed and then focus the input
|
||||
constructor(owner: unknown, args: Args) {
|
||||
super(owner, args);
|
||||
this.router.on('routeDidChange', this.focusNameFilter);
|
||||
}
|
||||
|
||||
willDestroy(): void {
|
||||
super.willDestroy();
|
||||
this.router.off('routeDidChange', this.focusNameFilter);
|
||||
}
|
||||
|
||||
focusNameFilter(transition?: Transition) {
|
||||
const route = 'vault.cluster.sync.secrets.destinations.index';
|
||||
if (transition?.from?.name === route && transition?.to?.name === route) {
|
||||
next(() => document.getElementById('name-filter')?.focus());
|
||||
}
|
||||
}
|
||||
|
||||
// typeFilter arg comes in as destination type but we need to pass the destination display name into the SearchSelect
|
||||
get typeFilterName() {
|
||||
return findDestination(this.args.typeFilter)?.name;
|
||||
}
|
||||
|
||||
get destinationNames() {
|
||||
return this.args.destinations.map((destination) => ({ id: destination.name, name: destination.name }));
|
||||
}
|
||||
|
||||
get destinationTypes() {
|
||||
return syncDestinations().map((d) => ({ id: d.name, name: d.type }));
|
||||
}
|
||||
@ -65,9 +83,10 @@ export default class SyncSecretsDestinationsPageComponent extends Component<Args
|
||||
}
|
||||
|
||||
@action
|
||||
onFilterChange(key: string, selectObject: Array<{ id: string; name: string } | undefined>) {
|
||||
onFilterChange(key: string, value: { id: string; name: string }[] | string | undefined) {
|
||||
const queryValue = Array.isArray(value) ? value[0]?.name : value;
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.destinations', {
|
||||
queryParams: { [key]: selectObject[0]?.name },
|
||||
queryParams: { [key]: queryValue },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ export default class DestinationsCreateForm extends Component<Args> {
|
||||
@waitFor
|
||||
*save(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.error = '';
|
||||
// clear out validation warnings
|
||||
this.modelValidations = null;
|
||||
const { destination } = this.args;
|
||||
|
||||
@ -8,12 +8,24 @@ import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import type StoreService from 'vault/services/store';
|
||||
import SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||
import type SyncAssociationModel from 'vault/vault/models/sync/association';
|
||||
import type Controller from '@ember/controller';
|
||||
|
||||
interface SyncDestinationSecretsRouteParams {
|
||||
page: string;
|
||||
}
|
||||
|
||||
interface SyncDestinationSecretsRouteModel {
|
||||
destination: SyncDestinationModel;
|
||||
associations: SyncAssociationModel[];
|
||||
}
|
||||
|
||||
interface SyncDestinationSecretsController extends Controller {
|
||||
model: SyncDestinationSecretsRouteModel;
|
||||
page: number | undefined;
|
||||
}
|
||||
|
||||
export default class SyncDestinationSecretsRoute extends Route {
|
||||
@service declare readonly store: StoreService;
|
||||
|
||||
@ -35,4 +47,10 @@ export default class SyncDestinationSecretsRoute extends Route {
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
resetController(controller: SyncDestinationSecretsController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.set('page', undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import type StoreService from 'vault/services/store';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type { ModelFrom } from 'vault/vault/route';
|
||||
import type SyncDestinationModel from 'vault/vault/models/sync/destination';
|
||||
import type Controller from '@ember/controller';
|
||||
|
||||
interface SyncSecretsDestinationsIndexRouteParams {
|
||||
name: string;
|
||||
@ -18,6 +19,19 @@ interface SyncSecretsDestinationsIndexRouteParams {
|
||||
page: string;
|
||||
}
|
||||
|
||||
interface SyncSecretsDestinationsRouteModel {
|
||||
destinations: SyncDestinationModel[];
|
||||
nameFilter: string | undefined;
|
||||
typeFilter: string | undefined;
|
||||
}
|
||||
|
||||
interface SyncSecretsDestinationsController extends Controller {
|
||||
model: SyncSecretsDestinationsRouteModel;
|
||||
page: number | undefined;
|
||||
name: number | undefined;
|
||||
type: number | undefined;
|
||||
}
|
||||
|
||||
export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
@service declare readonly store: StoreService;
|
||||
@service declare readonly router: RouterService;
|
||||
@ -35,7 +49,7 @@ export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
};
|
||||
|
||||
redirect(model: ModelFrom<SyncSecretsDestinationsIndexRoute>) {
|
||||
if (model.destinations.length === 0) {
|
||||
if (!model.destinations.meta.total) {
|
||||
this.router.transitionTo('vault.cluster.sync.secrets.overview');
|
||||
}
|
||||
}
|
||||
@ -43,7 +57,7 @@ export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
filterData(dataset: Array<SyncDestinationModel>, name: string, type: string): Array<SyncDestinationModel> {
|
||||
let filteredDataset = dataset;
|
||||
const filter = (key: keyof SyncDestinationModel, value: string) => {
|
||||
return dataset.filter((model) => {
|
||||
return filteredDataset.filter((model) => {
|
||||
return model[key].toLowerCase().includes(value.toLowerCase());
|
||||
});
|
||||
};
|
||||
@ -68,4 +82,14 @@ export default class SyncSecretsDestinationsIndexRoute extends Route {
|
||||
typeFilter: params.type,
|
||||
});
|
||||
}
|
||||
|
||||
resetController(controller: SyncSecretsDestinationsController, isExiting: boolean) {
|
||||
if (isExiting) {
|
||||
controller.setProperties({
|
||||
page: undefined,
|
||||
name: undefined,
|
||||
type: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import syncScenario from 'vault/mirage/scenarios/sync';
|
||||
import syncHandlers from 'vault/mirage/handlers/sync';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { click, visit } from '@ember/test-helpers';
|
||||
import { click, visit, fillIn } from '@ember/test-helpers';
|
||||
import { PAGE } from 'vault/tests/helpers/sync/sync-selectors';
|
||||
|
||||
const { searchSelect, filter, listItem } = PAGE;
|
||||
@ -29,6 +29,11 @@ module('Acceptance | sync | destinations', function (hooks) {
|
||||
assert.dom(listItem).exists({ count: 6 }, 'All destinations render');
|
||||
await click(`${filter('type')} .ember-basic-dropdown-trigger`);
|
||||
await click(searchSelect.option());
|
||||
assert.dom(listItem).exists({ count: 2 }, 'Filtered destinations render');
|
||||
assert.dom(listItem).exists({ count: 2 }, 'Destinations are filtered by type');
|
||||
await fillIn(filter('name'), 'new');
|
||||
assert.dom(listItem).exists({ count: 1 }, 'Destinations are filtered by type and name');
|
||||
await click(searchSelect.removeSelected);
|
||||
await fillIn(filter('name'), 'gcp');
|
||||
assert.dom(listItem).exists({ count: 1 }, 'Destinations are filtered by name');
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,6 +14,7 @@ export const SELECTORS = {
|
||||
icon: (name) => `[data-test-icon="${name}"]`,
|
||||
tab: (name) => `[data-test-tab="${name}"]`,
|
||||
filter: (name) => `[data-test-filter="${name}"]`,
|
||||
filterInput: '[data-test-filter-input]',
|
||||
confirmModalInput: '[data-test-confirmation-modal-input]',
|
||||
confirmButton: '[data-test-confirm-button]',
|
||||
emptyStateTitle: '[data-test-empty-state-title]',
|
||||
|
||||
@ -7,7 +7,7 @@ import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { render, click } from '@ember/test-helpers';
|
||||
import { render, click, fillIn } from '@ember/test-helpers';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs';
|
||||
import sinon from 'sinon';
|
||||
@ -97,8 +97,7 @@ module('Integration | Component | sync | Page::Destinations', function (hooks) {
|
||||
);
|
||||
|
||||
// NAME FILTER
|
||||
await click(`${filter('name')} .ember-basic-dropdown-trigger`);
|
||||
await click(searchSelect.option(searchSelect.optionIndex('destination-aws')));
|
||||
await fillIn(filter('name'), 'destination-aws');
|
||||
assert.deepEqual(
|
||||
this.transitionStub.lastCall.args,
|
||||
['vault.cluster.sync.secrets.destinations', { queryParams: { name: 'destination-aws' } }],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user