diff --git a/.vscode/settings.json b/.vscode/settings.json index 561c8e081c..3a5d8abc49 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.tabSize": 2, "files.eol": "\n", "editor.defaultFormatter": "esbenp.prettier-vscode", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "explorer.compactFolders": false } 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 f1622ff148..63f02a50b4 100644 --- a/angular.json +++ b/angular.json @@ -2522,6 +2522,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 92c799dd15..8e837d1fff 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 @@ -47,7 +47,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) @@ -56,12 +56,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) @@ -173,7 +171,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'); @@ -190,7 +188,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 fb639ad4f8..dacc2de8ca 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 @@ -23,6 +23,12 @@ export const clearAll = Selector('.dt-filter-field-clear-all-button'); export const filterTags = Selector('dt-filter-field-tag'); export const tagOverlay = Selector('.dt-overlay-container'); +/** 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..933af38a59 --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter-initial-data/quick-filter-initial-data.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 { 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], + ], + ]; + + constructor() { + console.log(this._initialFilters); + } +} 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..d533744f6a --- /dev/null +++ b/apps/components-e2e/src/components/quick-filter/quick-filter.e2e.ts @@ -0,0 +1,157 @@ +/** + * @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'; + +fixture('Quick Filter') + .page('http://localhost:4200/quick-filter') + .meta({ + 'filter-field': true, + 'quick-filter': true, + drawer: true, + checkbox: true, + radio: true, + }) + .beforeEach(async (testController: TestController) => { + await testController.resizeWindow(1200, 800); + 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 friedl 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 (testController: TestController) => { + await testController.resizeWindow(1200, 800); + await waitForAngular(); + }); + +test('if the initial filter in the filter field reflects 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('if a distinct group get set to any 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..85a632d8b8 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/README.md @@ -0,0 +1,49 @@ +# Quick Filter (experimental) + +Note: This component is still experimental, use with caution! 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 MyModule {} +``` + +To use the quick filter in your template there is the +`` where you have to bind the data +source. Every content that will be placed inside the quick filter tag is the +content for the sidebar. 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. | + +## 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..d598b639f5 --- /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..e82da3850d --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-data-source.ts @@ -0,0 +1,95 @@ +/** + * @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; + + 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..9dfd8433ad --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.html @@ -0,0 +1,34 @@ +

{{ _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..b7e9db03b3 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.scss @@ -0,0 +1,22 @@ +@import '../../../core/src/style/variables'; + +:host { + display: block; + padding: 8px 0; +} + +.dt-quick-filter-group-headline { + margin: 0 0 8px; + font-size: 18px; + font-weight: normal; + color: $gray-700; +} + +.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..9529f3bd57 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter-group.ts @@ -0,0 +1,141 @@ +/** + * @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 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 de select 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 options 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 && this._nodeDef.autocomplete.distinct + ); + } + + /** @internal Helper function that returns safely the viewValue of a nodeDef */ + _getViewValue(nodeDef: DtNodeDef): string { + return nodeDef && 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 && 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..955b9931c7 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.html @@ -0,0 +1,46 @@ + + + + + + +

+ +

+

+ +

+ + +
+ +
+ + +
+
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..6ef02c51bd --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.scss @@ -0,0 +1,67 @@ +@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-radius: 3px; +} + +.dt-drawer { + background-color: $gray-130; + position: relative; + padding-right: 26px; +} + +.dt-quick-filter-title { + font-size: 20px; + color: $gray-700; + margin: 0; +} + +.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 { + top: 8px; + background: white; + border-radius: 3px; + position: absolute; + color: $turquoise-600; + font-size: 1.2em; + cursor: pointer; +} + +.dt-quick-filter-open { + left: -4px; + padding: 6px 6px 6px 0; + border-left: none; + background-color: $gray-130; +} + +.dt-quick-filter-close { + right: -2px; + padding: 6px 0 6px 6px; + border-right: none; +} 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..6975dc5b19 --- /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..f3ff3a07e6 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/quick-filter.ts @@ -0,0 +1,170 @@ +/** + * @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 { 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 } from './state/store'; + +/** Directive that is used to place a title inside the quick filters side bar */ +@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 side bar */ +@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 orchestrated 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() + 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 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..e9ce277c4d --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/actions.ts @@ -0,0 +1,62 @@ +/** + * @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'; + +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, +}); + +export const setFilters = (filters: any[][]) => + action(ActionType.SET_FILTERS, filters); + +export const unsetFilterGroup = (group: DtNodeDef) => + action(ActionType.UNSET_FILTER_GROUP, group); + +export const addFilter = (item: DtNodeDef) => + action(ActionType.ADD_FILTER, item); + +export const removeFilter = (item: DtNodeDef) => + action(ActionType.REMOVE_FILTER, item); + +export const updateFilter = (item: DtNodeDef) => + action(ActionType.UPDATE_FILTER, item); + +export const switchDataSource = (item: DtFilterFieldDataSource) => + action(ActionType.SWITCH_DATA_SOURCE, item); + +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..3d4cda5067 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/effects.ts @@ -0,0 +1,46 @@ +/** + * @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 => { + return 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..a234d379a0 --- /dev/null +++ b/libs/barista-components/experimental/quick-filter/src/state/reducer.ts @@ -0,0 +1,153 @@ +/** + * @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 immutable 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 Add a filter to the filters array */ +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) { + const parts = item.option.uid.split(DELIMITER); + + if (distinct && parts[0] === path[0].name) { + return true; + } + + const dataPath = path.reduce( + (previousValue, currentValue) => + `${previousValue.name}${DELIMITER}${currentValue.name}${DELIMITER}`, + ); + + 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..9bc18159fd --- /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 out of 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..221a1e65d9 --- /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 54c11d43fb..0a093e3408 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), ); @@ -777,7 +721,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(); @@ -848,7 +792,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(); @@ -1671,7 +1615,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), ); @@ -2193,7 +2137,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; }, ); @@ -2347,7 +2291,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 fc7cfc2977..dff8d980ee 100644 --- a/libs/barista-components/filter-field/src/filter-field.ts +++ b/libs/barista-components/filter-field/src/filter-field.ts @@ -177,7 +177,7 @@ export const DT_FILTER_FIELD_TYPING_DEBOUNCE = 200; ]), ], }) -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/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 759773ae32..1645534b81 100644 --- a/libs/examples/src/index.ts +++ b/libs/examples/src/index.ts @@ -221,6 +221,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'; @@ -347,6 +348,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'; @@ -562,6 +564,7 @@ export { DtExampleProgressCircleWithColor, DtExampleProgressCircleWithIcon, DtExampleProgressCircleWithText, + DtExampleQuickFilterDefault, DtExampleRadialChartCustomColors, DtExampleRadialChartDefaultDonut, DtExampleRadialChartDefaultPie, @@ -895,6 +898,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/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 f26dd02371..02f1d1aeac 100644 --- a/nx.json +++ b/nx.json @@ -242,6 +242,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 0bff269c2a..86dc245651 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"] } ] }