mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-30 03:18:24 +00:00
Facets: Docs and a little cleanup
This commit is contained in:
parent
5ca0e831ef
commit
fa04b2cd95
7
wherehows-web/app/components/get-yield.ts
Normal file
7
wherehows-web/app/components/get-yield.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import Component from '@ember/component';
|
||||
|
||||
/**
|
||||
* Small component that will simply yield get a property of an object
|
||||
* to improve hbs ergonomics.
|
||||
*/
|
||||
export default class GetYield extends Component.extend({ tagName: '' }) {}
|
||||
@ -7,17 +7,30 @@ import { DatasetPlatform } from 'wherehows-web/constants';
|
||||
import { IDataPlatform } from 'wherehows-web/typings/api/list/platforms';
|
||||
import { readPlatforms } from 'wherehows-web/utils/api/list/platforms';
|
||||
import { arrayMap } from 'wherehows-web/utils/array';
|
||||
import { IFacetsSelectionsMap, IFacetsCounts } from 'wherehows-web/utils/api/search';
|
||||
|
||||
/**
|
||||
* Describes the interface of the object passed as radio option for source
|
||||
* @interface ISearchSourceOption
|
||||
* Options inside a facet
|
||||
* @interface ISearchFacetOption
|
||||
*/
|
||||
interface ISearchSourceOption {
|
||||
interface ISearchFacetOption {
|
||||
value: string;
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface of a facet
|
||||
*/
|
||||
interface ISearchFacet {
|
||||
name: string;
|
||||
displayName: string;
|
||||
values: Array<ISearchFacetOption>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container component for search facets
|
||||
* It will store state related to search facets.
|
||||
*/
|
||||
export default class SearchFacetsContainer extends Component {
|
||||
didInsertElement(this: SearchFacetsContainer) {
|
||||
get(this, 'getPlatformsTask').perform();
|
||||
@ -36,8 +49,26 @@ export default class SearchFacetsContainer extends Component {
|
||||
*/
|
||||
_sources: Array<DatasetPlatform> = [];
|
||||
|
||||
selections: any;
|
||||
model: any;
|
||||
/**
|
||||
* Current state of selections of facets
|
||||
* EI:
|
||||
* {
|
||||
* source: {
|
||||
* hdfs: true
|
||||
* },
|
||||
* fabric: {
|
||||
* corp: true,
|
||||
* prod: true
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
selections: IFacetsSelectionsMap;
|
||||
|
||||
/**
|
||||
* Counts for the facets in a similar fashion of selections
|
||||
*/
|
||||
counts: IFacetsCounts;
|
||||
|
||||
/**
|
||||
* Gets the available platforms and extracts a list of dataset sources
|
||||
* @type {(Task<Promise<Array<IDataPlatform>>, (a?: any) => TaskInstance<Promise<Array<IDataPlatform>>>>)}
|
||||
@ -50,49 +81,38 @@ export default class SearchFacetsContainer extends Component {
|
||||
get(this, '_sources').setObjects(dataPlatforms);
|
||||
});
|
||||
|
||||
/**
|
||||
* I will convert a string into a facet option with counts
|
||||
* @param facetValue
|
||||
* @param facetName
|
||||
*/
|
||||
stringToFacetOption(facetValue: string): ISearchFacetOption {
|
||||
return {
|
||||
value: facetValue,
|
||||
label: capitalize(facetValue)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of options with radio props for the data platforms that can be selected as a search filter
|
||||
* @type {(ComputedProperty<Array<ISearchSourceOption>>}
|
||||
*/
|
||||
@computed('_sources.[]', 'model')
|
||||
get sources(this: SearchFacetsContainer): Array<ISearchSourceOption> {
|
||||
return this._sources.map(
|
||||
(source: DatasetPlatform): ISearchSourceOption => ({
|
||||
value: source,
|
||||
label: capitalize(source),
|
||||
count: this.model.groupbysource[source] || 0
|
||||
})
|
||||
);
|
||||
@computed('_sources.[]', 'counts')
|
||||
get sources(this: SearchFacetsContainer): Array<ISearchFacetOption> {
|
||||
return this._sources.map(source => this.stringToFacetOption(source));
|
||||
}
|
||||
|
||||
@computed('sources')
|
||||
get facets() {
|
||||
/**
|
||||
* Facets that are available right now.
|
||||
* In the future, it should be fetched from the backend
|
||||
*/
|
||||
@computed('sources', 'counts')
|
||||
get facets(): Array<ISearchFacet> {
|
||||
return [
|
||||
{
|
||||
name: 'fabric',
|
||||
displayName: 'Fabrics',
|
||||
values: [
|
||||
{
|
||||
value: 'prod',
|
||||
label: 'Prod',
|
||||
count: this.model.groupbyfabric.prod || 0
|
||||
},
|
||||
{
|
||||
value: 'corp',
|
||||
label: 'Corp',
|
||||
count: this.model.groupbyfabric.corp || 0
|
||||
},
|
||||
{
|
||||
value: 'ei',
|
||||
label: 'EI',
|
||||
count: this.model.groupbyfabric.ei || 0
|
||||
},
|
||||
{
|
||||
value: 'dev',
|
||||
label: 'Dev',
|
||||
count: this.model.groupbyfabric.dev || 0
|
||||
}
|
||||
]
|
||||
values: ['prod', 'corp', 'ei', 'dev'].map(fabric => this.stringToFacetOption(fabric))
|
||||
},
|
||||
{
|
||||
name: 'source',
|
||||
@ -102,11 +122,21 @@ export default class SearchFacetsContainer extends Component {
|
||||
];
|
||||
}
|
||||
|
||||
onFacetsChange(_: any) {
|
||||
/**
|
||||
* External closure action that triggers when facet changes
|
||||
* @param _ Facet Selections
|
||||
*/
|
||||
onFacetsChange(_: IFacetsSelectionsMap) {
|
||||
//nothing
|
||||
}
|
||||
|
||||
onFacetChange(facet: any, facetValue: any) {
|
||||
/**
|
||||
* Internal action triggered when facet changes. It will update
|
||||
* the state of selections in a redux fashion.
|
||||
* @param facet The facet that changed
|
||||
* @param facetValue the option of the facet that changed
|
||||
*/
|
||||
onFacetChange(facet: ISearchFacet, facetValue: ISearchFacetOption) {
|
||||
const currentFacetValues = this.selections[facet.name] || {};
|
||||
this.set('selections', {
|
||||
...this.selections,
|
||||
@ -118,6 +148,10 @@ export default class SearchFacetsContainer extends Component {
|
||||
this.onFacetsChange(this.selections);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user clear the facet
|
||||
* @param facet the facet that the user selects
|
||||
*/
|
||||
onFacetClear(facet: any) {
|
||||
this.set('selections', {
|
||||
...this.selections,
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember-decorators/object';
|
||||
import { debounce } from '@ember-decorators/runloop';
|
||||
import { IFacetsSelectionsMap, facetToParamUrl, facetFromParamUrl } from 'wherehows-web/utils/api/search';
|
||||
import {
|
||||
IFacetsSelectionsMap,
|
||||
facetToParamUrl,
|
||||
facetFromParamUrl,
|
||||
facetToDynamicCounts,
|
||||
IFacetsCounts
|
||||
} from 'wherehows-web/utils/api/search';
|
||||
|
||||
// gradual refactor into es class, hence extends EmberObject instance
|
||||
export default class SearchController extends Controller {
|
||||
@ -31,13 +37,28 @@ export default class SearchController extends Controller {
|
||||
*/
|
||||
header = 'Refine By';
|
||||
|
||||
/**
|
||||
* Since the loading of search comes from two parts:
|
||||
* 1. Search Call
|
||||
* 2. During debouncing
|
||||
*
|
||||
* We put it as a flag to control it better
|
||||
*/
|
||||
searchLoading: boolean = false;
|
||||
|
||||
/**
|
||||
* When facets change we set the flag loading and call the debounced fn
|
||||
* @param selections facet selections
|
||||
*/
|
||||
onFacetsChange(selections: IFacetsSelectionsMap) {
|
||||
this.set('searchLoading', true);
|
||||
this.onFacetsChangeDebounced(selections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will set the facets in the URL to start a model refresh (see route)
|
||||
* @param selections Facet selections
|
||||
*/
|
||||
@debounce(1000)
|
||||
onFacetsChangeDebounced(selections: IFacetsSelectionsMap) {
|
||||
this.setProperties({
|
||||
@ -46,10 +67,27 @@ export default class SearchController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will translate backend fields into a dynamic facet
|
||||
* count structure.
|
||||
*/
|
||||
@computed('model')
|
||||
get facetCounts(): IFacetsCounts {
|
||||
return facetToDynamicCounts(this.model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will read selections from URL and translate it into
|
||||
* our selections object
|
||||
*/
|
||||
@computed('facets')
|
||||
get facetsSelections() {
|
||||
get facetsSelections(): IFacetsSelectionsMap {
|
||||
return facetFromParamUrl(this.facets || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return false if there is data to display
|
||||
*/
|
||||
@computed('model.data.length')
|
||||
get showNoResult() {
|
||||
return this.model.data ? this.model.data.length === 0 : true;
|
||||
|
||||
@ -24,6 +24,10 @@
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__option--empty {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] + label.search-facet__checkbox-wrapper {
|
||||
|
||||
5
wherehows-web/app/templates/components/get-yield.hbs
Normal file
5
wherehows-web/app/templates/components/get-yield.hbs
Normal file
@ -0,0 +1,5 @@
|
||||
{{#if (get object property)}}
|
||||
{{yield (get object property)}}
|
||||
{{else}}
|
||||
{{yield default}}
|
||||
{{/if}}
|
||||
@ -1,5 +1,5 @@
|
||||
{{#if getPlatformsTask.isRunning}}
|
||||
{{pendulum-ellipsis-animation}}
|
||||
{{else}}
|
||||
{{yield facets selections (action onFacetChange) (action onFacetClear)}}
|
||||
{{yield facets selections counts (action onFacetChange) (action onFacetClear)}}
|
||||
{{/if}}
|
||||
|
||||
@ -10,19 +10,23 @@
|
||||
{{/if}}
|
||||
</section>
|
||||
{{#each facet.values as |facetValue|}}
|
||||
<div class="search-facet__option">
|
||||
{{input
|
||||
class="search-facet__checkbox"
|
||||
type="checkbox"
|
||||
name="facet.name"
|
||||
id=facetValue.value
|
||||
checked=(readonly (get selections facetValue.value))
|
||||
change=(action onChange facet facetValue)
|
||||
}}
|
||||
<label for="{{facetValue.value}}" class="search-facet__checkbox-wrapper">
|
||||
<span class="search-facet__label">{{facetValue.label}}</span>
|
||||
<span class="search-facet__count">{{facetValue.count}}</span>
|
||||
</label>
|
||||
</div>
|
||||
{{#get-yield object=counts property=facetValue.value default=0 as |count|}}
|
||||
{{#get-yield object=selections property=facetValue.value as |checked|}}
|
||||
<div class="search-facet__option {{if (lt count 1) 'search-facet__option--empty'}}">
|
||||
{{input
|
||||
class="search-facet__checkbox"
|
||||
type="checkbox"
|
||||
name="facet.name"
|
||||
id=facetValue.value
|
||||
checked=(readonly checked)
|
||||
change=(action onChange facet facetValue)
|
||||
}}
|
||||
<label for="{{facetValue.value}}" class="search-facet__checkbox-wrapper">
|
||||
<span class="search-facet__label">{{facetValue.label}}</span>
|
||||
<span class="search-facet__count">{{readonly count}}</span>
|
||||
</label>
|
||||
</div>
|
||||
{{/get-yield}}
|
||||
{{/get-yield}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -2,14 +2,15 @@
|
||||
<div class="col-md-3 wh-sidebar">
|
||||
|
||||
{{#search/containers/search-facets
|
||||
model=model
|
||||
counts=facetCounts
|
||||
selections=(readonly facetsSelections)
|
||||
onFacetsChange=(action onFacetsChange) as |searchFacet selections onFacetChange onFacetClear|}}
|
||||
onFacetsChange=(action onFacetsChange) as |searchFacet selections counts onFacetChange onFacetClear|}}
|
||||
|
||||
{{#each searchFacet as |facet|}}
|
||||
{{search/search-facet
|
||||
facet=facet
|
||||
selections=(get selections facet.name)
|
||||
counts=(get counts facet.name)
|
||||
onChange=(action onFacetChange)
|
||||
onClear=(action onFacetClear)}}
|
||||
{{/each}}
|
||||
|
||||
@ -3,6 +3,9 @@ import buildUrl from 'wherehows-web/utils/build-url';
|
||||
import { getJSON } from 'wherehows-web/utils/api/fetcher';
|
||||
import { toRestli, fromRestli } from 'restliparams';
|
||||
|
||||
/**
|
||||
* Backend search expected parameters
|
||||
*/
|
||||
export interface ISearchApiParams {
|
||||
keyword: string;
|
||||
category: string;
|
||||
@ -11,26 +14,78 @@ export interface ISearchApiParams {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backend search expected response
|
||||
*/
|
||||
export interface ISearchResponse {
|
||||
status: ApiStatus;
|
||||
result: {
|
||||
keywords: string;
|
||||
data: Array<any>;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic facet selections
|
||||
*/
|
||||
export interface IFacetSelections {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic facets:
|
||||
* {
|
||||
* source: {
|
||||
* hdfs: true,
|
||||
* hive: true
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export interface IFacetsSelectionsMap {
|
||||
[key: string]: IFacetSelections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compressed version of selection to put it in a url
|
||||
* {
|
||||
* source: ['hdfs', 'hive]
|
||||
* }
|
||||
*/
|
||||
export interface IFacetsSelectionsArray {
|
||||
[key: string]: Array<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic counts facet option
|
||||
*/
|
||||
export interface IFacetCounts {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic counts facet similar to selections
|
||||
*/
|
||||
export interface IFacetsCounts {
|
||||
[key: string]: IFacetCounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert backend static structure into a dynamic facet count structure
|
||||
*/
|
||||
export const facetToDynamicCounts = (result: ISearchResponse['result']): IFacetsCounts => {
|
||||
return Object.keys(result).reduce((counts: IFacetsCounts, key) => {
|
||||
if (key.indexOf('groupby') === 0) {
|
||||
counts[key.replace('groupby', '')] = result[key];
|
||||
}
|
||||
return counts;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts IFacetsSelectionsMap into IFacetsSelectionsArray
|
||||
* @param selections
|
||||
*/
|
||||
export const toFacetSelectionsArray = (selections: IFacetsSelectionsMap): IFacetsSelectionsArray =>
|
||||
Object.keys(selections).reduce((newSelections: any, key) => {
|
||||
const selectionsArray = Object.keys(selections[key]).reduce((arr, selKey) => {
|
||||
@ -45,6 +100,10 @@ export const toFacetSelectionsArray = (selections: IFacetsSelectionsMap): IFacet
|
||||
return newSelections;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Converts IFacetsSelectionsArray into IFacetsSelectionsMap
|
||||
* @param selections
|
||||
*/
|
||||
export const toFacetSelectionsMap = (selections: IFacetsSelectionsArray): IFacetsSelectionsMap =>
|
||||
Object.keys(selections).reduce((newSelections: any, key) => {
|
||||
newSelections[key] = selections[key].reduce((obj: any, selKey: string) => {
|
||||
@ -54,16 +113,30 @@ export const toFacetSelectionsMap = (selections: IFacetsSelectionsArray): IFacet
|
||||
return newSelections;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Transform IFacetsSelectionsMap into this string: (source:List(hive, hdfs))
|
||||
* @param selections
|
||||
*/
|
||||
export const facetToParamUrl = (selections: IFacetsSelectionsMap) => {
|
||||
return toRestli(toFacetSelectionsArray(selections));
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform (source:List(hive, hdfs)) into IFacetsSelectionsMap
|
||||
* @param selections
|
||||
*/
|
||||
export const facetFromParamUrl = (value: string = '') => {
|
||||
return toFacetSelectionsMap(fromRestli(value));
|
||||
};
|
||||
|
||||
/**
|
||||
* Build search url
|
||||
*/
|
||||
export const searchUrl = ({ facets, ...params }: ISearchApiParams): string => {
|
||||
return buildUrl(`${getApiRoot()}/search`, { ...params, ...fromRestli(facets || '') });
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch Search from API
|
||||
*/
|
||||
export const readSearch = (params: ISearchApiParams) => getJSON<ISearchResponse>({ url: searchUrl(params) });
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user