Facets: Docs and a little cleanup

This commit is contained in:
Ignacio Bona 2018-09-14 16:40:45 -07:00
parent 5ca0e831ef
commit fa04b2cd95
9 changed files with 226 additions and 60 deletions

View 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: '' }) {}

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -0,0 +1,5 @@
{{#if (get object property)}}
{{yield (get object property)}}
{{else}}
{{yield default}}
{{/if}}

View File

@ -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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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) });