From 1eef763966cb417403028aab0d561126080ee672 Mon Sep 17 00:00:00 2001 From: Lukas Holzer Date: Wed, 5 Feb 2020 14:32:19 +0100 Subject: [PATCH] feat(quick-filter): Add a new quick filter component inside an experimental package. The quick filter component is used to provide a quick way to operate with the filter field inside a sidebar. Inside the quick filter only an autocomplete with simple options can be displayed. This is part of an experimental package and not meant to be used inside production. The experimental package does not follow semantic versioning like the rest of the library does. This means that we might break the api in every version. It is only meant to be used for testing and feedback purpose. Fixes #453 Fixes #254 --- README.md | 2 +- angular.json | 40 +++ .../src/app/app.routing.module.ts | 7 + .../filter-field/filter-field.e2e.ts | 14 +- .../filter-field/filter-field.po.ts | 13 +- .../components/filter-field/filter-field.ts | 92 +------ .../quick-filter-initial-data.html | 8 + .../quick-filter-initial-data.ts | 40 +++ .../quick-filter/quick-filter.e2e.ts | 158 +++++++++++ .../quick-filter/quick-filter.module.ts | 35 +++ .../quick-filter/quick-filter.po.ts | 44 +++ .../quick-filter/quick-filter.html | 20 ++ .../quick-filter/quick-filter/quick-filter.ts | 50 ++++ .../src/components/quick-filter/util.ts | 0 apps/universal/src/app/barista.module.ts | 60 +++-- .../src/app/kitchen-sink/kitchen-sink.html | 9 + .../src/app/kitchen-sink/kitchen-sink.ts | 123 ++------- .../drawer/src/drawer-container.scss | 1 - .../experimental/quick-filter/README.md | 51 ++++ .../experimental/quick-filter/barista.json | 22 ++ .../experimental/quick-filter/index.ts | 20 ++ .../experimental/quick-filter/jest.config.js | 11 + .../experimental/quick-filter/package.json | 7 + .../src/quick-filter-data-source.ts | 96 +++++++ .../src/quick-filter-default-data-source.ts | 253 ++++++++++++++++++ .../quick-filter/src/quick-filter-group.html | 40 +++ .../quick-filter/src/quick-filter-group.scss | 21 ++ .../quick-filter/src/quick-filter-group.ts | 145 ++++++++++ .../quick-filter/src/quick-filter-utils.ts | 26 ++ .../quick-filter/src/quick-filter.html | 43 +++ .../quick-filter/src/quick-filter.module.ts | 43 +++ .../quick-filter/src/quick-filter.scss | 81 ++++++ .../quick-filter/src/quick-filter.spec.ts | 221 +++++++++++++++ .../quick-filter/src/quick-filter.ts | 181 +++++++++++++ .../quick-filter/src/state/actions.ts | 70 +++++ .../quick-filter/src/state/effects.ts | 44 +++ .../quick-filter/src/state/reducer.spec.ts | 30 +++ .../quick-filter/src/state/reducer.ts | 161 +++++++++++ .../quick-filter/src/state/selectors.ts | 55 ++++ .../quick-filter/src/state/store.ts | 106 ++++++++ .../quick-filter/src/test-setup.ts | 18 ++ .../experimental/quick-filter/tsconfig.json | 7 + .../quick-filter/tsconfig.lib.json | 20 ++ .../quick-filter/tsconfig.spec.json | 10 + .../experimental/quick-filter/tslint.json | 4 + libs/barista-components/filter-field/index.ts | 3 +- .../filter-field/src/filter-field-util.ts | 15 +- .../filter-field/src/filter-field.spec.ts | 94 ++----- .../filter-field/src/filter-field.ts | 2 +- .../barista-components/style/font-styles.scss | 1 - libs/examples/src/examples.module.ts | 2 + libs/examples/src/index.ts | 4 + libs/examples/src/quick-filter/index.ts | 18 ++ .../quick-filter-default-example.html | 13 + .../quick-filter-default-example.ts | 41 +++ .../quick-filter-examples.module.ts | 28 ++ libs/testing/fixtures/src/index.ts | 3 + .../lib/filter-field/test-data-validators.ts | 61 +++++ .../src/lib/filter-field/test-data.ts | 112 ++++++++ .../fixtures/src/lib/tree-table/test-data.ts | 121 +++++++++ nx.json | 3 + tsconfig.json | 3 + tslint.json | 6 +- 63 files changed, 2715 insertions(+), 317 deletions(-) create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.module.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter.po.ts create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html create mode 100644 apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts create mode 100644 apps/components-e2e/src/components/quick-filter/util.ts create mode 100644 libs/barista-components/experimental/quick-filter/README.md create mode 100644 libs/barista-components/experimental/quick-filter/barista.json create mode 100644 libs/barista-components/experimental/quick-filter/index.ts create mode 100644 libs/barista-components/experimental/quick-filter/jest.config.js create mode 100644 libs/barista-components/experimental/quick-filter/package.json create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.html create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.html create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.scss create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/quick-filter.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/actions.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/effects.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/reducer.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/selectors.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/state/store.ts create mode 100644 libs/barista-components/experimental/quick-filter/src/test-setup.ts create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.json create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.lib.json create mode 100644 libs/barista-components/experimental/quick-filter/tsconfig.spec.json create mode 100644 libs/barista-components/experimental/quick-filter/tslint.json create mode 100644 libs/examples/src/quick-filter/index.ts create mode 100644 libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.html create mode 100644 libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.ts create mode 100644 libs/examples/src/quick-filter/quick-filter-examples.module.ts create mode 100644 libs/testing/fixtures/src/lib/filter-field/test-data-validators.ts create mode 100644 libs/testing/fixtures/src/lib/filter-field/test-data.ts create mode 100644 libs/testing/fixtures/src/lib/tree-table/test-data.ts diff --git a/README.md b/README.md index 3a7c783099..b50e902610 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [Barista](https://barista.dynatrace.com) is the name of [Dynatrace](https://www.dynatrace.com)'s design system, that contains all ingredients to create extraordinary user experiences. Like a barista picks the -best ingredients for their coffee – and we love coffee! +best ingredients for their coffee – and we love coffee! One major part of the Barista design system is our [components library](https://github.com/dynatrace-oss/barista/tree/master/components) diff --git a/angular.json b/angular.json index 017191e639..e1f2b12ac8 100644 --- a/angular.json +++ b/angular.json @@ -2647,6 +2647,46 @@ }, "schematics": {} }, + "quick-filter": { + "projectType": "library", + "root": "libs/barista-components/experimental/quick-filter", + "sourceRoot": "libs/barista-components/experimental/quick-filter/src", + "prefix": "dt", + "architect": { + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "libs/barista-components/experimental/quick-filter/tsconfig.lib.json", + "libs/barista-components/experimental/quick-filter/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**", + "!libs/barista-components/experimental/quick-filter/**" + ] + } + }, + "lint-styles": { + "builder": "@dynatrace/barista-builders:stylelint", + "options": { + "stylelintConfig": ".stylelintrc", + "reportFile": "dist/stylelint/report.xml", + "exclude": ["**/node_modules/**"], + "files": [ + "libs/barista-components/experimental/quick-filter/**/*.scss" + ] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "options": { + "jestConfig": "libs/barista-components/experimental/quick-filter/jest.config.js", + "tsConfig": "libs/barista-components/experimental/quick-filter/tsconfig.spec.json", + "setupFile": "libs/barista-components/experimental/quick-filter/src/test-setup.ts" + } + } + } + }, "radial-chart": { "projectType": "library", "root": "libs/barista-components/radial-chart", diff --git a/apps/components-e2e/src/app/app.routing.module.ts b/apps/components-e2e/src/app/app.routing.module.ts index c07b84634f..e4ffb37bef 100644 --- a/apps/components-e2e/src/app/app.routing.module.ts +++ b/apps/components-e2e/src/app/app.routing.module.ts @@ -138,6 +138,13 @@ export const routes: Routes = [ module => module.DtE2EProgressBarModule, ), }, + { + path: 'quick-filter', + loadChildren: () => + import('../components/quick-filter/quick-filter.module').then( + module => module.DtE2EQuickFilterModule, + ), + }, { path: 'radial-chart', loadChildren: () => diff --git a/apps/components-e2e/src/components/filter-field/filter-field.e2e.ts b/apps/components-e2e/src/components/filter-field/filter-field.e2e.ts index 111bba4e17..af14b71697 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.e2e.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.e2e.ts @@ -48,7 +48,7 @@ test('should not show a error box if there is no validator provided', async () = test('should show a error box if does not meet the validation function', async () => { await clickOption(3) - .typeText(input, 'a') + .typeText(input, 'a', { speed: 0.1 }) // Wait for the filter field to refresh the error message. .wait(250) .expect(errorBox.exists) @@ -57,12 +57,10 @@ test('should show a error box if does not meet the validation function', async ( .match(/min 3 characters/gm); }); -// TODO: lukas.holzer investigate why this test is flaky on Browserstack -// tslint:disable-next-line: dt-no-focused-tests -test.skip('should show is required error when the input is dirty', async () => { +test('should show is required error when the input is dirty', async () => { await clickOption(3) - .typeText(input, 'a') - .pressKey('backspace') + .typeText(input, 'a', { speed: 0.1 }) + .pressKey('backspace', { speed: 0.1 }) .expect(errorBox.exists) .ok() .expect(errorBox.innerText) @@ -174,7 +172,7 @@ test('should choose a freetext node with the mouse and submit the correct value // Select the free text node and start typing await clickOption(4) - // Wait for a certain amout fo time to let the filterfield refresh + // Wait for a certain amount fo time to let the filterfield refresh .wait(250) // Send the correct value into the input field .typeText(input, 'Custom selection'); @@ -191,7 +189,7 @@ test('should choose a freetext node with the mouse and submit the correct value .expect(tags.length) .eql(1) .expect(tags[0]) - .eql('Autocomplete with free text optionsCustom selection'); + .match(/Autocomplete with free text options/); }); test('should choose a freetext node with the mouse and submit an empty value immediately', async (testController: TestController) => { diff --git a/apps/components-e2e/src/components/filter-field/filter-field.po.ts b/apps/components-e2e/src/components/filter-field/filter-field.po.ts index 86b51fd40c..787d3209f3 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.po.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.po.ts @@ -24,6 +24,12 @@ export const filterTags = Selector('dt-filter-field-tag'); export const tagOverlay = Selector('.dt-overlay-container'); export const filterFieldRangePanel = Selector('.dt-filter-field-range-panel'); +/** Selector for the delete button (x) on a filter with a specific text */ +export const tagDeleteButton = (filterText: string) => + Selector('dt-filter-field-tag') + .withText(filterText) + .child('.dt-filter-field-tag-button'); + export const input = Selector('input'); export const switchToFirstDatasource = Selector('#switchToFirstDatasource'); @@ -36,9 +42,8 @@ export function clickOption( const controller = testController || t; return controller - .click(filterField, { speed: 0.2 }) - .wait(250) - .click(option(nth), { speed: 0.2 }); + .click(filterField, { speed: 0.4 }) + .click(option(nth), { speed: 0.4 }); } /** Focus the input of the filter field to send key events to it. */ @@ -46,7 +51,7 @@ export const focusFilterFieldInput = ClientFunction(() => { (document.querySelector('#filter-field input') as HTMLElement).focus(); }); -/** Retreive all set tags in the filter field and their values. */ +/** Retrieve all set tags in the filter field and their values. */ export const getFilterfieldTags = ClientFunction(() => { const filterFieldTags: HTMLElement[] = [].slice.call( document.querySelectorAll('.dt-filter-field-tag'), diff --git a/apps/components-e2e/src/components/filter-field/filter-field.ts b/apps/components-e2e/src/components/filter-field/filter-field.ts index c9d4020fe5..2f16e43335 100644 --- a/apps/components-e2e/src/components/filter-field/filter-field.ts +++ b/apps/components-e2e/src/components/filter-field/filter-field.ts @@ -18,98 +18,24 @@ // tslint:disable no-any max-file-line-count no-unbound-method use-component-selector import { - Component, - ViewChild, ChangeDetectorRef, + Component, OnDestroy, + ViewChild, } from '@angular/core'; -import { Validators } from '@angular/forms'; - import { + DtFilterField, DtFilterFieldDefaultDataSource, DtFilterFieldDefaultDataSourceType, - DtFilterField, } from '@dynatrace/barista-components/filter-field'; -import { takeUntil } from 'rxjs/operators'; +import { + FILTER_FIELD_TEST_DATA, + FILTER_FIELD_TEST_DATA_VALIDATORS, +} from '@dynatrace/testing/fixtures'; import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; -const TEST_DATA = { - autocomplete: [ - { - name: 'custom normal', - suggestions: [], - }, - { - name: 'custom required', - suggestions: [], - validators: [ - { validatorFn: Validators.required, error: 'field is required' }, - ], - }, - { - name: 'custom with multiple', - suggestions: [], - validators: [ - { validatorFn: Validators.required, error: 'field is required' }, - { validatorFn: Validators.minLength(3), error: 'min 3 characters' }, - ], - }, - { - name: 'outer-option', - autocomplete: [ - { - name: 'inner-option', - }, - ], - }, - { - name: 'Autocomplete with free text options', - autocomplete: [ - { name: 'Autocomplete option 1' }, - { name: 'Autocomplete option 2' }, - { name: 'Autocomplete option 3' }, - { - name: 'Autocomplete free text', - suggestions: ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'], - validators: [], - }, - ], - }, - ], -}; - -const TEST_DATA_2 = { - autocomplete: [ - { - name: 'AUT', - distinct: true, - autocomplete: [{ name: 'Linz' }, { name: 'Vienna' }, { name: 'Graz' }], - }, - { - name: 'USA', - autocomplete: [ - { name: 'San Francisco' }, - { name: 'Los Angeles' }, - { name: 'New York' }, - { name: 'Custom', suggestions: [] }, - ], - }, - { - name: 'Requests per minute', - range: { - operators: { - range: true, - equal: true, - greaterThanEqual: true, - lessThanEqual: true, - }, - unit: 's', - }, - }, - ], -}; - -const DATA = [TEST_DATA, TEST_DATA_2]; +const DATA = [FILTER_FIELD_TEST_DATA_VALIDATORS, FILTER_FIELD_TEST_DATA]; @Component({ selector: 'dt-e2e-filter-field', diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html new file mode 100644 index 0000000000..64d492a50e --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.html @@ -0,0 +1,8 @@ + + Quick-filter + + All options in the filter field above + + + my content + diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts new file mode 100644 index 0000000000..64f90d7db7 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { DtQuickFilterDefaultDataSource } from '@dynatrace/barista-components/experimental/quick-filter'; +import { FILTER_FIELD_TEST_DATA } from '@dynatrace/testing/fixtures'; + +@Component({ + selector: 'dt-e2e-quick-filter', + templateUrl: 'quick-filter-initial-data.html', +}) +export class DtE2EQuickFilterInitialData { + _dataSource = new DtQuickFilterDefaultDataSource(FILTER_FIELD_TEST_DATA, { + showInSidebar: () => true, + }); + + _initialFilters = [ + [ + FILTER_FIELD_TEST_DATA.autocomplete[0], + FILTER_FIELD_TEST_DATA.autocomplete[0].autocomplete![1], + ], + [ + FILTER_FIELD_TEST_DATA.autocomplete[1], + FILTER_FIELD_TEST_DATA.autocomplete[1].autocomplete![2], + ], + ]; +} diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts new file mode 100644 index 0000000000..e3f09ec6a6 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts @@ -0,0 +1,158 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { waitForAngular } from '../../utils/wait-for-angular'; +import { + clearAll, + getFilterfieldTags, + tagDeleteButton, +} from '../filter-field/filter-field.po'; +import { + getGroupItem, + getGroupItemInput, + getSelectedItem, +} from './quick-filter.po'; +import { resetWindowSizeToDefault } from '../../utils'; + +fixture('Quick Filter') + .page('http://localhost:4200/quick-filter') + .meta({ + 'filter-field': true, + 'quick-filter': true, + drawer: true, + checkbox: true, + radio: true, + }) + .beforeEach(async () => { + await resetWindowSizeToDefault(); + await waitForAngular(); + }); + +test('if nothing is selected the distinct should be set to all', async (testController: TestController) => { + await testController + .expect(getSelectedItem('AUT').textContent) + .match(/Any/) + .expect(getSelectedItem('USA').exists) + .notOk(); +}); + +test('if nothing is selected the filter field should be empty', async (testController: TestController) => { + await testController.expect(getFilterfieldTags()).eql([]); +}); + +test('if distinct option gets updated it should update the filter field', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql([]) + .click(getGroupItem('AUT', 'Linz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['AUTLinz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Linz/) + .click(getGroupItem('AUT', 'Graz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['AUTGraz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Graz/); +}); + +test('if it is possible to select multiple options', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco']) + .click(getGroupItem('USA', 'Los Angeles'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles']) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']); +}); + +test('if it is possible to select and deselect multiple options', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']) + .click(getGroupItem('USA', 'San Francisco'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USALos Angeles', 'USANew York']); +}); + +test('if it is possible to reset all filters via the filter fields clearAll button', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USALos Angeles', 'USANew York']) + .click(clearAll, { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql([]) + .expect(getSelectedItem('USA').exists) + .notOk(); +}); + +test('if it is possible to delete an option via the filter field', async (testController: TestController) => { + await testController + .click(getGroupItem('USA', 'San Francisco')) + .click(getGroupItem('USA', 'Los Angeles')) + .click(getGroupItem('USA', 'New York'), { speed: 0.4 }) + .click(tagDeleteButton('Los Angeles'), { speed: 0.4 }) + .expect(getFilterfieldTags()) + .eql(['USASan Francisco', 'USANew York']) + .expect(getGroupItemInput('USA', 'New York').checked) + .ok() + .expect(getGroupItemInput('USA', 'Los Angeles').checked) + .notOk(); +}); + +fixture('Quick Filter with initial data') + .page('http://localhost:4200/quick-filter/initial-data') + .beforeEach(async () => { + await resetWindowSizeToDefault(); + await waitForAngular(); + }); + +test('if the initial filter in the filter field be reflected in the quick-filter state', async (testController: TestController) => { + await testController + .expect(getSelectedItem('AUT').textContent) + .match(/Vienna/) + .expect(getSelectedItem('USA').textContent) + .match(/New York/); +}); + +test('if the initial filters are reflected in the filter field ', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql(['AUTVienna', 'USANew York']); +}); + +test('when the distinct group get set to the any option, then remove the group from the filter', async (testController: TestController) => { + await testController + .expect(getFilterfieldTags()) + .eql(['AUTVienna', 'USANew York']) + .click(getGroupItem('AUT', 'Any'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['USANew York']) + .click(getGroupItem('AUT', 'Graz'), { speed: 0.3 }) + .expect(getFilterfieldTags()) + .eql(['USANew York', 'AUTGraz']) + .expect(getSelectedItem('AUT').textContent) + .match(/Graz/); +}); diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts new file mode 100644 index 0000000000..9949a31d66 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.module.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { Route, RouterModule } from '@angular/router'; +import { DtQuickFilterModule } from '@dynatrace/barista-components/experimental/quick-filter'; +import { DtE2EQuickFilter } from './quick-filter/quick-filter'; +import { DtE2EQuickFilterInitialData } from './quick-filter-initial-data/quick-filter-initial-data'; + +const routes: Route[] = [ + { path: '', component: DtE2EQuickFilter }, + { path: 'initial-data', component: DtE2EQuickFilterInitialData }, +]; + +@NgModule({ + declarations: [DtE2EQuickFilter, DtE2EQuickFilterInitialData], + imports: [CommonModule, RouterModule.forChild(routes), DtQuickFilterModule], + exports: [], + providers: [], +}) +export class DtE2EQuickFilterModule {} diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts b/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts new file mode 100644 index 0000000000..915bbc6f65 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.po.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Selector } from 'testcafe'; + +export const quickFilterCloseButton = Selector('.dt-quick-filter-close'); +export const quickFilterOpenButton = Selector('.dt-quick-filter-open'); +export const quickFilterGroup = Selector('.dt-quick-filter-group'); + +/** get a group by its name */ +export const getGroup = (group: string) => + quickFilterGroup + .child('.dt-quick-filter-group-headline') + .withText(group) + .sibling(); + +/** get a group item by its group and its name */ +export const getGroupItem = (group: string, item: string) => + getGroup(group) + .child('.dt-quick-filter-group-items > *') + .withText(item); + +/** get the native input of the specified group item */ +export const getGroupItemInput = (group: string, item: string) => + getGroupItem(group, item) + .child('label') + .child('input'); + +/** get the selected item of the group */ +export const getSelectedItem = (groupText: string) => + getGroup(groupText).child('.dt-checkbox-checked, .dt-radio-checked'); diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html new file mode 100644 index 0000000000..b57249fce3 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.html @@ -0,0 +1,20 @@ + + Quick-filter + + All options in the filter field above + + + my content + + + + diff --git a/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts new file mode 100644 index 0000000000..4c1f2be671 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter/quick-filter.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { + DtQuickFilterDefaultDataSource, + DtQuickFilterDefaultDataSourceConfig, + DtQuickFilterChangeEvent, +} from '@dynatrace/barista-components/experimental/quick-filter'; +import { + FILTER_FIELD_TEST_DATA, + FILTER_FIELD_TEST_DATA_VALIDATORS, +} from '@dynatrace/testing/fixtures'; +import { isObject } from 'util'; + +const DATA = [FILTER_FIELD_TEST_DATA_VALIDATORS, FILTER_FIELD_TEST_DATA]; + +const config: DtQuickFilterDefaultDataSourceConfig = { + showInSidebar: node => + isObject(node) && node.name && node.name !== 'Not in Quickfilter', +}; + +@Component({ + selector: 'dt-e2e-quick-filter', + templateUrl: 'quick-filter.html', +}) +export class DtE2EQuickFilter { + _dataSource = new DtQuickFilterDefaultDataSource(DATA[1], config); + + filterChanges(filterEvent: DtQuickFilterChangeEvent): void { + console.log(filterEvent); + } + + switchToDataSource(index: number): void { + this._dataSource = new DtQuickFilterDefaultDataSource(DATA[index], config); + } +} diff --git a/apps/components-e2e/src/components/quick-filter/util.ts b/apps/components-e2e/src/components/quick-filter/util.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/universal/src/app/barista.module.ts b/apps/universal/src/app/barista.module.ts index 51be78c68c..dc937b56f3 100644 --- a/apps/universal/src/app/barista.module.ts +++ b/apps/universal/src/app/barista.module.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; import { DtAlertModule } from '@dynatrace/barista-components/alert'; import { DtAutocompleteModule } from '@dynatrace/barista-components/autocomplete'; -import { DtButtonGroupModule } from '@dynatrace/barista-components/button-group'; import { DtButtonModule } from '@dynatrace/barista-components/button'; +import { DtButtonGroupModule } from '@dynatrace/barista-components/button-group'; import { DtCardModule } from '@dynatrace/barista-components/card'; import { DtCheckboxModule } from '@dynatrace/barista-components/checkbox'; import { DtConsumptionModule } from '@dynatrace/barista-components/consumption'; @@ -28,6 +30,7 @@ import { DtDrawerModule } from '@dynatrace/barista-components/drawer'; import { DtEmptyStateModule } from '@dynatrace/barista-components/empty-state'; import { DtEventChartModule } from '@dynatrace/barista-components/event-chart'; import { DtExpandableTextModule } from '@dynatrace/barista-components/expandable-text'; +import { DtQuickFilterModule } from '@dynatrace/barista-components/experimental/quick-filter'; import { DtFilterFieldModule } from '@dynatrace/barista-components/filter-field'; import { DtHighlightModule } from '@dynatrace/barista-components/highlight'; import { DtIconModule } from '@dynatrace/barista-components/icon'; @@ -52,8 +55,6 @@ import { DtTimelineChartModule } from '@dynatrace/barista-components/timeline-ch import { DtToggleButtonGroupModule } from '@dynatrace/barista-components/toggle-button-group'; import { DtTopBarNavigationModule } from '@dynatrace/barista-components/top-bar-navigation'; import { DtTreeTableModule } from '@dynatrace/barista-components/tree-table'; -import { HttpClientModule } from '@angular/common/http'; -import { NgModule } from '@angular/core'; @NgModule({ imports: [ @@ -63,41 +64,42 @@ import { NgModule } from '@angular/core'; exports: [ DtAlertModule, DtAutocompleteModule, + DtButtonGroupModule, DtButtonModule, - DtCheckboxModule, - DtTableModule, - DtLoadingDistractorModule, - DtTileModule, - DtTagModule, DtCardModule, + DtCheckboxModule, + DtConsumptionModule, + DtContainerBreakpointObserverModule, DtContextDialogModule, - DtDrawerModule, DtCopyToClipboardModule, - DtButtonGroupModule, - DtRadioModule, - DtShowMoreModule, - DtProgressCircleModule, + DtDrawerModule, + DtEmptyStateModule, + DtEventChartModule, + DtExpandableTextModule, + DtFilterFieldModule, + DtHighlightModule, + DtInfoGroupModule, + DtInputModule, + DtLoadingDistractorModule, + DtMenuModule, + DtOverlayModule, DtPaginationModule, - DtSwitchModule, DtProgressBarModule, + DtProgressCircleModule, + DtQuickFilterModule, + DtRadialChartModule, + DtRadioModule, DtSelectModule, - DtInputModule, - DtOverlayModule, - DtTreeTableModule, - DtToggleButtonGroupModule, - DtInfoGroupModule, - DtHighlightModule, - DtConsumptionModule, - DtFilterFieldModule, - DtMenuModule, - DtEmptyStateModule, + DtShowMoreModule, + DtStepperModule, + DtSwitchModule, + DtTableModule, + DtTagModule, + DtTileModule, DtTimelineChartModule, - DtExpandableTextModule, - DtEventChartModule, + DtToggleButtonGroupModule, DtTopBarNavigationModule, - DtStepperModule, - DtContainerBreakpointObserverModule, - DtRadialChartModule, + DtTreeTableModule, ], }) export class BaristaModule {} diff --git a/apps/universal/src/app/kitchen-sink/kitchen-sink.html b/apps/universal/src/app/kitchen-sink/kitchen-sink.html index 66a99f3794..5af6f8c9cb 100644 --- a/apps/universal/src/app/kitchen-sink/kitchen-sink.html +++ b/apps/universal/src/app/kitchen-sink/kitchen-sink.html @@ -355,3 +355,12 @@

Packets

+ + + Quick-filter + + All options in the filter field above + + + my content + diff --git a/apps/universal/src/app/kitchen-sink/kitchen-sink.ts b/apps/universal/src/app/kitchen-sink/kitchen-sink.ts index 42c38bdc25..7cfafae252 100644 --- a/apps/universal/src/app/kitchen-sink/kitchen-sink.ts +++ b/apps/universal/src/app/kitchen-sink/kitchen-sink.ts @@ -14,117 +14,19 @@ * limitations under the License. */ +import { Component } from '@angular/core'; import { DtTreeControl, DtTreeDataSource, DtTreeFlattener, } from '@dynatrace/barista-components/core'; - -import { Component } from '@angular/core'; -import { DtIconType } from '@dynatrace/barista-icons'; - -const TESTDATA: ThreadNode[] = [ - { - name: 'hz.hzInstance_1_cluster.thread', - icon: 'airplane', - threadlevel: 'S0', - totalTimeConsumption: 150, - waiting: 123, - running: 20, - blocked: 0, - children: [ - { - name: - 'hz.hzInstance_1_cluster.thread_1_hz.hzInstance_1_cluster.thread-1', - icon: 'airplane', - threadlevel: 'S1', - totalTimeConsumption: 150, - waiting: 123, - running: 20, - blocked: 0, - }, - { - name: 'hz.hzInstance_1_cluster.thread-2', - icon: 'airplane', - threadlevel: 'S1', - totalTimeConsumption: 150, - waiting: 130, - running: 0, - blocked: 0, - }, - ], - }, - { - name: 'jetty', - icon: 'airplane', - threadlevel: 'S0', - totalTimeConsumption: 150, - waiting: 123, - running: 20, - blocked: 0, - children: [ - { - name: 'jetty-422', - icon: 'airplane', - threadlevel: 'S1', - totalTimeConsumption: 150, - waiting: 123, - running: 20, - blocked: 0, - }, - { - name: 'jetty-423', - icon: 'airplane', - threadlevel: 'S1', - totalTimeConsumption: 150, - waiting: 130, - running: 0, - blocked: 0, - }, - { - name: 'jetty-424', - icon: 'airplane', - threadlevel: 'S1', - totalTimeConsumption: 150, - waiting: 130, - running: 0, - blocked: 0, - }, - ], - }, - { - name: 'Downtime timer', - icon: 'airplane', - threadlevel: 'S0', - totalTimeConsumption: 150, - waiting: 123, - running: 20, - blocked: 0, - }, -]; - -export class ThreadNode { - name: string; - threadlevel: string; - totalTimeConsumption: number; - blocked: number; - running: number; - waiting: number; - icon: DtIconType; - children?: ThreadNode[]; -} - -export class ThreadFlatNode { - name: string; - threadlevel: string; - totalTimeConsumption: number; - blocked: number; - running: number; - waiting: number; - icon: DtIconType; - level: number; - expandable: boolean; -} +import { DtQuickFilterDefaultDataSource } from '@dynatrace/barista-components/experimental/quick-filter'; +import { + FILTER_FIELD_TEST_DATA, + ThreadFlatNode, + ThreadNode, + TREE_TABLE_TEST_DATA, +} from '@dynatrace/testing/fixtures'; @Component({ selector: 'dt-kitchen-sink', @@ -141,6 +43,13 @@ export class KitchenSink { treeFlattener: DtTreeFlattener; treeTableDataSource: DtTreeDataSource; + quickFilterDataSource = new DtQuickFilterDefaultDataSource( + FILTER_FIELD_TEST_DATA, + { + showInSidebar: () => true, + }, + ); + constructor() { this.treeControl = new DtTreeControl( this._getLevel, @@ -156,7 +65,7 @@ export class KitchenSink { this.treeControl, this.treeFlattener, ); - this.treeTableDataSource.data = TESTDATA; + this.treeTableDataSource.data = TREE_TABLE_TEST_DATA; } hasChild = (_: number, _nodeData: ThreadFlatNode) => _nodeData.expandable; diff --git a/libs/barista-components/drawer/src/drawer-container.scss b/libs/barista-components/drawer/src/drawer-container.scss index 38b5d31bce..4539c4ae82 100644 --- a/libs/barista-components/drawer/src/drawer-container.scss +++ b/libs/barista-components/drawer/src/drawer-container.scss @@ -40,7 +40,6 @@ $dt-backdrop-opacity: 0.4; flex-grow: 1; flex-shrink: 1; display: block; - height: 100%; overflow: auto; will-change: contents; diff --git a/libs/barista-components/experimental/quick-filter/README.md b/libs/barista-components/experimental/quick-filter/README.md new file mode 100644 index 0000000000..4c656b0cf8 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/README.md @@ -0,0 +1,51 @@ +# Quick Filter (experimental) + +Note: This component is still experimental, use with caution! The API is NOT +stable, and might change in minor or patch versions of the library. Help us get +this component out of the experimental state by providing feedback. + + + + + +## Imports + +You have to import the `DtQuickFilterModule` when you want to use the +``, `` and +``. Note that you need Angular's +`BrowserAnimationsModule` if you want to have animations or the +`NoopAnimationsModule` if you don't. + +```typescript +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DtQuickFilterModule } from '@dynatrace/barista-components/experimental/quick-filter'; + +@NgModule({ + imports: [BrowserAnimationsModule, DtQuickFilterModule], +}) +class AppModule {} +``` + +To use the quick filter in your template there is the +`` where you have to bind the data +source. The default content within the `dt-quick-filter` will be placed within +the quick-filter main content area. To set the title and the subtitle of the +sidebar you can leverage the `` and +`` tags to provide a level of customization. + +## Inputs + +| Name | Type | Default | Description | +| ------------------- | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `dataSource` | `DtQuickFilterDataSource` | | Provide a DataSource to feed data to the filter field and the quick filter. This input is mandatory. | +| `filters` | `any[][]` | | The currently selected filters. This input can also be used to programmatically add filters to the quick filter and filter field. | +| `label` | `string` | | The label for the input field. Can be set to something like "Filter by". Will be placed next to the filter icon in the filter field | +| `clearAllLabel` | `string` | | Label for the "Clear all" button in the filter field. Can be set to something like "Clear all". | +| `aria-label` | `string` | | Sets the value for the Aria-Label attribute. | +| `groupHeadlineRole` | `number` | | The aria-level of the group headlines for the document outline. | + +## Outputs + +| Name | Type | Description | +| --------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `filterChanges` | `EventEmitter` | Event emitted when filters have been updated by user interaction. Wont be triggered by programmatic changes | diff --git a/libs/barista-components/experimental/quick-filter/barista.json b/libs/barista-components/experimental/quick-filter/barista.json new file mode 100644 index 0000000000..7c496d6070 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/barista.json @@ -0,0 +1,22 @@ +{ + "title": "Quick Filter (experimental)", + "description": "", + "public": true, + "contributors": { + "dev": [ + { + "name": "Lukas Holzer", + "githubuser": "lukasholzer" + } + ], + "ux": [ + { + "name": "Xavier Javaloyas", + "githubuser": "Xavi-J" + } + ] + }, + "properties": ["work in progress", "experimental"], + "related": ["filter field"], + "tags": ["filter", "filter field", "quick filter", "angular", "component"] +} diff --git a/libs/barista-components/experimental/quick-filter/index.ts b/libs/barista-components/experimental/quick-filter/index.ts new file mode 100644 index 0000000000..cf545b7620 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/index.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './src/quick-filter'; +export * from './src/quick-filter-data-source'; +export * from './src/quick-filter-default-data-source'; +export * from './src/quick-filter.module'; diff --git a/libs/barista-components/experimental/quick-filter/jest.config.js b/libs/barista-components/experimental/quick-filter/jest.config.js new file mode 100644 index 0000000000..eb94995fb4 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + name: 'quick-filter', + preset: '../../../../jest.config.js', + coverageDirectory: + '../../../../coverage/components/experimental/quick-filter', + snapshotSerializers: [ + 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', + 'jest-preset-angular/build/AngularSnapshotSerializer.js', + 'jest-preset-angular/build/HTMLCommentSerializer.js', + ], +}; diff --git a/libs/barista-components/experimental/quick-filter/package.json b/libs/barista-components/experimental/quick-filter/package.json new file mode 100644 index 0000000000..dedb72ce9c --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/package.json @@ -0,0 +1,7 @@ +{ + "ngPackage": { + "lib": { + "entryFile": "index.ts" + } + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts new file mode 100644 index 0000000000..c353c6599c --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Observable } from 'rxjs'; +import { + DtNodeDef, + DtFilterFieldDataSource, +} from '@dynatrace/barista-components/filter-field'; + +export abstract class DtQuickFilterDataSource + implements DtFilterFieldDataSource { + /** + * Used by the DtFilterFieldControl. Called when it connects to the data source. + * Should return a stream of data that will be transformed, filtered and + * displayed by the DtFilterField and the DtFilterFieldControl. + */ + abstract connect(): Observable; + + /** Used by the DtFilterField. Called when it is destroyed. */ + abstract disconnect(): void; + + /** Whether the provided data object can be transformed into an DtAutocompleteDef. */ + abstract isAutocomplete(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtOptionDef. */ + abstract isOption(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtGroupDef. */ + abstract isGroup(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtFreeTextDef. */ + abstract isFreeText(data: T): boolean; + + /** Whether the provided data object can be transformed into an DtRangeDef. */ + abstract isRange(data: T): boolean; + + /** A function that receives each node and needs to return whether the given node should be shown in the sidebar */ + abstract showInSidebarFunction = (_node: any): boolean => true; + + /** Transforms the provided data into a DtNodeDef which contains a DtAutocompleteDef. */ + abstract transformAutocomplete( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtOptionDef. */ + abstract transformOption( + data: T, + parentAutocompleteOrOption: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtGroupDef. */ + abstract transformGroup( + data: T, + parentAutocomplete: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtFreeTextDef. */ + abstract transformFreeText( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef which contains a DtRangeDef. */ + abstract transformRange( + data: T, + parent: DtNodeDef | null, + existingDef: DtNodeDef | null, + ): DtNodeDef; + + /** Transforms the provided data into a DtNodeDef. */ + abstract transformObject( + data: T | null, + parent: DtNodeDef | null, + ): DtNodeDef | null; + + /** Transforms the provided list of data objects into an array of DtNodeDefs. */ + abstract transformList(list: T[], parent: DtNodeDef | null): DtNodeDef[]; +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts new file mode 100644 index 0000000000..fadfe97756 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-default-data-source.ts @@ -0,0 +1,253 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DtQuickFilterDataSource } from './quick-filter-data-source'; +import { isDefined, isObject } from '@dynatrace/barista-components/core'; +import { + DtNodeDef, + dtRangeDef, + dtFreeTextDef, + dtGroupDef, + dtOptionDef, + isDtAutocompleteDef, + DtFilterFieldDefaultDataSourceAutocomplete, + DtFilterFieldDefaultDataSourceSimpleGroup, + DtFilterFieldDefaultDataSourceGroup, + DtFilterFieldDefaultDataSourceFreeText, + DtFilterFieldDefaultDataSourceRange, + dtAutocompleteDef, + isDtGroupDef, +} from '@dynatrace/barista-components/filter-field'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export interface DtQuickFilterDefaultDataSourceSimpleOption { + name: string; +} + +export type DtQuickFilterDefaultDataSourceRange = DtFilterFieldDefaultDataSourceRange; +export type DtQuickFilterDefaultDataSourceFreeText = DtFilterFieldDefaultDataSourceFreeText; +export type DtQuickFilterDefaultDataSourceAutocomplete = DtFilterFieldDefaultDataSourceAutocomplete; +export type DtQuickFilterDefaultDataSourceGroup = DtFilterFieldDefaultDataSourceGroup; +export type DtQuickFilterDefaultDataSourceSimpleGroup = DtFilterFieldDefaultDataSourceSimpleGroup; + +export type DtQuickFilterDefaultDataSourceType = + | DtQuickFilterDefaultDataSourceOption + | DtQuickFilterDefaultDataSourceGroup + | DtQuickFilterDefaultDataSourceAutocomplete + | DtQuickFilterDefaultDataSourceFreeText + | DtQuickFilterDefaultDataSourceRange; + +export type DtQuickFilterDefaultDataSourceOption = + | DtQuickFilterDefaultDataSourceSimpleOption + | (DtQuickFilterDefaultDataSourceAutocomplete & + DtQuickFilterDefaultDataSourceSimpleOption) + | (DtQuickFilterDefaultDataSourceFreeText & + DtQuickFilterDefaultDataSourceSimpleOption) + | (DtQuickFilterDefaultDataSourceRange & + DtQuickFilterDefaultDataSourceSimpleOption); + +export interface DtQuickFilterDefaultDataSourceConfig { + showInSidebar: (node: any) => boolean; +} + +export class DtQuickFilterDefaultDataSource< + T extends DtQuickFilterDefaultDataSourceType +> implements DtQuickFilterDataSource { + private readonly _data$: BehaviorSubject; + + /** Structure of data that is used, transformed and rendered by the filter-field. */ + get data(): T { + return this._data$.value; + } + set data(data: T) { + this._data$.next(data); + } + + constructor( + initialData: T = (null as unknown) as T, + config: DtQuickFilterDefaultDataSourceConfig, + ) { + this._data$ = new BehaviorSubject(initialData); + this.showInSidebarFunction = config.showInSidebar; + } + + /** Function that evaluates if a node should be displayed in the quick filter sidebar */ + showInSidebarFunction: (node: any) => boolean; + + /** + * Used by the DtQuickFilter. Called when it connects to the data source. + * Should return a stream of data that will be transformed, filtered and + * displayed by the DtQuickFilterViewer (filter-field) + */ + connect(): Observable { + return this._data$.pipe(map(data => this.transformObject(data))); + } + + /** Used by the DtQuickFilter. Called when it is destroyed. No-op. */ + disconnect(): void { + this._data$.complete(); + } + + /** Whether the provided data object is of type AutocompleteData */ + isAutocomplete( + data: any, + ): data is DtQuickFilterDefaultDataSourceAutocomplete { + return isObject(data) && Array.isArray(data.autocomplete); + } + + /** Whether the provided data object is of type OptionData */ + isOption(data: any): data is DtQuickFilterDefaultDataSourceOption { + return isObject(data) && typeof data.name === 'string'; + } + + /** Whether the provided data object is of type GroupData */ + isGroup(data: any): data is DtQuickFilterDefaultDataSourceGroup { + return ( + isObject(data) && + typeof data.name === 'string' && + Array.isArray(data.options) + ); + } + + /** Whether the provided data object is of type FreeTextData */ + isFreeText(data: any): data is DtQuickFilterDefaultDataSourceFreeText { + return isObject(data) && Array.isArray(data.suggestions); + } + + /** Whether the provided data object is of type RangeData */ + isRange(data: any): data is DtQuickFilterDefaultDataSourceRange { + return isObject(data) && isObject(data.range); + } + + /** Transforms the provided data into a DtNodeDef which contains a DtAutocompleteDef. */ + transformAutocomplete( + data: DtQuickFilterDefaultDataSourceAutocomplete, + ): DtNodeDef { + const def = dtAutocompleteDef( + data, + null, + [], + !!data.distinct, + !!data.async, + ); + def.autocomplete!.optionsOrGroups = this.transformList( + data.autocomplete, + def, + ); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtOptionDef. */ + transformOption( + data: DtQuickFilterDefaultDataSourceOption, + parentAutocompleteOrOption: DtNodeDef | null = null, + existingDef: DtNodeDef | null = null, + ): DtNodeDef { + const parentGroup = isDtGroupDef(parentAutocompleteOrOption) + ? parentAutocompleteOrOption + : null; + const parentAutocomplete = + parentGroup !== null + ? parentGroup.group.parentAutocomplete + : isDtAutocompleteDef(parentAutocompleteOrOption) + ? (parentAutocompleteOrOption as DtNodeDef) + : null; + return dtOptionDef( + data, + existingDef, + data.name, + null, + parentAutocomplete, + parentGroup, + ); + } + + /** Transforms the provided data into a DtNodeDef which contains a DtGroupDef. */ + transformGroup( + data: DtQuickFilterDefaultDataSourceGroup, + parentAutocomplete: DtNodeDef | null = null, + existingDef: DtNodeDef | null = null, + ): DtNodeDef { + const def = dtGroupDef( + data, + existingDef, + data.name, + [], + parentAutocomplete, + ); + def.group!.options = this.transformList(data.options, def); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtFreeTextDef. */ + transformFreeText(data: DtQuickFilterDefaultDataSourceFreeText): DtNodeDef { + const def = dtFreeTextDef( + data, + null, + [], + data.validators, + isDefined(data.unique) ? data.unique! : false, + ); + def.freeText!.suggestions = this.transformList(data.suggestions, def); + return def; + } + + /** Transforms the provided data into a DtNodeDef which contains a DtRangeDef. */ + transformRange(data: DtQuickFilterDefaultDataSourceRange): DtNodeDef { + return dtRangeDef( + data, + null, + !!data.range.operators.range, + !!data.range.operators.equal, + !!data.range.operators.greaterThanEqual, + !!data.range.operators.lessThanEqual, + data.range.unit, + isDefined(data.unique) ? data.unique! : false, + ); + } + + /** Transforms the provided data into a DtNodeDef. */ + transformObject( + data: DtQuickFilterDefaultDataSourceType | null, + parent: DtNodeDef | null = null, + ): DtNodeDef | null { + let def: DtNodeDef | null = null; + if (this.isAutocomplete(data)) { + def = this.transformAutocomplete(data); + } else if (this.isFreeText(data)) { + def = this.transformFreeText(data); + } else if (this.isRange(data)) { + def = this.transformRange(data); + } + + if (this.isGroup(data)) { + def = this.transformGroup(data); + } else if (this.isOption(data)) { + def = this.transformOption(data, parent, def); + } + return def; + } + + /** Transforms the provided list of data objects into an array of DtNodeDefs. */ + transformList( + list: Array, + parent: DtNodeDef | null = null, + ): DtNodeDef[] { + return list + .map(item => this.transformObject(item, parent)) + .filter(item => item !== null) as DtNodeDef[]; + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html new file mode 100644 index 0000000000..b955535356 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html @@ -0,0 +1,40 @@ +
+ {{ _getViewValue(_nodeDef)}} +
+ + + + + Any + + + {{_getViewValue(item)}} + + + + + +
+ + {{_getViewValue(item)}} + +
+
+ + diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss new file mode 100644 index 0000000000..47ff48005c --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss @@ -0,0 +1,21 @@ +@import '../../../core/src/style/variables'; +@import '../../../style/font-mixins'; + +:host { + display: block; + padding: 8px 0; +} + +.dt-quick-filter-group-headline { + @include dt-h3-font(); + margin: 0 0 8px; +} + +.dt-quick-filter-group-items { + display: flex; + flex-flow: column; + + > * { + margin-bottom: 8px; + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts new file mode 100644 index 0000000000..12411ef867 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + Output, + ViewEncapsulation, +} from '@angular/core'; +import { DtCheckboxChange } from '@dynatrace/barista-components/checkbox'; +import { + DtNodeDef, + isDtOptionDef, + isDtRenderType, +} from '@dynatrace/barista-components/filter-field'; +import { DtRadioChange } from '@dynatrace/barista-components/radio'; +import { buildIdPathsFromFilters } from './quick-filter-utils'; +import { + Action, + addFilter, + removeFilter, + unsetFilterGroup, + updateFilter, +} from './state/actions'; + +/** @internal The DtQuickFilterGroup is an internal component */ +@Component({ + selector: 'dt-quick-filter-group', + templateUrl: './quick-filter-group.html', + styleUrls: ['./quick-filter-group.scss'], + encapsulation: ViewEncapsulation.Emulated, + changeDetection: ChangeDetectionStrategy.OnPush, + preserveWhitespaces: false, + host: { + class: 'dt-quick-filter-group', + }, +}) +export class DtQuickFilterGroup { + /** + * @internal + * The aria-level of the group headlines for the document outline. + */ + @Input() groupHeadlineRole: number = 3; + + /** @internal The nodeDef of the autocomplete that should be rendered */ + @Input('nodeDef') _nodeDef: DtNodeDef; + + /** @internal The list of all active filters */ + @Input() + set activeFilters(filters: any[][]) { + this._activeFilterPaths = buildIdPathsFromFilters(filters || []); + this._changeDetectorRef.markForCheck(); + } + + /** @internal Emits a new action that changes the filter */ + @Output() readonly filterChange = new EventEmitter(); + + /** A list of active filter ids */ + private _activeFilterPaths: string[] = []; + + constructor(private _changeDetectorRef: ChangeDetectorRef) {} + + /** @internal Method that is used to unset a whole filter group */ + _unsetGroup(): void { + this.filterChange.emit(unsetFilterGroup(this._nodeDef)); + } + + /** @internal Updates a radio box */ + _selectOption(change: DtRadioChange): void { + if (change.value) { + this.filterChange.emit(updateFilter(change.value)); + } + } + + /** @internal Select or deselect a checkbox */ + _selectCheckBox(change: DtCheckboxChange): void { + const action = change.checked + ? addFilter(change.source.value) + : removeFilter(change.source.value); + this.filterChange.emit(action); + } + + /** @internal Helper function that checks if nothing is selected inside a group */ + _isNothingSelected(): boolean { + if (this._nodeDef.option && this._nodeDef.option.uid) { + const index = this._activeFilterPaths.findIndex(path => + path.startsWith(this._nodeDef.option!.uid!), + ); + return index === -1; + } + return false; + } + + /** @internal Helper function that checks if an option is active */ + _isActive(node: DtNodeDef): boolean { + return !!( + node.option && this._activeFilterPaths.includes(node.option.uid || '') + ); + } + + /** + * @internal + * Helper function that checks if the nodeDef of the autocomplete is distinct. + * Needed to differentiate between radios or checkboxes. + */ + _isDistinct(): boolean { + return !!this._nodeDef.autocomplete?.distinct; + } + + /** @internal Helper function that returns safely the viewValue of a nodeDef */ + _getViewValue(nodeDef: DtNodeDef): string { + return nodeDef?.option ? nodeDef.option.viewValue : ''; + } + + /** + * @internal + * Helper function that returns all options that can be displayed. + * They can only be displayed if it is an option of an autocomplete that + * is not a render type so only have a text. + */ + _getOptions(): DtNodeDef[] { + if (this._nodeDef?.autocomplete) { + return this._nodeDef.autocomplete.optionsOrGroups.filter( + def => isDtOptionDef(def) && !isDtRenderType(def), + ); + } + return []; + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts new file mode 100644 index 0000000000..be553fd585 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-utils.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DELIMITER } from '@dynatrace/barista-components/filter-field'; + +export function buildIdPathsFromFilters(filters: any[][]): string[] { + return filters.map(path => + path.reduce( + (previousValue, currentValue) => + `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, + ), + ); +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.html b/libs/barista-components/experimental/quick-filter/src/quick-filter.html new file mode 100644 index 0000000000..542bbc02c8 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + +
+ + +
+
diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts new file mode 100644 index 0000000000..b1379045ce --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.module.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { DtCheckboxModule } from '@dynatrace/barista-components/checkbox'; +import { DtDrawerModule } from '@dynatrace/barista-components/drawer'; +import { DtFilterFieldModule } from '@dynatrace/barista-components/filter-field'; +import { DtRadioModule } from '@dynatrace/barista-components/radio'; +import { + DtQuickFilterSubTitle, + DtQuickFilterTitle, + DtQuickFilter, +} from './quick-filter'; +import { DtQuickFilterGroup } from './quick-filter-group'; + +const COMPONENTS = [DtQuickFilter, DtQuickFilterSubTitle, DtQuickFilterTitle]; + +@NgModule({ + imports: [ + CommonModule, + DtDrawerModule, + DtFilterFieldModule, + DtCheckboxModule, + DtRadioModule, + ], + exports: COMPONENTS, + declarations: [...COMPONENTS, DtQuickFilterGroup], +}) +export class DtQuickFilterModule {} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.scss b/libs/barista-components/experimental/quick-filter/src/quick-filter.scss new file mode 100644 index 0000000000..c086c440a0 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.scss @@ -0,0 +1,81 @@ +@import '../../../core/src/style/variables'; +@import '../../../core/src/style/interactive-common'; + +:host { + display: block; + background-color: $gray-130; + padding: 20px; +} + +.dt-quick-filter-drawer { + margin-top: 12px; +} + +.dt-quick-filter-content { + position: relative; + display: block; + padding: 12px 12px 12px 26px; + height: 100%; + background-color: white; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.dt-drawer { + background-color: $gray-130; + position: relative; + padding-right: 26px; +} + +:host ::ng-deep { + .dt-quick-filter-title, + .dt-quick-filter-sub-title { + display: block; + margin: 0; + } + + .dt-quick-filter-title { + font-size: 20px; + color: $gray-700; + } + + .dt-quick-filter-sub-title { + font-size: 12px; + color: $gray-500; + margin: 0; + } +} + +.dt-quick-filter-group + .dt-quick-filter-group { + border-top: 1px solid $gray-200; +} + +.dt-quick-filter-open, +.dt-quick-filter-close { + appearance: none; + top: 8px; + background: $white; + border-width: 1px; + border-style: solid; + position: absolute; + color: $turquoise-600; + font-size: 1.2em; + cursor: pointer; +} + +.dt-quick-filter-open { + left: 0; + padding: 6px 6px 6px 0; + border-left: none; + background-color: $gray-130; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.dt-quick-filter-close { + right: -1px; + padding: 6px 0 6px 6px; + border-right: none; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts new file mode 100644 index 0000000000..7dbf382c08 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.spec.ts @@ -0,0 +1,221 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// tslint:disable no-lifecycle-call no-use-before-declare no-magic-numbers +// tslint:disable no-any max-file-line-count no-unbound-method use-component-selector + +import { HttpClient } from '@angular/common/http'; +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { DtCheckbox } from '@dynatrace/barista-components/checkbox'; +import { DtFilterField } from '@dynatrace/barista-components/filter-field'; +import { DtIconModule } from '@dynatrace/barista-components/icon'; +import { + createComponent, + dispatchMouseEvent, +} from '@dynatrace/testing/browser'; +import { FILTER_FIELD_TEST_DATA } from '@dynatrace/testing/fixtures'; +import { of } from 'rxjs'; +import { DtQuickFilter, DtQuickFilterChangeEvent } from './quick-filter'; +import { DtQuickFilterDefaultDataSource } from './quick-filter-default-data-source'; +import { DtQuickFilterModule } from './quick-filter.module'; + +describe('dt-quick-filter', () => { + let instanceDebugElement: DebugElement; + let quickFilterInstance: DtQuickFilter; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + DtQuickFilterModule, + NoopAnimationsModule, + DtIconModule.forRoot({ svgIconLocation: '{{name}}.svg' }), + ], + declarations: [QuickFilterSimpleComponent, QuickFilterDefaultComponent], + providers: [ + { + provide: HttpClient, + useValue: { + get: jest.fn().mockReturnValue(of('')), + }, + }, + ], + }); + TestBed.compileComponents(); + }); + + describe('Simple QuickFilter without dataSource', () => { + let fixture: ComponentFixture; + beforeEach(() => { + fixture = createComponent(QuickFilterSimpleComponent); + instanceDebugElement = fixture.debugElement.query( + By.directive(DtQuickFilter), + ); + quickFilterInstance = instanceDebugElement.injector.get( + DtQuickFilter, + ); + }); + + it('should have an empty filters array if no dataSource is set', () => { + expect(quickFilterInstance.filters).toHaveLength(0); + }); + }); + + describe('Normal QuickFilter with mixed dataSource', () => { + let fixture: ComponentFixture; + let filterFieldDebugElement: DebugElement; + let filterFieldInstance: DtFilterField; + + beforeEach(() => { + fixture = createComponent(QuickFilterDefaultComponent); + instanceDebugElement = fixture.debugElement.query( + By.directive(DtQuickFilter), + ); + quickFilterInstance = instanceDebugElement.injector.get( + DtQuickFilter, + ); + + filterFieldDebugElement = fixture.debugElement.query( + By.directive(DtFilterField), + ); + filterFieldInstance = filterFieldDebugElement.injector.get( + DtFilterField, + ); + }); + + it('should set the filters on the filter field if they are set on the quick-filter', () => { + expect(quickFilterInstance.filters).toHaveLength(0); + const filters = [ + [ + FILTER_FIELD_TEST_DATA.autocomplete[0], + FILTER_FIELD_TEST_DATA.autocomplete[0].autocomplete![0], + ], + ]; + + quickFilterInstance.filters = filters; + fixture.detectChanges(); + expect(filterFieldInstance.filters).toMatchObject(filters); + }); + + it('should reset the filters if the data source gets switched', () => { + quickFilterInstance.filters = [ + [ + FILTER_FIELD_TEST_DATA.autocomplete[0], + FILTER_FIELD_TEST_DATA.autocomplete[0].autocomplete![0], + ], + ]; + fixture.detectChanges(); + + fixture.componentInstance._dataSource = new DtQuickFilterDefaultDataSource( + FILTER_FIELD_TEST_DATA, + { + showInSidebar: () => true, + }, + ); + fixture.detectChanges(); + + expect(filterFieldInstance.filters).toMatchObject([]); + expect(quickFilterInstance.filters).toMatchObject([]); + }); + + it('should filter the groups that should be displayed in the sidebar dynamically', () => { + let groups = getGroupHeadlines(fixture.debugElement); + expect(groups).toHaveLength(3); + expect(groups).toMatchObject(['AUT', 'USA', 'Not in Quickfilter']); + + fixture.detectChanges(); + + fixture.componentInstance._dataSource = new DtQuickFilterDefaultDataSource( + FILTER_FIELD_TEST_DATA, + { + showInSidebar: node => node.name !== 'Not in Quickfilter', + }, + ); + fixture.detectChanges(); + groups = getGroupHeadlines(fixture.debugElement); + + expect(groups).toHaveLength(2); + expect(groups).toMatchObject(['AUT', 'USA']); + }); + + it('should dispatch an event with the changes on selecting an option', () => { + const changeSpy = jest.spyOn(fixture.componentInstance, 'filterChanges'); + expect(changeSpy).toHaveBeenCalledTimes(0); + const checkboxes = fixture.debugElement + .queryAll(By.directive(DtCheckbox)) + .map(el => el.query(By.css('label'))); + + dispatchMouseEvent(checkboxes[1].nativeElement, 'click'); + fixture.detectChanges(); + + expect(changeSpy).toHaveBeenCalledTimes(1); + expect(changeSpy.mock.calls[0][0].filters).toMatchObject([ + [ + FILTER_FIELD_TEST_DATA.autocomplete[1], + FILTER_FIELD_TEST_DATA.autocomplete[1].autocomplete![1], + ], + ]); + changeSpy.mockClear(); + }); + }); +}); + +/** Get all quick filter group item headlines */ +function getGroupHeadlines(debugElement: DebugElement): string[] { + return debugElement + .queryAll(By.css('.dt-quick-filter-group-headline')) + .map(el => el.nativeElement.textContent); +} + +@Component({ + selector: 'dt-quick-filter-simple', + template: ` + + `, +}) +class QuickFilterSimpleComponent {} + +@Component({ + selector: 'dt-quick-filter-simple', + template: ` + + Quick-filter + + All options in the filter field above + + + my content + + `, +}) +class QuickFilterDefaultComponent { + filterFn = () => true; + label = 'Filter by'; + clearAllLabel = 'Clear all'; + + filterChanges(_event: DtQuickFilterChangeEvent): void {} + + _dataSource = new DtQuickFilterDefaultDataSource(FILTER_FIELD_TEST_DATA, { + showInSidebar: this.filterFn, + }); +} diff --git a/libs/barista-components/experimental/quick-filter/src/quick-filter.ts b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts new file mode 100644 index 0000000000..6283a01a58 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts @@ -0,0 +1,181 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + Directive, + EventEmitter, + Input, + OnDestroy, + Output, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; +import { + DtFilterField, + DtFilterFieldChangeEvent, +} from '@dynatrace/barista-components/filter-field'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { DtQuickFilterDataSource } from './quick-filter-data-source'; +import { Action, setFilters, switchDataSource } from './state/actions'; +import { quickFilterReducer } from './state/reducer'; +import { getAutocompletes, getDataSource, getFilters } from './state/selectors'; +import { createQuickFilterStore, QuickFilterState } from './state/store'; + +/** Directive that is used to place a title inside the quick filters sidebar */ +@Directive({ + selector: 'dt-quick-filter-title', + exportAs: 'dtQuickFilterTitle', + host: { + class: 'dt-quick-filter-title', + }, +}) +export class DtQuickFilterTitle {} + +/** Directive that is used to place a subtitle inside the quick filters sidebar */ +@Directive({ + selector: 'dt-quick-filter-sub-title', + exportAs: 'dtQuickFilterSubTitle', + host: { + class: 'dt-quick-filter-sub-title', + }, +}) +export class DtQuickFilterSubTitle {} + +/** + * The `DtQuickFilterChangeEvent` is a class that is used to transport data. + * It contains the added and removed filters as the current set of all filters. + */ +export class DtQuickFilterChangeEvent extends DtFilterFieldChangeEvent {} + +@Component({ + selector: 'dt-quick-filter', + exportAs: 'dtQuickFilter', + templateUrl: 'quick-filter.html', + styleUrls: ['quick-filter.scss'], + host: { + class: 'dt-quick-filter', + }, + preserveWhitespaces: false, + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.Emulated, +}) +export class DtQuickFilter implements AfterViewInit, OnDestroy { + /** Emits when a new filter has been added or removed. */ + @Output() readonly filterChanges = new EventEmitter< + DtQuickFilterChangeEvent + >(); + + /** + * @internal + * Instance of the filter field that will be controlled by the quick filter + */ + @ViewChild(DtFilterField, { static: true }) + _filterField: DtFilterField; + + /** + * Label for the filter field (e.g. "Filter by"). + * Will be placed next to the filter icon in the filter field + */ + @Input() label = ''; + + /** Label for the "Clear all" button in the filter field (e.g. "Clear all"). */ + @Input() clearAllLabel = ''; + + /** Set the Aria-Label attribute */ + @Input('aria-label') ariaLabel = ''; + + /** The data source instance that should be connected to the filter field. */ + @Input() + get dataSource(): DtQuickFilterDataSource { + const dataSource = this._store.select( + (state: BehaviorSubject) => state.value.dataSource, + )!; + return dataSource; + } + set dataSource(dataSource: DtQuickFilterDataSource) { + this._store.dispatch(switchDataSource(dataSource)); + } + + /** The currently applied filters */ + @Input() + get filters(): T[][] { + return this._filterField.filters; + } + set filters(filters: T[][]) { + this._store.dispatch(setFilters(filters)); + } + + /** + * The aria-level of the group headlines for the document outline. + */ + @Input() groupHeadlineRole: number = 3; + + /** The store where the data flow is managed */ + private _store = createQuickFilterStore(quickFilterReducer); + + /** @internal the autocomplete fields that should be rendered by the quick filter */ + readonly _autocompleteData$ = this._store.select(getAutocompletes); + /** @internal the dataSource that gets passed to the filter field */ + readonly _filterFieldDataSource$ = this._store.select(getDataSource); + /** @internal the list of all current active filters */ + readonly _activeFilters$ = this._store.select(getFilters); + + /** Subject that is used for bulk unsubscribing */ + private _destroy$ = new Subject(); + + /** Angular life-cycle hook that will be called after the view is initialized */ + ngAfterViewInit(): void { + // When the filters changes apply them to the filter field + this._activeFilters$.pipe(takeUntil(this._destroy$)).subscribe(filters => { + if (this._filterField.filters !== filters) { + this._filterField.filters = filters; + } + }); + } + + /** Angular life-cycle hook that will be called on component destroy */ + ngOnDestroy(): void { + this._destroy$.next(); + this._destroy$.complete(); + } + + /** + * @internal + * When the user selects an option in the quick filter an action gets passed + * to this function that will be dispatched to the store + */ + _changeFilter(action: Action): void { + this._store.dispatch(action); + this.filterChanges.emit( + new DtQuickFilterChangeEvent( + this._filterField, + [], + [], + this._filterField.filters, + ), + ); + } + + /** @internal Bubble the filter field change event through */ + _filterFiledChanged(change: DtFilterFieldChangeEvent): void { + this._store.dispatch(setFilters(change.filters)); + this.filterChanges.emit(change); + } +} diff --git a/libs/barista-components/experimental/quick-filter/src/state/actions.ts b/libs/barista-components/experimental/quick-filter/src/state/actions.ts new file mode 100644 index 0000000000..733849c656 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/actions.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + DtFilterFieldDataSource, + DtNodeDef, +} from '@dynatrace/barista-components/filter-field'; + +/** Enum for all the possible action types */ +export enum ActionType { + INIT = '@@actions init', + ADD_FILTER = '@@actions add filter', + REMOVE_FILTER = '@@actions remove filter', + UPDATE_FILTER = '@@actions update filter', + SET_FILTERS = '@@actions set filters', + UNSET_FILTER_GROUP = '@@actions unset filter group', + SWITCH_DATA_SOURCE = '@@actions switch dataSource', + UPDATE_DATA_SOURCE = '@@actions update dataSource', +} + +/** Interface for an action */ +export interface Action { + readonly type: ActionType; + payload?: T; +} +/** Function which helps to create actions without mistakes */ +export const action = (type: ActionType, payload?: T): Action => ({ + type, + payload, +}); + +/** Action that sets filters (Bulk operation for addFilter) */ +export const setFilters = (filters: any[][]) => + action(ActionType.SET_FILTERS, filters); + +/** Action that unsets a filter group */ +export const unsetFilterGroup = (group: DtNodeDef) => + action(ActionType.UNSET_FILTER_GROUP, group); + +/** Action that adds a filter */ +export const addFilter = (item: DtNodeDef) => + action(ActionType.ADD_FILTER, item); + +/** Action that removes a filter */ +export const removeFilter = (item: DtNodeDef) => + action(ActionType.REMOVE_FILTER, item); + +/** Action that updates a filter */ +export const updateFilter = (item: DtNodeDef) => + action(ActionType.UPDATE_FILTER, item); + +/** Action that subscribes to a new data source */ +export const switchDataSource = (item: DtFilterFieldDataSource) => + action(ActionType.SWITCH_DATA_SOURCE, item); + +/** Action that updates the data source */ +export const updateDataSource = (nodeDef: DtNodeDef) => + action(ActionType.UPDATE_DATA_SOURCE, nodeDef); diff --git a/libs/barista-components/experimental/quick-filter/src/state/effects.ts b/libs/barista-components/experimental/quick-filter/src/state/effects.ts new file mode 100644 index 0000000000..8cd82a9d10 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/effects.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DtFilterFieldDataSource, + DtNodeDef, +} from '@dynatrace/barista-components/filter-field'; +import { MonoTypeOperatorFunction, Observable } from 'rxjs'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { Action, ActionType, updateDataSource } from './actions'; +import { QuickFilterState } from './store'; + +/** Type for an effect */ +export type Effect = ( + action$: Observable, + state$?: Observable, +) => Observable; + +/** Operator to filter actions */ +export const ofType = ( + ...types: ActionType[] +): MonoTypeOperatorFunction> => + filter((action: Action) => types.indexOf(action.type) > -1); + +/** Connects to a new Data dataSource */ +export const switchDataSourceEffect: Effect = (action$: Observable) => + action$.pipe( + ofType(ActionType.SWITCH_DATA_SOURCE), + switchMap(action => action.payload!.connect()), + map((nodeDef: DtNodeDef) => updateDataSource(nodeDef)), + ); diff --git a/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts b/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts new file mode 100644 index 0000000000..a4f3d9363d --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.spec.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { dtAutocompleteDef } from '@dynatrace/barista-components/filter-field'; +import { buildData } from './reducer'; + +test('should build the data with an object', () => { + const data = { name: 'Linz' }; + const autocomplete = dtAutocompleteDef(data, null, [], false, false); + expect(buildData(autocomplete)).toMatchObject([data]); +}); + +test('should build the data with undefined', () => { + const data = undefined; + const autocomplete = dtAutocompleteDef(data, null, [], false, false); + + expect(buildData(autocomplete)).toMatchObject([data]); +}); diff --git a/libs/barista-components/experimental/quick-filter/src/state/reducer.ts b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts new file mode 100644 index 0000000000..b9e62a93b3 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts @@ -0,0 +1,161 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DtLogger, DtLoggerFactory } from '@dynatrace/barista-components/core'; +import { + DELIMITER, + DtNodeDef, +} from '@dynatrace/barista-components/filter-field'; +import { Action, ActionType } from './actions'; +import { initialState, QuickFilterState } from './store'; + +const logger: DtLogger = DtLoggerFactory.create('DtQuickFilter State'); + +/** @internal Type of a reducer */ +export type Reducer = ( + state: QuickFilterState, + action: Action, +) => QuickFilterState; + +/** + * @internal + * The Quick Filter reducer is the place where we handle all the state updates + * To have a single entry point. Every action can trigger an update of the state. + * It has to be a pure function that always returns a new object of the state. + * @param state The state that should be modified. + * @param action The current action that should be handled. + */ +export function quickFilterReducer( + state: QuickFilterState, + action: Action, +): QuickFilterState { + logger.debug(`Reducer <${action.type}> `, action); + + switch (action.type) { + case ActionType.SWITCH_DATA_SOURCE: + if (state.dataSource) { + state.dataSource.disconnect(); + } + return { ...initialState, dataSource: action.payload }; + case ActionType.UPDATE_DATA_SOURCE: + return { ...state, nodeDef: action.payload }; + case ActionType.SET_FILTERS: + return { ...state, filters: action.payload }; + case ActionType.UNSET_FILTER_GROUP: + return { + ...state, + filters: unsetFilterGroup(state.filters, action.payload), + }; + case ActionType.ADD_FILTER: + return { ...state, filters: addFilter(state.filters, action.payload) }; + case ActionType.UPDATE_FILTER: + return { ...state, filters: updateFilter(state.filters, action.payload) }; + case ActionType.REMOVE_FILTER: + return { ...state, filters: removeFilter(state.filters, action.payload) }; + default: + // Default return the same state as it was passed so don't modify anything + return state; + } +} + +// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +// # +// # HELPER functions for modifying the filters +// # +// # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +/** @internal Add a filter to the filters array */ +export function addFilter(filters: any[][], item: DtNodeDef): any[][] { + return [...filters, buildData(item)]; +} + +/** @internal Remove a filter from the filters array */ +export function removeFilter(filters: any[][], item: DtNodeDef): any[][] { + const index = findSelectedOption(filters, item, false); + const updatedState = [...filters]; + + if (index > -1) { + delete updatedState[index]; + } + + return updatedState.filter(Boolean); +} + +/** @internal Update a filter inside the filters array */ +export function updateFilter(filters: any[][], item: DtNodeDef): any[][] { + const index = findSelectedOption(filters, item, true); + + if (index < 0) { + return addFilter(filters, item); + } + + filters[index] = buildData(item); + + return filters; +} + +/** @internal Remove a group from the filters array */ +export function unsetFilterGroup(filters: any[][], group: DtNodeDef): any[][] { + if (group.option && group.option.viewValue) { + return filters.filter(filter => filter[0].name !== group.option!.viewValue); + } + return filters; +} + +/** @internal Build the quick filter data out of a node definition */ +export function buildData(item: DtNodeDef): any[] { + const data = [item.data]; + + if (item.option && item.option.parentAutocomplete) { + data.unshift(item.option.parentAutocomplete.data); + } + return data; +} + +/** @internal Find a filter inside the filters array based on a NodeDef */ +export function findSelectedOption( + filters: any[][], + item: DtNodeDef, + distinct: boolean = false, +): number { + return filters.findIndex(path => { + if (item.option && item.option.uid) { + // split the uid in the parts that define the path + // (like the user clicked through in the filter field) + const parts = item.option.uid.split(DELIMITER); + + // if the option is distinct we only have to check for the groups name because + // there can only be one distinct option selected so we know immediately if it is selected + if (distinct && parts[0] === path[0].name) { + return true; + } + + // if it is not distinct we have to build the full path out of the current filters to check + // wether the path matches them provided node definition + const dataPath = path.reduce( + (previousValue, currentValue) => + `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, + ); + + // if the built path for the filters inside the array is equal to the option + // in the provided nodeDef then we found our selected option + if (item.option.uid === dataPath) { + return true; + } + } + + return false; + }); +} diff --git a/libs/barista-components/experimental/quick-filter/src/state/selectors.ts b/libs/barista-components/experimental/quick-filter/src/state/selectors.ts new file mode 100644 index 0000000000..7e6a3c3e22 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/selectors.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + applyDtOptionIds, + DtNodeDef, + isDtAutocompleteDef, +} from '@dynatrace/barista-components/filter-field'; +import { Observable } from 'rxjs'; +import { filter, map, pluck, tap, withLatestFrom } from 'rxjs/operators'; +import { DtQuickFilterDataSource } from '../quick-filter-data-source'; +import { QuickFilterState } from './store'; + +/** @internal Select all autocompletes from the root Node Def from the store */ +export const getAutocompletes = ( + state$: Observable, +): Observable => + state$.pipe( + tap(state => { + // apply the ids to the node to identify them later on + if (state.nodeDef) { + applyDtOptionIds(state.nodeDef); + } + }), + pluck('nodeDef'), + filter(isDtAutocompleteDef), + withLatestFrom( + getDataSource(state$).pipe(filter(Boolean)), + ), + map(([{ autocomplete }, { showInSidebarFunction }]) => + autocomplete.optionsOrGroups.filter( + node => isDtAutocompleteDef(node) && showInSidebarFunction(node.data), + ), + ), + ); + +/** @internal Select the data Source from the store */ +export const getDataSource = (state$: Observable) => + state$.pipe(pluck('dataSource')); + +/** @internal Select the actual applied filters */ +export const getFilters = (state$: Observable) => + state$.pipe(pluck('filters')); diff --git a/libs/barista-components/experimental/quick-filter/src/state/store.ts b/libs/barista-components/experimental/quick-filter/src/state/store.ts new file mode 100644 index 0000000000..088f7af624 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/store.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DtNodeDef } from '@dynatrace/barista-components/filter-field'; +import { BehaviorSubject, merge, Observable } from 'rxjs'; +import { map, shareReplay, withLatestFrom } from 'rxjs/operators'; +import { DtQuickFilterDataSource } from '../quick-filter-data-source'; +import { Action, ActionType } from './actions'; +import { Effect, switchDataSourceEffect } from './effects'; +import { Reducer } from './reducer'; + +/** @internal Interface that describes the QuickFilter state */ +export interface QuickFilterState { + /** The root NodeDef of the dataSource */ + nodeDef?: DtNodeDef; + /** The dataSource that is connected with the QuickFilter */ + dataSource?: DtQuickFilterDataSource; + /** Array of all active filters */ + filters: any[][]; +} + +/** @internal The initial QuickFilter state */ +export const initialState: QuickFilterState = { + filters: [], +}; + +/** Array of side effects */ +const effects: Effect[] = [switchDataSourceEffect]; + +/** + * The Quick Filter Store is one place where the state is handled. + * It is a minimal implementation of a redux like architecture to handle + * state in an immutable and on-directional way. + * + * This makes testing and debugging way easier, because there is always a + * clear state that can only be modified through actions. + * + * 1. Action gets dispatched (An action indicates a change in the store) + * 2. The reducer gets an action and the current state, and according to the action + * modifies the state. + * 3. A Selector can always read the latest value from the store and displays it in + * a template. So the only way to modify the state is dispatching an action. + * 4. If some async work has to be done the effect is responsible for that. + * Effects are listening for actions then doing some async work and dispatching some + * Other actions with the payload of the async stuff. + */ +class QuickFilterStore { + /** The current action that got dispatched */ + private readonly action$ = new BehaviorSubject({ + type: ActionType.INIT, + }); + + /** The current state that is present */ + private readonly state$: BehaviorSubject; + + constructor(reducer: Reducer, initialStoreState: QuickFilterState) { + this.state$ = new BehaviorSubject(initialStoreState); + + this.action$ + .pipe( + shareReplay(), + withLatestFrom(this.state$), + map(([action, state]) => reducer(state, action)), + ) + .subscribe(state => { + // Here the state gets modified through the outcome of the reducer + this.state$.next(state); + }); + + // Each effect will get the stream of actions and will dispatch other actions in return + // The emitted actions will be immediately dispatched through the normal store.dispatch() + merge(...effects.map(epic => epic(this.action$, this.state$))).subscribe( + (action: Action) => { + this.dispatch(action); + }, + ); + } + + /** Dispatch a new Action that modifies the store */ + dispatch(action: Action): void { + this.action$.next(action); + } + + /** Use a provided selector function to get a State out of the store */ + select(selector: (state$: Observable) => T): T { + return selector(this.state$); + } +} + +/** @internal This function creates the store for the quick filter */ +export function createQuickFilterStore(reducer: Reducer): QuickFilterStore { + return new QuickFilterStore(reducer, initialState); +} diff --git a/libs/barista-components/experimental/quick-filter/src/test-setup.ts b/libs/barista-components/experimental/quick-filter/src/test-setup.ts new file mode 100644 index 0000000000..6381bbc656 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/test-setup.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import 'jest-preset-angular'; +import '@angular/localize/init'; diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.json b/libs/barista-components/experimental/quick-filter/tsconfig.json new file mode 100644 index 0000000000..a6233e13a0 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "types": ["node", "jest"] + }, + "include": ["**/*.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.lib.json b/libs/barista-components/experimental/quick-filter/tsconfig.lib.json new file mode 100644 index 0000000000..b639cbf40a --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "target": "es2015", + "declaration": true, + "inlineSources": true, + "types": [], + "lib": ["dom", "es2018"] + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true, + "enableResourceInlining": true + }, + "exclude": ["src/test-setup.ts", "**/*.spec.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tsconfig.spec.json b/libs/barista-components/experimental/quick-filter/tsconfig.spec.json new file mode 100644 index 0000000000..aed68bc6bf --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/libs/barista-components/experimental/quick-filter/tslint.json b/libs/barista-components/experimental/quick-filter/tslint.json new file mode 100644 index 0000000000..b20a356378 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/tslint.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../tslint.json", + "rules": {} +} diff --git a/libs/barista-components/filter-field/index.ts b/libs/barista-components/filter-field/index.ts index 4f9b0f832f..30986293e2 100644 --- a/libs/barista-components/filter-field/index.ts +++ b/libs/barista-components/filter-field/index.ts @@ -22,7 +22,7 @@ export * from './src/filter-field-range/filter-field-range-trigger'; export * from './src/filter-field-data-source'; export * from './src/filter-field-default-data-source'; export * from './src/filter-field-errors'; - +export { applyDtOptionIds, DELIMITER } from './src/filter-field-util'; export { DtNodeFlags, DtNodeDef, @@ -43,4 +43,5 @@ export { isDtOptionDef, dtGroupDef, isDtGroupDef, + isDtRenderType, } from './src/types'; diff --git a/libs/barista-components/filter-field/src/filter-field-util.ts b/libs/barista-components/filter-field/src/filter-field-util.ts index 424dd6501a..f36c0b58d8 100644 --- a/libs/barista-components/filter-field/src/filter-field-util.ts +++ b/libs/barista-components/filter-field/src/filter-field-util.ts @@ -409,12 +409,15 @@ export function findDefForSource( return null; } -// Use an obscure Unicode character to delimit the words in the concatenated string. -// This avoids matches where the values of two columns combined will match the user's query -// (e.g. `Flute` and `Stop` will match `Test`). The character is intended to be something -// that has a very low chance of being typed in by somebody in a text field. This one in -// particular is "White up-pointing triangle with dot" from -// https://en.wikipedia.org/wiki/List_of_Unicode_characters +/** + * @internal + * Use an obscure Unicode character to delimit the words in the concatenated string. + * This avoids matches where the values of two columns combined will match the user's query + * (e.g. `Flute` and `Stop` will match `Test`). The character is intended to be something + * that has a very low chance of being typed in by somebody in a text field. This one in + * particular is "White up-pointing triangle with dot" from + * https://en.wikipedia.org/wiki/List_of_Unicode_characters + */ export const DELIMITER = '◬'; /** Peeks into a option node definition and returns its distinct id or creates a new one. */ diff --git a/libs/barista-components/filter-field/src/filter-field.spec.ts b/libs/barista-components/filter-field/src/filter-field.spec.ts index 68ebebbcc3..970c852e20 100644 --- a/libs/barista-components/filter-field/src/filter-field.spec.ts +++ b/libs/barista-components/filter-field/src/filter-field.spec.ts @@ -17,98 +17,42 @@ // tslint:disable no-lifecycle-call no-use-before-declare no-magic-numbers // tslint:disable no-any max-file-line-count no-unbound-method use-component-selector -import { BACKSPACE, ENTER, ESCAPE, DOWN_ARROW } from '@angular/cdk/keycodes'; +import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE } from '@angular/cdk/keycodes'; import { OverlayContainer } from '@angular/cdk/overlay'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component, DebugElement, NgZone, ViewChild } from '@angular/core'; import { ComponentFixture, - TestBed, fakeAsync, flush, inject, + TestBed, tick, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; - import { - DT_FILTER_FIELD_TYPING_DEBOUNCE, DtFilterField, DtFilterFieldChangeEvent, DtFilterFieldDefaultDataSource, DtFilterFieldModule, dtRangeDef, + DT_FILTER_FIELD_TYPING_DEBOUNCE, getDtFilterFieldRangeNoOperatorsError, - DtFilterFieldDefaultDataSourceType, } from '@dynatrace/barista-components/filter-field'; import { DtIconModule } from '@dynatrace/barista-components/icon'; - import { + createComponent, dispatchFakeEvent, dispatchKeyboardEvent, - createComponent, MockNgZone, typeInElement, wrappedErrorMessage, } from '@dynatrace/testing/browser'; - -const TEST_DATA = { - autocomplete: [ - { - name: 'AUT', - autocomplete: [ - { - name: 'Upper Austria', - distinct: true, - autocomplete: [ - { - name: 'Cities', - options: [{ name: 'Linz' }, { name: 'Wels' }, { name: 'Steyr' }], - }, - ], - }, - { - name: 'Vienna', - }, - ], - }, - { - name: 'USA', - autocomplete: [{ name: 'Los Angeles' }, { name: 'San Fran' }], - }, - { - name: 'Free', - suggestions: [], - validators: [], - }, - { - name: 'DE (async)', - async: true, - autocomplete: [{ name: 'Berlin' }], - }, - ], -}; - -const TEST_DATA_SINGLE_DISTINCT: DtFilterFieldDefaultDataSourceType = { - autocomplete: [ - { - name: 'AUT', - distinct: true, - autocomplete: [ - { - name: 'Vienna', - }, - { - name: 'Linz', - }, - ], - }, - ], -}; - -const TEST_DATA_SINGLE_OPTION = { - autocomplete: [{ name: 'option' }], -}; +import { + FILTER_FIELD_TEST_DATA_ASYNC, + FILTER_FIELD_TEST_DATA_SINGLE_DISTINCT, + FILTER_FIELD_TEST_DATA_SINGLE_OPTION, +} from '@dynatrace/testing/fixtures'; const TEST_DATA_SUGGESTIONS = { autocomplete: [ @@ -269,7 +213,7 @@ describe('DtFilterField', () => { it('should disable all tags if filter field is disabled', fakeAsync(() => { // given - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_DISTINCT; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_DISTINCT; fixture.detectChanges(); filterField.focus(); @@ -307,7 +251,7 @@ describe('DtFilterField', () => { it('should restore the previous state of tags if filter field gets enabled', fakeAsync(() => { // given - fixture.componentInstance.dataSource.data = TEST_DATA; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_ASYNC; fixture.detectChanges(); // Add filter "AUT - Vienna" @@ -621,7 +565,7 @@ describe('DtFilterField', () => { it('should emit filterChanges when adding an option', fakeAsync(() => { let filterChangeEvent: DtFilterFieldChangeEvent | undefined; - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_OPTION; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_OPTION; const sub = filterField.filterChanges.subscribe( ev => (filterChangeEvent = ev), ); @@ -648,7 +592,7 @@ describe('DtFilterField', () => { it('should emit filterChanges when removing an option', fakeAsync(() => { let filterChangeEvent: DtFilterFieldChangeEvent | undefined; - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_OPTION; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_OPTION; const sub = filterField.filterChanges.subscribe( ev => (filterChangeEvent = ev), ); @@ -823,7 +767,7 @@ describe('DtFilterField', () => { }); it('should show option again after adding all possible options and removing this option from the filters', () => { - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_DISTINCT; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_DISTINCT; fixture.detectChanges(); filterField.focus(); @@ -894,7 +838,7 @@ describe('DtFilterField', () => { }); it('should remove a parent from an autocomplete if it is distinct and an option has been selected', () => { - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_DISTINCT; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_DISTINCT; fixture.detectChanges(); filterField.focus(); zone.simulateMicrotasksEmpty(); @@ -1717,7 +1661,7 @@ describe('DtFilterField', () => { it('should emit a filterchange event when the edit of a range is completed', () => { let filterChangeEvent: DtFilterFieldChangeEvent | undefined; - fixture.componentInstance.dataSource.data = TEST_DATA_SINGLE_OPTION; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_SINGLE_OPTION; const sub = filterField.filterChanges.subscribe( ev => (filterChangeEvent = ev), ); @@ -2239,7 +2183,7 @@ describe('DtFilterField', () => { it('should not remove the current filter if the data is changed when the filterChanges event fires', () => { const filterChangesSubscription = filterField.filterChanges.subscribe( () => { - fixture.componentInstance.dataSource.data = TEST_DATA; + fixture.componentInstance.dataSource.data = FILTER_FIELD_TEST_DATA_ASYNC; }, ); @@ -2393,7 +2337,9 @@ function isClearAllVisible(fixture: ComponentFixture): boolean { }) export class TestApp { // tslint:disable-next-line:no-any - dataSource = new DtFilterFieldDefaultDataSource(TEST_DATA); + dataSource = new DtFilterFieldDefaultDataSource( + FILTER_FIELD_TEST_DATA_ASYNC, + ); label = 'Filter by'; clearAllLabel = 'Clear all'; diff --git a/libs/barista-components/filter-field/src/filter-field.ts b/libs/barista-components/filter-field/src/filter-field.ts index d4b67b42ef..dc311d5901 100644 --- a/libs/barista-components/filter-field/src/filter-field.ts +++ b/libs/barista-components/filter-field/src/filter-field.ts @@ -181,7 +181,7 @@ let currentlyOpenFilterField: DtFilterField | null = null; ]), ], }) -export class DtFilterField +export class DtFilterField implements CanDisable, AfterViewInit, OnDestroy, OnChanges { /** Label for the filter field (e.g. "Filter by"). Will be placed next to the filter icon. */ @Input() label = ''; diff --git a/libs/barista-components/style/font-styles.scss b/libs/barista-components/style/font-styles.scss index 89a4f16b9d..e6695e8e98 100644 --- a/libs/barista-components/style/font-styles.scss +++ b/libs/barista-components/style/font-styles.scss @@ -17,7 +17,6 @@ h2 { } h3 { - @include dt-h3-font(); } pre, diff --git a/libs/examples/src/examples.module.ts b/libs/examples/src/examples.module.ts index 46fd9a7118..d1ffc2f12d 100644 --- a/libs/examples/src/examples.module.ts +++ b/libs/examples/src/examples.module.ts @@ -60,6 +60,7 @@ import { DtOverlayExamplesModule } from './overlay/overlay-examples.module'; import { DtPaginationExamplesModule } from './pagination/pagination-examples.module'; import { DtProgressBarExamplesModule } from './progress-bar/progress-bar-examples.module'; import { DtProgressCircleExamplesModule } from './progress-circle/progress-circle-examples.module'; +import { DtQuickFilterExamplesModule } from './quick-filter/quick-filter-examples.module'; import { DtRadialChartExamplesModule } from './radial-chart/radial-chart-examples.module'; import { DtRadioExamplesModule } from './radio/radio-examples.module'; import { DtExamplesSecondaryNAvModule } from './secondary-nav/secondary-nav-examples.module'; @@ -122,6 +123,7 @@ import { DtExamplesTreeTableModule } from './tree-table/tree-table-examples.modu DtPaginationExamplesModule, DtProgressBarExamplesModule, DtProgressCircleExamplesModule, + DtQuickFilterExamplesModule, DtRadialChartExamplesModule, DtRadioExamplesModule, DtExamplesSecondaryNAvModule, diff --git a/libs/examples/src/index.ts b/libs/examples/src/index.ts index 70422e299b..78d79a9901 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -224,6 +224,7 @@ import { DtExampleProgressCircleDefault } from './progress-circle/progress-circl import { DtExampleProgressCircleWithColor } from './progress-circle/progress-circle-with-color-example/progress-circle-with-color-example'; import { DtExampleProgressCircleWithIcon } from './progress-circle/progress-circle-with-icon-example/progress-circle-with-icon-example'; import { DtExampleProgressCircleWithText } from './progress-circle/progress-circle-with-text-example/progress-circle-with-text-example'; +import { DtExampleQuickFilterDefault } from './quick-filter/quick-filter-default-example/quick-filter-default-example'; import { DtExampleRadialChartCustomColors } from './radial-chart/radial-chart-custom-colors-example/radial-chart-custom-colors-example'; import { DtExampleRadialChartDefaultDonut } from './radial-chart/radial-chart-default-donut-example/radial-chart-default-donut-example'; import { DtExampleRadialChartDefaultPie } from './radial-chart/radial-chart-default-pie-example/radial-chart-default-pie-example'; @@ -350,6 +351,7 @@ export { DtOverlayExamplesModule } from './overlay/overlay-examples.module'; export { DtPaginationExamplesModule } from './pagination/pagination-examples.module'; export { DtProgressBarExamplesModule } from './progress-bar/progress-bar-examples.module'; export { DtProgressCircleExamplesModule } from './progress-circle/progress-circle-examples.module'; +export { DtQuickFilterExamplesModule } from './quick-filter/quick-filter-examples.module'; export { DtRadialChartExamplesModule } from './radial-chart/radial-chart-examples.module'; export { DtRadioExamplesModule } from './radio/radio-examples.module'; export { DtExamplesSecondaryNAvModule } from './secondary-nav/secondary-nav-examples.module'; @@ -568,6 +570,7 @@ export { DtExampleProgressCircleWithColor, DtExampleProgressCircleWithIcon, DtExampleProgressCircleWithText, + DtExampleQuickFilterDefault, DtExampleRadialChartCustomColors, DtExampleRadialChartDefaultDonut, DtExampleRadialChartDefaultPie, @@ -904,6 +907,7 @@ export const EXAMPLES_MAP = new Map>([ ['DtExampleProgressCircleWithColor', DtExampleProgressCircleWithColor], ['DtExampleProgressCircleWithIcon', DtExampleProgressCircleWithIcon], ['DtExampleProgressCircleWithText', DtExampleProgressCircleWithText], + ['DtExampleQuickFilterDefault', DtExampleQuickFilterDefault], ['DtExampleRadialChartCustomColors', DtExampleRadialChartCustomColors], ['DtExampleRadialChartDefaultDonut', DtExampleRadialChartDefaultDonut], ['DtExampleRadialChartDefaultPie', DtExampleRadialChartDefaultPie], diff --git a/libs/examples/src/quick-filter/index.ts b/libs/examples/src/quick-filter/index.ts new file mode 100644 index 0000000000..113c95a6b4 --- /dev/null +++ b/libs/examples/src/quick-filter/index.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './quick-filter-default-example/quick-filter-default-example'; +export * from './quick-filter-examples.module'; diff --git a/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.html b/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.html new file mode 100644 index 0000000000..771d132a42 --- /dev/null +++ b/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.html @@ -0,0 +1,13 @@ + + Quick-filter + + All options in the filter field above + + + my content + diff --git a/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.ts b/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.ts new file mode 100644 index 0000000000..5bb770a01f --- /dev/null +++ b/libs/examples/src/quick-filter/quick-filter-default-example/quick-filter-default-example.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component } from '@angular/core'; +import { isObject } from '@dynatrace/barista-components/core'; +import { + DtQuickFilterDefaultDataSource, + DtQuickFilterDefaultDataSourceConfig, +} from '@dynatrace/barista-components/experimental/quick-filter'; +import { FILTER_FIELD_TEST_DATA as filterFieldData } from '@dynatrace/testing/fixtures'; + +@Component({ + selector: 'dt-example-quick-filter-default', + templateUrl: 'quick-filter-default-example.html', +}) +export class DtExampleQuickFilterDefault { + /** configuration for the quick filter */ + private _config: DtQuickFilterDefaultDataSourceConfig = { + // Method to decide if a node should be displayed in the quick filter + showInSidebar: node => + isObject(node) && node.name && node.name !== 'Not in Quickfilter', + }; + + _dataSource = new DtQuickFilterDefaultDataSource( + filterFieldData, + this._config, + ); +} diff --git a/libs/examples/src/quick-filter/quick-filter-examples.module.ts b/libs/examples/src/quick-filter/quick-filter-examples.module.ts new file mode 100644 index 0000000000..8c2417e457 --- /dev/null +++ b/libs/examples/src/quick-filter/quick-filter-examples.module.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { NgModule } from '@angular/core'; +import { DtQuickFilterModule } from '@dynatrace/barista-components/experimental/quick-filter'; +import { DtExampleQuickFilterDefault } from './quick-filter-default-example/quick-filter-default-example'; + +export const DT_QUICK_FILTER_EXAMPLES = [DtExampleQuickFilterDefault]; + +@NgModule({ + imports: [DtQuickFilterModule], + declarations: [...DT_QUICK_FILTER_EXAMPLES], + entryComponents: [...DT_QUICK_FILTER_EXAMPLES], +}) +export class DtQuickFilterExamplesModule {} diff --git a/libs/testing/fixtures/src/index.ts b/libs/testing/fixtures/src/index.ts index f1795bf364..de85075447 100644 --- a/libs/testing/fixtures/src/index.ts +++ b/libs/testing/fixtures/src/index.ts @@ -15,3 +15,6 @@ */ export * from './lib/chart'; +export * from './lib/filter-field/test-data'; +export * from './lib/filter-field/test-data-validators'; +export * from './lib/tree-table/test-data'; diff --git a/libs/testing/fixtures/src/lib/filter-field/test-data-validators.ts b/libs/testing/fixtures/src/lib/filter-field/test-data-validators.ts new file mode 100644 index 0000000000..139661e641 --- /dev/null +++ b/libs/testing/fixtures/src/lib/filter-field/test-data-validators.ts @@ -0,0 +1,61 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Validators } from '@angular/forms'; + +export const FILTER_FIELD_TEST_DATA_VALIDATORS = { + autocomplete: [ + { + name: 'custom normal', + suggestions: [], + }, + { + name: 'custom required', + suggestions: [], + validators: [ + { validatorFn: Validators.required, error: 'field is required' }, + ], + }, + { + name: 'custom with multiple', + suggestions: [], + validators: [ + { validatorFn: Validators.required, error: 'field is required' }, + { validatorFn: Validators.minLength(3), error: 'min 3 characters' }, + ], + }, + { + name: 'outer-option', + autocomplete: [ + { + name: 'inner-option', + }, + ], + }, + { + name: 'Autocomplete with free text options', + autocomplete: [ + { name: 'Autocomplete option 1' }, + { name: 'Autocomplete option 2' }, + { name: 'Autocomplete option 3' }, + { + name: 'Autocomplete free text', + suggestions: ['Suggestion 1', 'Suggestion 2', 'Suggestion 3'], + validators: [], + }, + ], + }, + ], +}; diff --git a/libs/testing/fixtures/src/lib/filter-field/test-data.ts b/libs/testing/fixtures/src/lib/filter-field/test-data.ts new file mode 100644 index 0000000000..a75237b258 --- /dev/null +++ b/libs/testing/fixtures/src/lib/filter-field/test-data.ts @@ -0,0 +1,112 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const FILTER_FIELD_TEST_DATA = { + autocomplete: [ + { + name: 'AUT', + distinct: true, + autocomplete: [{ name: 'Linz' }, { name: 'Vienna' }, { name: 'Graz' }], + }, + { + name: 'USA', + autocomplete: [ + { name: 'San Francisco' }, + { name: 'Los Angeles' }, + { name: 'New York' }, + { name: 'Custom', suggestions: [] }, + ], + }, + { + name: 'Requests per minute', + range: { + operators: { + range: true, + equal: true, + greaterThanEqual: true, + lessThanEqual: true, + }, + }, + unit: 's', + }, + { + name: 'Not in Quickfilter', + autocomplete: [ + { name: 'Option1' }, + { name: 'Option2' }, + { name: 'Option3' }, + ], + }, + ], +}; + +export const FILTER_FIELD_TEST_DATA_SINGLE_DISTINCT = { + autocomplete: [ + { + name: 'AUT', + distinct: true, + autocomplete: [ + { + name: 'Vienna', + }, + { + name: 'Linz', + }, + ], + }, + ], +}; + +export const FILTER_FIELD_TEST_DATA_SINGLE_OPTION = { + autocomplete: [{ name: 'option' }], +}; + +export const FILTER_FIELD_TEST_DATA_ASYNC = { + autocomplete: [ + { + name: 'AUT', + autocomplete: [ + { + name: 'Upper Austria', + distinct: true, + autocomplete: [ + { + name: 'Cities', + options: [{ name: 'Linz' }, { name: 'Wels' }, { name: 'Steyr' }], + }, + ], + }, + { + name: 'Vienna', + }, + ], + }, + { + name: 'USA', + autocomplete: [{ name: 'Los Angeles' }, { name: 'San Fran' }], + }, + { + name: 'Free', + suggestions: [], + validators: [], + }, + { + name: 'DE (async)', + async: true, + autocomplete: [{ name: 'Berlin' }], + }, + ], +}; diff --git a/libs/testing/fixtures/src/lib/tree-table/test-data.ts b/libs/testing/fixtures/src/lib/tree-table/test-data.ts new file mode 100644 index 0000000000..713cc5fbec --- /dev/null +++ b/libs/testing/fixtures/src/lib/tree-table/test-data.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright 2020 Dynatrace LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DtIconType } from '@dynatrace/barista-icons'; + +// tslint:disable: dt-document-public-fields + +export const TREE_TABLE_TEST_DATA: ThreadNode[] = [ + { + name: 'hz.hzInstance_1_cluster.thread', + icon: 'airplane', + threadlevel: 'S0', + totalTimeConsumption: 150, + waiting: 123, + running: 20, + blocked: 0, + children: [ + { + name: + 'hz.hzInstance_1_cluster.thread_1_hz.hzInstance_1_cluster.thread-1', + icon: 'airplane', + threadlevel: 'S1', + totalTimeConsumption: 150, + waiting: 123, + running: 20, + blocked: 0, + }, + { + name: 'hz.hzInstance_1_cluster.thread-2', + icon: 'airplane', + threadlevel: 'S1', + totalTimeConsumption: 150, + waiting: 130, + running: 0, + blocked: 0, + }, + ], + }, + { + name: 'jetty', + icon: 'airplane', + threadlevel: 'S0', + totalTimeConsumption: 150, + waiting: 123, + running: 20, + blocked: 0, + children: [ + { + name: 'jetty-422', + icon: 'airplane', + threadlevel: 'S1', + totalTimeConsumption: 150, + waiting: 123, + running: 20, + blocked: 0, + }, + { + name: 'jetty-423', + icon: 'airplane', + threadlevel: 'S1', + totalTimeConsumption: 150, + waiting: 130, + running: 0, + blocked: 0, + }, + { + name: 'jetty-424', + icon: 'airplane', + threadlevel: 'S1', + totalTimeConsumption: 150, + waiting: 130, + running: 0, + blocked: 0, + }, + ], + }, + { + name: 'Downtime timer', + icon: 'airplane', + threadlevel: 'S0', + totalTimeConsumption: 150, + waiting: 123, + running: 20, + blocked: 0, + }, +]; + +export class ThreadNode { + name: string; + threadlevel: string; + totalTimeConsumption: number; + blocked: number; + running: number; + waiting: number; + icon: DtIconType; + children?: ThreadNode[]; +} + +export class ThreadFlatNode { + name: string; + threadlevel: string; + totalTimeConsumption: number; + blocked: number; + running: number; + waiting: number; + icon: DtIconType; + level: number; + expandable: boolean; +} diff --git a/nx.json b/nx.json index fb9fe66c74..1e9cabdb89 100644 --- a/nx.json +++ b/nx.json @@ -245,6 +245,9 @@ "progress-circle": { "tags": ["scope:components", "type:library"] }, + "quick-filter": { + "tags": ["scope:components", "type:library"] + }, "radial-chart": { "tags": ["scope:components", "type:library"] }, diff --git a/tsconfig.json b/tsconfig.json index f2aa5db6d8..bd4807a031 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -86,6 +86,9 @@ "@dynatrace/barista-components/expandable-text": [ "libs/barista-components/expandable-text/index.ts" ], + "@dynatrace/barista-components/experimental/quick-filter": [ + "libs/barista-components/experimental/quick-filter/index.ts" + ], "@dynatrace/barista-components/filter-field": [ "libs/barista-components/filter-field/index.ts" ], diff --git a/tslint.json b/tslint.json index 4b948bd320..c60546feb5 100644 --- a/tslint.json +++ b/tslint.json @@ -76,9 +76,13 @@ "sourceTag": "type:app", "onlyDependOnLibsWithTags": ["type:library"] }, + { + "sourceTag": "type:e2e", + "onlyDependOnLibsWithTags": ["type:library"] + }, { "sourceTag": "scope:examples", - "onlyDependOnLibsWithTags": ["scope:components"] + "onlyDependOnLibsWithTags": ["scope:components", "scope:testing"] } ] }