From eae5e114ba4d8346cfee9cb6644094fda9998ae2 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 25 Oct 2019 13:16:45 -0500 Subject: [PATCH] UI - replication path filtering (#7620) * rename mount-filter-config models, components, serializer, adapters to path-filter-config * move search-select component to core addon * add js class for search-select-placeholder and sort out power-select deps for moving to the core component * expose oninput from powerselect through search-select * don't fetch mounts in the replication routes * remove toggle from add template * start cross-namespace fetching * group options and set up for namespace fetch via power-select search prop * add and style up radio-card CSS component * add xlm size for icons between l and xl * copy defaults so they're not getting mutated * finalize cross-namespace fetching and getting that to work with power-select * when passing options but no models, format the options in search select so that they render properly in the list * tint the background of a selected radio card * default to null mode and uniq options in search-select * finish styling radio-card * format inputValues when first rendering the component if options are being passed from outside * treat mode:null as deleting existing config which simplifies save logic * correctly prune the auto complete list since path-filter-config-list handles all of that and finish styling * remove old component * add search debounce and fix linting * update search-select docs * updating tests * support grouped options for when to show the create prompt * update and add tests for path-filter-config-list * fix tests for search-select and path-filter-config-list * the new api uses allow/deny instead of whitelist/blacklist --- ...filter-config.js => path-filter-config.js} | 2 +- ...filter-config.js => path-filter-config.js} | 4 +- ...filter-config.js => path-filter-config.js} | 0 ui/app/styles/components/hs-icon.scss | 4 + ui/app/styles/components/radio-card.scss | 90 ++++++++++++ ui/app/styles/components/search-select.scss | 2 +- ui/app/styles/core.scss | 1 + ui/lib/core/addon/components/icon.js | 4 +- .../components/search-select-placeholder.js | 6 + .../core/addon}/components/search-select.js | 49 ++++--- .../components/search-select-placeholder.hbs | 0 .../templates/components/search-select.hbs | 3 +- .../components/search-select-placeholder.js | 1 + ui/lib/core/app/components/search-select.js | 1 + ui/lib/core/package.json | 1 + ui/lib/core/stories/icon.md | 2 + ui/{ => lib/core}/stories/search-select.md | 9 +- ui/lib/core/stories/search-select.stories.js | 41 ++++++ .../components/mount-filter-config-list.js | 27 ---- .../components/path-filter-config-list.js | 138 ++++++++++++++++++ .../addon/controllers/application.js | 28 ++-- .../mode/secondaries/config-edit.js | 14 +- .../addon/routes/mode/secondaries/add.js | 13 +- .../routes/mode/secondaries/config-create.js | 9 +- .../routes/mode/secondaries/config-edit.js | 3 +- .../routes/mode/secondaries/config-show.js | 2 +- .../addon/routes/replication-base.js | 10 -- .../components/mount-filter-config-list.hbs | 79 ---------- .../components/path-filter-config-list.hbs | 126 ++++++++++++++++ .../addon/templates/mode/secondaries/add.hbs | 25 +--- .../mode/secondaries/config-create.hbs | 6 +- .../mode/secondaries/config-edit.hbs | 26 +--- ui/public/file-error.svg | 3 + ui/public/file-success.svg | 3 + ui/stories/search-select.stories.js | 34 ----- .../mount-filter-config-list-test.js | 38 ----- .../path-filter-config-list-test.js | 116 +++++++++++++++ 37 files changed, 620 insertions(+), 300 deletions(-) rename ui/app/adapters/{mount-filter-config.js => path-filter-config.js} (87%) rename ui/app/models/{mount-filter-config.js => path-filter-config.js} (73%) rename ui/app/serializers/{mount-filter-config.js => path-filter-config.js} (100%) create mode 100644 ui/app/styles/components/radio-card.scss create mode 100644 ui/lib/core/addon/components/search-select-placeholder.js rename ui/{app => lib/core/addon}/components/search-select.js (75%) rename ui/{app => lib/core/addon}/templates/components/search-select-placeholder.hbs (100%) rename ui/{app => lib/core/addon}/templates/components/search-select.hbs (93%) create mode 100644 ui/lib/core/app/components/search-select-placeholder.js create mode 100644 ui/lib/core/app/components/search-select.js rename ui/{ => lib/core}/stories/search-select.md (62%) create mode 100644 ui/lib/core/stories/search-select.stories.js delete mode 100644 ui/lib/replication/addon/components/mount-filter-config-list.js create mode 100644 ui/lib/replication/addon/components/path-filter-config-list.js delete mode 100644 ui/lib/replication/addon/templates/components/mount-filter-config-list.hbs create mode 100644 ui/lib/replication/addon/templates/components/path-filter-config-list.hbs create mode 100644 ui/public/file-error.svg create mode 100644 ui/public/file-success.svg delete mode 100644 ui/stories/search-select.stories.js delete mode 100644 ui/tests/integration/components/mount-filter-config-list-test.js create mode 100644 ui/tests/integration/components/path-filter-config-list-test.js diff --git a/ui/app/adapters/mount-filter-config.js b/ui/app/adapters/path-filter-config.js similarity index 87% rename from ui/app/adapters/mount-filter-config.js rename to ui/app/adapters/path-filter-config.js index 597eedb0d..d46739715 100644 --- a/ui/app/adapters/mount-filter-config.js +++ b/ui/app/adapters/path-filter-config.js @@ -2,7 +2,7 @@ import ApplicationAdapter from './application'; export default ApplicationAdapter.extend({ url(id) { - return `${this.buildURL()}/replication/performance/primary/mount-filter/${id}`; + return `${this.buildURL()}/replication/performance/primary/paths-filter/${id}`; }, findRecord(store, type, id) { diff --git a/ui/app/models/mount-filter-config.js b/ui/app/models/path-filter-config.js similarity index 73% rename from ui/app/models/mount-filter-config.js rename to ui/app/models/path-filter-config.js index b0234a749..893c6c678 100644 --- a/ui/app/models/mount-filter-config.js +++ b/ui/app/models/path-filter-config.js @@ -2,9 +2,7 @@ import DS from 'ember-data'; const { attr } = DS; export default DS.Model.extend({ - mode: attr('string', { - defaultValue: 'whitelist', - }), + mode: attr('string'), paths: attr('array', { defaultValue: function() { return []; diff --git a/ui/app/serializers/mount-filter-config.js b/ui/app/serializers/path-filter-config.js similarity index 100% rename from ui/app/serializers/mount-filter-config.js rename to ui/app/serializers/path-filter-config.js diff --git a/ui/app/styles/components/hs-icon.scss b/ui/app/styles/components/hs-icon.scss index 904fd9d2b..80507831e 100644 --- a/ui/app/styles/components/hs-icon.scss +++ b/ui/app/styles/components/hs-icon.scss @@ -30,6 +30,10 @@ height: 20px; } +.hs-icon-xlm { + width: 24px; + height: 24px; +} .hs-icon-xl { width: 28px; height: 28px; diff --git a/ui/app/styles/components/radio-card.scss b/ui/app/styles/components/radio-card.scss new file mode 100644 index 000000000..d48932ae4 --- /dev/null +++ b/ui/app/styles/components/radio-card.scss @@ -0,0 +1,90 @@ +.radio-card-selector { + display: flex; + margin-bottom: $spacing-xs; +} +.radio-card { + width: 19rem; + box-shadow: $box-shadow-low; + display: flex; + flex-direction: column; + justify-content: space-between; + margin: $spacing-xs $spacing-m; + border: $base-border; + border-radius: $radius; + transition: all ease-in-out $speed; + + input[type='radio'] { + position: absolute; + z-index: 1; + opacity: 0; + } + + input[type='radio'] + label { + border: 1px solid $grey-light; + border-radius: 50%; + cursor: pointer; + display: block; + height: 1rem; + width: 1rem; + flex-shrink: 0; + flex-grow: 0; + } + + input[type='radio']:checked + label { + background: $blue; + border: 1px solid $blue; + box-shadow: inset 0 0 0 0.15rem $white; + } + input[type='radio']:focus + label { + box-shadow: 0 0 10px 1px rgba($blue, 0.4), inset 0 0 0 0.15rem $white; + } +} +.radio-card:first-child { + margin-left: 0; +} +.radio-card:last-child { + margin-right: 0; +} + +.radio-card-row { + display: flex; + padding: $spacing-m; +} +.radio-card-icon { + color: $ui-gray-300; +} +.radio-card-message { + margin: $spacing-xxs; +} + +.radio-card-message-title { + font-weight: $font-weight-semibold; + font-size: $size-7; + margin-bottom: $spacing-xxs; +} +.radio-card-message-body { + line-height: 1.2; + color: $ui-gray-500; + font-size: $size-8; +} + +.radio-card-radio-row { + display: flex; + justify-content: center; + background: $ui-gray-050; + padding: $spacing-xs; +} + +.is-selected { + &.radio-card { + border-color: $blue-500; + background: $ui-gray-010; + box-shadow: $box-shadow-middle; + } + .radio-card-icon { + color: $black; + } + .radio-card-radio-row { + background: $blue-050; + } +} diff --git a/ui/app/styles/components/search-select.scss b/ui/app/styles/components/search-select.scss index cd764ae96..027cea6a4 100644 --- a/ui/app/styles/components/search-select.scss +++ b/ui/app/styles/components/search-select.scss @@ -56,7 +56,7 @@ padding-left: $spacing-xxs + $spacing-l; } -.ember-power-select-options { +div > .ember-power-select-options { background: $white; border: $base-border; box-shadow: $box-shadow-middle; diff --git a/ui/app/styles/core.scss b/ui/app/styles/core.scss index e1402d54c..87706441b 100644 --- a/ui/app/styles/core.scss +++ b/ui/app/styles/core.scss @@ -72,6 +72,7 @@ @import './components/navigate-input'; @import './components/page-header'; @import './components/popup-menu'; +@import './components/radio-card'; @import './components/radial-progress'; @import './components/raft-join'; @import './components/role-item'; diff --git a/ui/lib/core/addon/components/icon.js b/ui/lib/core/addon/components/icon.js index c1f4c47bd..eae63b0cc 100644 --- a/ui/lib/core/addon/components/icon.js +++ b/ui/lib/core/addon/components/icon.js @@ -7,7 +7,7 @@ * * ``` * @param glyph=null {String} - The name of the SVG to render inline. - * @param [size='m'] {String} - The size of the Icon, can be one of 's', 'm', 'l', 'xl', 'xxl'. The default is 'm'. + * @param [size='m'] {String} - The size of the Icon, can be one of 's', 'm', 'l', 'xlm', 'xl', 'xxl'. The default is 'm'. * */ import Component from '@ember/component'; @@ -15,7 +15,7 @@ import { computed } from '@ember/object'; import { assert } from '@ember/debug'; import layout from '../templates/components/icon'; -const SIZES = ['s', 'm', 'l', 'xl', 'xxl']; +const SIZES = ['s', 'm', 'l', 'xlm', 'xl', 'xxl']; export default Component.extend({ tagName: '', diff --git a/ui/lib/core/addon/components/search-select-placeholder.js b/ui/lib/core/addon/components/search-select-placeholder.js new file mode 100644 index 000000000..cf5e49baf --- /dev/null +++ b/ui/lib/core/addon/components/search-select-placeholder.js @@ -0,0 +1,6 @@ +import Component from '@ember/component'; +import layout from '../templates/components/search-select-placeholder'; + +export default Component.extend({ + layout, +}); diff --git a/ui/app/components/search-select.js b/ui/lib/core/addon/components/search-select.js similarity index 75% rename from ui/app/components/search-select.js rename to ui/lib/core/addon/components/search-select.js index d1c626bf4..bc710c5d2 100644 --- a/ui/app/components/search-select.js +++ b/ui/lib/core/addon/components/search-select.js @@ -3,6 +3,7 @@ import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import { computed } from '@ember/object'; import { singularize } from 'ember-inflector'; +import layout from '../templates/components/search-select'; /** * @module SearchSelect @@ -13,34 +14,25 @@ import { singularize } from 'ember-inflector'; * @param id {String} - The name of the form field * @param models {String} - An array of model types to fetch from the API. * @param onChange {Func} - The onchange action for this form field. - * @param inputValue {String} - A comma-separated string or an array of strings. + * @param inputValue {String | Array} - A comma-separated string or an array of strings. * @param [helpText] {String} - Text to be displayed in the info tooltip for this form field * @param label {String} - Label for this form field * @param fallbackComponent {String} - name of component to be rendered if the API call 403s * + * @param options {Array} - *Advanced usage* - `options` can be passed directly from the outside to the + * power-select component. If doing this, `models` should not also be passed as that will overwrite the + * passed value. + * @param search {Func} - *Advanced usage* - Customizes how the power-select component searches for matches - + * see the power-select docs for more information. + * */ export default Component.extend({ + layout, 'data-test-component': 'search-select', classNames: ['field', 'search-select'], store: service(), - /* - * @public - * @param Function - * - * Function called when any of the inputs change - * accepts a single param `value` - * - */ onChange: () => {}, - - /* - * @public - * @param String | Array - * A comma-separated string or an array of strings. - * Defaults to an empty array. - * - */ inputValue: computed(function() { return []; }), @@ -52,6 +44,16 @@ export default Component.extend({ this._super(...arguments); this.set('selectedOptions', this.inputValue || []); }, + didRender() { + this._super(...arguments); + let { oldOptions, options, selectedOptions } = this; + let hasFormattedInput = typeof selectedOptions.firstObject !== 'string'; + if (options && !oldOptions && !hasFormattedInput) { + // this is the first time they've been set, so we need to format them + this.formatOptions(options); + } + this.set('oldOptions', options); + }, formatOptions: function(options) { options = options.toArray().map(option => { option.searchText = `${option.name} ${option.id}`; @@ -68,11 +70,17 @@ export default Component.extend({ }); this.set('selectedOptions', formattedOptions); if (this.options) { - options = this.options.concat(options); + options = this.options.concat(options).uniq(); } this.set('options', options); }, fetchOptions: task(function*() { + if (!this.models) { + if (this.options) { + this.formatOptions(this.options); + } + return; + } for (let modelType of this.models) { if (modelType.includes('identity')) { this.set('shouldRenderName', true); @@ -128,7 +136,10 @@ export default Component.extend({ constructSuggestion(id) { return `Add new ${singularize(this.label)}: ${id}`; }, - hideCreateOptionOnSameID(id) { + hideCreateOptionOnSameID(id, options) { + if (options && options.length && options.firstObject.groupName) { + return !options.some(group => group.options.findBy('id', id)); + } let existingOption = this.options && (this.options.findBy('id', id) || this.options.findBy('name', id)); return !existingOption; }, diff --git a/ui/app/templates/components/search-select-placeholder.hbs b/ui/lib/core/addon/templates/components/search-select-placeholder.hbs similarity index 100% rename from ui/app/templates/components/search-select-placeholder.hbs rename to ui/lib/core/addon/templates/components/search-select-placeholder.hbs diff --git a/ui/app/templates/components/search-select.hbs b/ui/lib/core/addon/templates/components/search-select.hbs similarity index 93% rename from ui/app/templates/components/search-select.hbs rename to ui/lib/core/addon/templates/components/search-select.hbs index 848910a17..a38c05f5d 100644 --- a/ui/app/templates/components/search-select.hbs +++ b/ui/lib/core/addon/templates/components/search-select.hbs @@ -7,7 +7,7 @@ helpText=helpText }} {{else}} -