From 3153673894781bdffb8562435dcc6ac8427d7205 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Fri, 5 Jan 2024 16:41:57 -0700 Subject: [PATCH] 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 --- ui/app/styles/helper-classes/typography.scss | 3 ++ .../components/search-select-placeholder.hbs | 2 +- .../components/secrets/page/destinations.hbs | 23 +++++++------- .../components/secrets/page/destinations.ts | 31 +++++++++++++++---- .../page/destinations/create-and-edit.ts | 2 +- .../destinations/destination/secrets.ts | 20 +++++++++++- .../routes/secrets/destinations/index.ts | 28 +++++++++++++++-- .../sync/secrets/destinations-test.js | 9 ++++-- ui/tests/helpers/general-selectors.js | 1 + .../sync/secrets/page/destinations-test.js | 5 ++- 10 files changed, 96 insertions(+), 28 deletions(-) diff --git a/ui/app/styles/helper-classes/typography.scss b/ui/app/styles/helper-classes/typography.scss index 4cde9b2d3c..8466b0c294 100644 --- a/ui/app/styles/helper-classes/typography.scss +++ b/ui/app/styles/helper-classes/typography.scss @@ -110,6 +110,9 @@ } } +.opacity-050 { + opacity: 0.5; +} .opacity-060 { opacity: 0.6; } diff --git a/ui/lib/core/addon/components/search-select-placeholder.hbs b/ui/lib/core/addon/components/search-select-placeholder.hbs index b168b1c9ab..32b9adeea1 100644 --- a/ui/lib/core/addon/components/search-select-placeholder.hbs +++ b/ui/lib/core/addon/components/search-select-placeholder.hbs @@ -6,7 +6,7 @@

- {{or @placeholder "Search"}} + {{or @placeholder "Search"}}

diff --git a/ui/lib/sync/addon/components/secrets/page/destinations.hbs b/ui/lib/sync/addon/components/secrets/page/destinations.hbs index 40eba8842c..a1cf7ecbea 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations.hbs +++ b/ui/lib/sync/addon/components/secrets/page/destinations.hbs @@ -28,18 +28,17 @@ class="is-marginless" data-test-filter="type" /> - +
+ +
diff --git a/ui/lib/sync/addon/components/secrets/page/destinations.ts b/ui/lib/sync/addon/components/secrets/page/destinations.ts index d4aa6c08b5..77ac790f06 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations.ts +++ b/ui/lib/sync/addon/components/secrets/page/destinations.ts @@ -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; @@ -28,15 +30,31 @@ export default class SyncSecretsDestinationsPageComponent extends Component 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) { + 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 }, }); } diff --git a/ui/lib/sync/addon/components/secrets/page/destinations/create-and-edit.ts b/ui/lib/sync/addon/components/secrets/page/destinations/create-and-edit.ts index 04e9331bcf..6ba75cfe0e 100644 --- a/ui/lib/sync/addon/components/secrets/page/destinations/create-and-edit.ts +++ b/ui/lib/sync/addon/components/secrets/page/destinations/create-and-edit.ts @@ -59,7 +59,7 @@ export default class DestinationsCreateForm extends Component { @waitFor *save(event: Event) { event.preventDefault(); - + this.error = ''; // clear out validation warnings this.modelValidations = null; const { destination } = this.args; diff --git a/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts b/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts index ddf1ca5dd5..9d24413acd 100644 --- a/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts +++ b/ui/lib/sync/addon/routes/secrets/destinations/destination/secrets.ts @@ -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); + } + } } diff --git a/ui/lib/sync/addon/routes/secrets/destinations/index.ts b/ui/lib/sync/addon/routes/secrets/destinations/index.ts index 132417095f..ae8d37cbcc 100644 --- a/ui/lib/sync/addon/routes/secrets/destinations/index.ts +++ b/ui/lib/sync/addon/routes/secrets/destinations/index.ts @@ -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) { - 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, name: string, type: string): Array { 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, + }); + } + } } diff --git a/ui/tests/acceptance/sync/secrets/destinations-test.js b/ui/tests/acceptance/sync/secrets/destinations-test.js index 017460621f..715d8ab1f5 100644 --- a/ui/tests/acceptance/sync/secrets/destinations-test.js +++ b/ui/tests/acceptance/sync/secrets/destinations-test.js @@ -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'); }); }); diff --git a/ui/tests/helpers/general-selectors.js b/ui/tests/helpers/general-selectors.js index 670fda1f4c..36ee70c5f9 100644 --- a/ui/tests/helpers/general-selectors.js +++ b/ui/tests/helpers/general-selectors.js @@ -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]', diff --git a/ui/tests/integration/components/sync/secrets/page/destinations-test.js b/ui/tests/integration/components/sync/secrets/page/destinations-test.js index 4366bb379b..66bf310a79 100644 --- a/ui/tests/integration/components/sync/secrets/page/destinations-test.js +++ b/ui/tests/integration/components/sync/secrets/page/destinations-test.js @@ -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' } }],