feat(ui) - assist filters
This commit is contained in:
parent
5eeea45d63
commit
0584883a1b
20 changed files with 430 additions and 58 deletions
|
|
@ -27,6 +27,7 @@ import LiveSessionList from './LiveSessionList'
|
|||
import SessionSearch from 'Shared/SessionSearch';
|
||||
import MainSearchBar from 'Shared/MainSearchBar';
|
||||
import LiveSearchBar from 'Shared/LiveSearchBar';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
|
||||
const weakEqual = (val1, val2) => {
|
||||
|
|
@ -186,7 +187,7 @@ export default class BugFinder extends React.PureComponent {
|
|||
<>
|
||||
<div className="mb-5">
|
||||
<LiveSearchBar />
|
||||
<SessionSearch />
|
||||
<LiveSessionSearch />
|
||||
</div>
|
||||
{ activeTab.type === 'live' && <LiveSessionList /> }
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Loader, NoContent, Button, LoadMoreButton } from 'UI';
|
||||
import { applyFilter, addAttribute, addEvent } from 'Duck/filters';
|
||||
import { fetchSessions } from 'Duck/search';
|
||||
import { fetchSessions, addFilterByKeyAndValue } from 'Duck/search';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import SessionListHeader from './SessionListHeader';
|
||||
import { addFilterByKeyAndValue } from 'Duck/search';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
|
||||
const ALL = 'all';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Icon, Loader } from 'UI';
|
|||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import stl from './FilterModal.css';
|
||||
import { filtersMap, getMetaDataFilter } from 'Types/filter/newFilter';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
|
||||
interface Props {
|
||||
filters: any,
|
||||
|
|
@ -26,17 +26,6 @@ function FilterModal(props: Props) {
|
|||
} = props;
|
||||
const hasSearchQuery = searchQuery && searchQuery.length > 0;
|
||||
const showSearchList = isMainSearch && searchQuery.length > 0;
|
||||
|
||||
const allFilters = Object.assign({}, filters);
|
||||
if (metaOptions.size > 0) {
|
||||
allFilters['Metadata'] = [];
|
||||
metaOptions.forEach((option) => {
|
||||
if (option.key) {
|
||||
const _metaFilter = getMetaDataFilter(option.key, option.value);
|
||||
allFilters['Metadata'].push(_metaFilter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const onFilterSearchClick = (filter) => {
|
||||
const _filter = filtersMap[filter.type];
|
||||
|
|
@ -79,11 +68,11 @@ function FilterModal(props: Props) {
|
|||
|
||||
{ !hasSearchQuery && (
|
||||
<div className="" style={{ columns: "100px 2" }}>
|
||||
{allFilters && Object.keys(allFilters).map((key) => (
|
||||
{filters && Object.keys(filters).map((key) => (
|
||||
<div className="mb-6" key={key}>
|
||||
<div className="uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm">{key}</div>
|
||||
<div>
|
||||
{allFilters[key].map((filter: any) => (
|
||||
{filters[key].map((filter: any) => (
|
||||
<div key={filter.label} className={cn(stl.optionItem, "flex items-center py-2 cursor-pointer -mx-2 px-2")} onClick={() => onFilterClick(filter)}>
|
||||
<Icon name={filter.icon} size="16"/>
|
||||
<span className="ml-2">{filter.label}</span>
|
||||
|
|
@ -99,7 +88,7 @@ function FilterModal(props: Props) {
|
|||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filters: state.getIn([ 'filters', 'filterList' ]),
|
||||
filters: state.getIn([ 'search', 'filterList' ]),
|
||||
filterSearchList: state.getIn([ 'search', 'filterSearchList' ]),
|
||||
metaOptions: state.getIn([ 'customFields', 'list' ]),
|
||||
fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]),
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import React, { useState } from 'react';
|
||||
import FilterModal from '../FilterModal';
|
||||
import LiveFilterModal from '../LiveFilterModal';
|
||||
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
|
||||
import { Icon } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
filter: any; // event/filter
|
||||
onFilterClick: (filter) => void;
|
||||
children?: any;
|
||||
isLive?: boolean;
|
||||
}
|
||||
function FilterSelection(props: Props) {
|
||||
const { filter, onFilterClick, children } = props;
|
||||
const { filter, onFilterClick, children, isLive = true } = props;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
|
|
@ -37,11 +40,13 @@ function FilterSelection(props: Props) {
|
|||
</OutsideClickDetectingDiv>
|
||||
{showModal && (
|
||||
<div className="absolute left-0 top-20 border shadow rounded bg-white z-50">
|
||||
<FilterModal onFilterClick={onFilterClick} />
|
||||
{ isLive ? <LiveFilterModal onFilterClick={onFilterClick} /> : <FilterModal onFilterClick={onFilterClick} /> }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilterSelection;
|
||||
export default connect(state => ({
|
||||
isLive: state.getIn([ 'sessions', 'activeTab' ]).type === 'live',
|
||||
}), { })(FilterSelection);
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
.wrapper {
|
||||
border-radius: 3px;
|
||||
border: solid thin $gray-light;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 2px 0 $gray-light;
|
||||
}
|
||||
.optionItem {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
color: $teal !important;
|
||||
& svg {
|
||||
fill: $teal !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filterSearchItem {
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
color: $teal;
|
||||
|
||||
& svg {
|
||||
fill: $teal;
|
||||
}
|
||||
}
|
||||
|
||||
& div {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import React from 'react';
|
||||
import { Icon, Loader } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import stl from './LiveFilterModal.css';
|
||||
import { filtersMap, getMetaDataFilter } from 'Types/filter/newFilter';
|
||||
import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
|
||||
|
||||
interface Props {
|
||||
filters: any,
|
||||
onFilterClick?: (filter) => void,
|
||||
filterSearchList: any,
|
||||
metaOptions: any,
|
||||
isMainSearch?: boolean,
|
||||
fetchingFilterSearchList: boolean,
|
||||
searchQuery?: string,
|
||||
}
|
||||
function LiveFilterModal(props: Props) {
|
||||
const {
|
||||
filters,
|
||||
metaOptions,
|
||||
onFilterClick = () => null,
|
||||
filterSearchList,
|
||||
isMainSearch = false,
|
||||
fetchingFilterSearchList,
|
||||
searchQuery = '',
|
||||
} = props;
|
||||
const hasSearchQuery = searchQuery && searchQuery.length > 0;
|
||||
const showSearchList = isMainSearch && searchQuery.length > 0;
|
||||
|
||||
const onFilterSearchClick = (filter) => {
|
||||
const _filter = filtersMap[filter.type];
|
||||
_filter.value = [filter.value];
|
||||
onFilterClick(_filter);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper} style={{ width: '490px', maxHeight: '400px', overflowY: 'auto'}}>
|
||||
{ showSearchList && (
|
||||
<Loader size="small" loading={fetchingFilterSearchList}>
|
||||
<div className="-mx-6 px-6">
|
||||
{ filterSearchList && Object.keys(filterSearchList).map((key, index) => {
|
||||
const filter = filterSearchList[key];
|
||||
const option = filtersMap[key];
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={cn('mb-3')}
|
||||
>
|
||||
<div className="font-medium uppercase color-gray-medium text-sm mb-2">{option.label}</div>
|
||||
<div>
|
||||
{filter.map((f, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(stl.filterSearchItem, "cursor-pointer px-3 py-1 text-sm flex items-center")}
|
||||
onClick={() => onFilterSearchClick({ type: key, value: f.value })}
|
||||
>
|
||||
<Icon className="mr-2" name={option.icon} size="16" />
|
||||
<div className="whitespace-nowrap text-ellipsis overflow-hidden">{f.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
|
||||
{ !hasSearchQuery && (
|
||||
<div className="">
|
||||
{filters && Object.keys(filters).filter(i => i === 'User' || i === 'Metadata').map((key) => (
|
||||
<div className="mb-6" key={key}>
|
||||
<div className="uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm">{key}</div>
|
||||
<div>
|
||||
{filters[key].filter((i: any) => i.key === FilterKey.USERID || i.key === FilterKey.USERANONYMOUSID || i.category === FilterCategory.METADATA).map((filter: any) => (
|
||||
<div key={filter.label} className={cn(stl.optionItem, "flex items-center py-2 cursor-pointer -mx-2 px-2")} onClick={() => onFilterClick(filter)}>
|
||||
<Icon name={filter.icon} size="16"/>
|
||||
<span className="ml-2">{filter.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filters: state.getIn([ 'search', 'filterList' ]),
|
||||
filterSearchList: state.getIn([ 'search', 'filterSearchList' ]),
|
||||
metaOptions: state.getIn([ 'customFields', 'list' ]),
|
||||
fetchingFilterSearchList: state.getIn([ 'search', 'fetchFilterSearch', 'loading' ]),
|
||||
}))(LiveFilterModal);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LiveFilterModal';
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import SessionSearchField from 'Shared/SessionSearchField';
|
||||
import SavedSearch from 'Shared/SavedSearch';
|
||||
import LiveSessionSearchField from 'Shared/LiveSessionSearchField';
|
||||
import { Button, Popup } from 'UI';
|
||||
import { clearSearch } from 'Duck/search';
|
||||
import { clearSearch } from 'Duck/liveSearch';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -15,7 +14,7 @@ const LiveSearchBar = (props: Props) => {
|
|||
return (
|
||||
<div className="flex items-center">
|
||||
<div style={{ width: "80%", marginRight: "10px"}}>
|
||||
<SessionSearchField />
|
||||
<LiveSessionSearchField />
|
||||
</div>
|
||||
<div className="flex items-center" style={{ width: "20%"}}>
|
||||
<Popup
|
||||
|
|
@ -39,5 +38,5 @@ const LiveSearchBar = (props: Props) => {
|
|||
)
|
||||
}
|
||||
export default connect(state => ({
|
||||
appliedFilter: state.getIn(['search', 'instance']),
|
||||
appliedFilter: state.getIn(['liveSearch', 'instance']),
|
||||
}), { clearSearch })(LiveSearchBar);
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import { connect } from 'react-redux';
|
||||
import { edit, addFilter } from 'Duck/liveSearch';
|
||||
import LiveFilterModal from 'Shared/Filters/LiveFilterModal';
|
||||
|
||||
interface Props {
|
||||
appliedFilter: any;
|
||||
edit: typeof edit;
|
||||
addFilter: typeof addFilter;
|
||||
}
|
||||
function LiveSessionSearch(props: Props) {
|
||||
const { appliedFilter } = props;
|
||||
const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0;
|
||||
const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0;
|
||||
|
||||
const onUpdateFilter = (filterIndex, filter) => {
|
||||
const newFilters = appliedFilter.filters.map((_filter, i) => {
|
||||
if (i === filterIndex) {
|
||||
return filter;
|
||||
} else {
|
||||
return _filter;
|
||||
}
|
||||
});
|
||||
|
||||
props.edit({
|
||||
...appliedFilter,
|
||||
filters: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
const onRemoveFilter = (filterIndex) => {
|
||||
const newFilters = appliedFilter.filters.filter((_filter, i) => {
|
||||
return i !== filterIndex;
|
||||
});
|
||||
|
||||
props.edit({
|
||||
filters: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
const onChangeEventsOrder = (e, { name, value }) => {
|
||||
props.edit({
|
||||
eventsOrder: value,
|
||||
});
|
||||
}
|
||||
|
||||
return (hasEvents || hasFilters) ? (
|
||||
<div className="border bg-white rounded mt-4">
|
||||
<div className="p-5">
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : <></>;
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
appliedFilter: state.getIn([ 'liveSearch', 'instance' ]),
|
||||
}), { edit, addFilter })(LiveSessionSearch);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LiveSessionSearch';
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.searchField {
|
||||
box-shadow: none !important;
|
||||
& input {
|
||||
box-shadow: none !important;
|
||||
border-radius: 3 !important;
|
||||
border: solid thin $gray-light !important;
|
||||
height: 34px !important;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import stl from './LiveSessionSearchField.css';
|
||||
import { Input } from 'UI';
|
||||
import LiveFilterModal from 'Shared/Filters/LiveFilterModal';
|
||||
import { fetchFilterSearch } from 'Duck/search';
|
||||
import { debounce } from 'App/utils';
|
||||
import { edit as editFilter, addFilterByKeyAndValue } from 'Duck/liveSearch';
|
||||
|
||||
interface Props {
|
||||
fetchFilterSearch: (query: any) => void;
|
||||
editFilter: typeof editFilter;
|
||||
addFilterByKeyAndValue: (key: string, value: string) => void;
|
||||
}
|
||||
function LiveSessionSearchField(props: Props) {
|
||||
const debounceFetchFilterSearch = debounce(props.fetchFilterSearch, 1000)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const onSearchChange = (e, { value }) => {
|
||||
setSearchQuery(value)
|
||||
debounceFetchFilterSearch({ q: value });
|
||||
}
|
||||
|
||||
const onAddFilter = (filter) => {
|
||||
console.log('onAddFilter', filter)
|
||||
props.addFilterByKeyAndValue(filter.key, filter.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
// inputProps={ { "data-openreplay-label": "Search", "autocomplete": "off" } }
|
||||
className={stl.searchField}
|
||||
onFocus={ () => setShowModal(true) }
|
||||
onBlur={ () => setTimeout(setShowModal, 200, false) }
|
||||
onChange={ onSearchChange }
|
||||
icon="search"
|
||||
iconPosition="left"
|
||||
placeholder={ 'Find live sessions by user or metadata.'}
|
||||
fluid
|
||||
id="search"
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
{ showModal && (
|
||||
<div className="absolute left-0 top-20 border shadow rounded bg-white z-50">
|
||||
<LiveFilterModal
|
||||
searchQuery={searchQuery}
|
||||
isMainSearch={true}
|
||||
onFilterClick={onAddFilter}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, { fetchFilterSearch, editFilter, addFilterByKeyAndValue })(LiveSessionSearchField);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './LiveSessionSearchField';
|
||||
|
|
@ -4,6 +4,9 @@ import { fetchListType, saveType, editType, initType, removeType } from './funcT
|
|||
import { createItemInListUpdater, mergeReducers, success, array } from './funcTools/tools';
|
||||
import { createEdit, createInit } from './funcTools/crud';
|
||||
import { createRequestReducer } from './funcTools/request';
|
||||
import { addElementToFiltersMap } from 'Types/filter/newFilter';
|
||||
import { FilterCategory } from '../types/filter/filterType';
|
||||
import { refreshFilterOptions } from './search'
|
||||
|
||||
const name = "integration/variable";
|
||||
const idKey = 'index';
|
||||
|
|
@ -25,7 +28,7 @@ const FETCH_SOURCES_SUCCESS = success(FETCH_SOURCES);
|
|||
|
||||
// const defaultMeta = [{key: 'user_id', index: 0}, {key: 'user_anonymous_id', index: 0}];
|
||||
const initialState = Map({
|
||||
list: List([{key: 'user_id'}, {key: 'user_anonymous_id'}]),
|
||||
list: List(),
|
||||
instance: CustomField(),
|
||||
sources: List(),
|
||||
});
|
||||
|
|
@ -33,6 +36,10 @@ const initialState = Map({
|
|||
const reducer = (state = initialState, action = {}) => {
|
||||
switch(action.type) {
|
||||
case FETCH_SUCCESS:
|
||||
console.log('FETCH_SUCCESS', action.data);
|
||||
action.data.forEach(item => {
|
||||
addElementToFiltersMap(FilterCategory.METADATA, item.key);
|
||||
});
|
||||
return state.set('list', List(action.data).map(CustomField)) //.concat(defaultMeta))
|
||||
case FETCH_SOURCES_SUCCESS:
|
||||
return state.set('sources', List(action.data.map(({ value, ...item}) => ({label: value, key: value, ...item}))).map(CustomField))
|
||||
|
|
@ -53,13 +60,14 @@ const reducer = (state = initialState, action = {}) => {
|
|||
export const edit = createEdit(name);
|
||||
export const init = createInit(name);
|
||||
|
||||
export const fetchList = (siteId) => {
|
||||
return {
|
||||
export const fetchList = (siteId) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH_LIST),
|
||||
call: client => client.get(siteId ? `/${siteId}/metadata` : '/metadata'),
|
||||
}
|
||||
}).then(() => {
|
||||
dispatch(refreshFilterOptions());
|
||||
});
|
||||
}
|
||||
|
||||
export const fetchSources = () => {
|
||||
return {
|
||||
types: array(FETCH_SOURCES),
|
||||
|
|
|
|||
|
|
@ -12,17 +12,6 @@ import logger from 'App/logger';
|
|||
|
||||
import { newFiltersList } from 'Types/filter'
|
||||
import NewFilter, { filtersMap } from 'Types/filter/newFilter';
|
||||
|
||||
const filterOptions = {}
|
||||
|
||||
Object.keys(filtersMap).forEach(key => {
|
||||
const filter = filtersMap[key];
|
||||
if (filterOptions.hasOwnProperty(filter.category)) {
|
||||
filterOptions[filter.category].push(filter);
|
||||
} else {
|
||||
filterOptions[filter.category] = [filter];
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// for (var i = 0; i < newFiltersList.length; i++) {
|
||||
|
|
@ -58,8 +47,9 @@ const SET_ACTIVE_FLOW = 'filters/SET_ACTIVE_FLOW';
|
|||
|
||||
const UPDATE_VALUE = 'filters/UPDATE_VALUE';
|
||||
|
||||
const REFRESH_FILTER_OPTIONS = 'filters/REFRESH_FILTER_OPTIONS';
|
||||
|
||||
const initialState = Map({
|
||||
filterList: filterOptions,
|
||||
instance: Filter(),
|
||||
activeFilter: null,
|
||||
list: List(),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import config from './config';
|
|||
import roles from './roles';
|
||||
import customMetrics from './customMetrics';
|
||||
import search from './search';
|
||||
import liveSearch from './liveSearch';
|
||||
|
||||
export default combineReducers({
|
||||
jwt,
|
||||
|
|
@ -72,6 +73,7 @@ export default combineReducers({
|
|||
roles,
|
||||
customMetrics,
|
||||
search,
|
||||
liveSearch,
|
||||
...integrations,
|
||||
...sources,
|
||||
});
|
||||
|
|
|
|||
84
frontend/app/duck/liveSearch.js
Normal file
84
frontend/app/duck/liveSearch.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import { fetchType, editType } from './funcTools/crud';
|
||||
import { createRequestReducer } from './funcTools/request';
|
||||
import { mergeReducers } from './funcTools/tools';
|
||||
import Filter from 'Types/filter';
|
||||
import SavedFilter from 'Types/filter/savedFilter';
|
||||
import { fetchList as fetchSessionList } from './sessions';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { filterMap, checkFilterValue } from './search';
|
||||
|
||||
const name = "liveSearch";
|
||||
const idKey = "searchId";
|
||||
|
||||
const FETCH = fetchType(name);
|
||||
const EDIT = editType(name);
|
||||
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
|
||||
const APPLY = `${name}/APPLY`;
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
instance: new Filter({ filters: [] }),
|
||||
filterSearchList: {},
|
||||
});
|
||||
|
||||
|
||||
function reducer(state = initialState, action = {}) {
|
||||
switch (action.type) {
|
||||
case EDIT:
|
||||
return state.mergeIn(['instance'], action.instance);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export default mergeReducers(
|
||||
reducer,
|
||||
createRequestReducer({
|
||||
fetch: FETCH,
|
||||
}),
|
||||
);
|
||||
|
||||
const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => {
|
||||
dispatch(actionCreator(...args));
|
||||
const filter = getState().getIn([ 'search', 'instance']).toData();
|
||||
filter.filters = filter.filters.map(filterMap);
|
||||
|
||||
return dispatch(fetchSessionList(filter));
|
||||
};
|
||||
|
||||
export const edit = reduceThenFetchResource((instance) => ({
|
||||
type: EDIT,
|
||||
instance,
|
||||
}));
|
||||
|
||||
export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
|
||||
type: APPLY,
|
||||
filter,
|
||||
fromUrl,
|
||||
}));
|
||||
|
||||
export const fetchSessions = (filter) => (dispatch, getState) => {
|
||||
const _filter = filter ? filter : getState().getIn([ 'search', 'instance']);
|
||||
return dispatch(applyFilter(_filter));
|
||||
};
|
||||
|
||||
export const clearSearch = () => (dispatch, getState) => {
|
||||
// dispatch(applySavedSearch(new SavedFilter({})));
|
||||
dispatch(edit(new Filter({ filters: [] })));
|
||||
return dispatch({
|
||||
type: CLEAR_SEARCH,
|
||||
});
|
||||
}
|
||||
|
||||
export const addFilter = (filter) => (dispatch, getState) => {
|
||||
filter.value = checkFilterValue(filter.value);
|
||||
const instance = getState().getIn([ 'liveSearch', 'instance']);
|
||||
const filters = instance.filters.push(filter);
|
||||
return dispatch(edit(instance.set('filters', filters)));
|
||||
}
|
||||
|
||||
export const addFilterByKeyAndValue = (key, value) => (dispatch, getState) => {
|
||||
let defaultFilter = filtersMap[key];
|
||||
defaultFilter.value = value;
|
||||
dispatch(addFilter(defaultFilter));
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import { errors as errorsRoute, isRoute } from "App/routes";
|
|||
import { fetchList as fetchSessionList } from './sessions';
|
||||
import { fetchList as fetchErrorsList } from './errors';
|
||||
import { FilterCategory, FilterKey } from '../types/filter/filterType';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { filtersMap, generateFilterOptions } from 'Types/filter/newFilter';
|
||||
|
||||
const ERRORS_ROUTE = errorsRoute();
|
||||
|
||||
|
|
@ -29,6 +29,8 @@ const UPDATE = `${name}/UPDATE`;
|
|||
const APPLY = `${name}/APPLY`;
|
||||
const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
|
||||
|
||||
const REFRESH_FILTER_OPTIONS = 'filters/REFRESH_FILTER_OPTIONS';
|
||||
|
||||
function chartWrapper(chart = []) {
|
||||
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
|
||||
}
|
||||
|
|
@ -40,6 +42,7 @@ const updateInstance = (state, instance) => state.getIn([ "savedSearch", savedSe
|
|||
: state;
|
||||
|
||||
const initialState = Map({
|
||||
filterList: generateFilterOptions(filtersMap),
|
||||
list: List(),
|
||||
alertMetricId: null,
|
||||
instance: new Filter({ filters: [] }),
|
||||
|
|
@ -50,6 +53,8 @@ const initialState = Map({
|
|||
// Metric - Series - [] - filters
|
||||
function reducer(state = initialState, action = {}) {
|
||||
switch (action.type) {
|
||||
case REFRESH_FILTER_OPTIONS:
|
||||
return state.set('filterList', generateFilterOptions(filtersMap));
|
||||
case EDIT:
|
||||
return state.mergeIn(['instance'], action.instance);
|
||||
case APPLY:
|
||||
|
|
@ -100,8 +105,11 @@ const checkValues = (key, value) => {
|
|||
return value.filter(i => i !== '' && i !== null);
|
||||
}
|
||||
|
||||
export const checkFilterValue = (value) => {
|
||||
return Array.isArray(value) ? (value.length === 0 ? [""] : value) : [value];
|
||||
}
|
||||
|
||||
export const filterMap = ({category, value, key, operator, sourceOperator, source, custom, isEvent }) => ({
|
||||
// value: value.filter(i => i !== '' && i !== null),
|
||||
value: checkValues(key, value),
|
||||
custom,
|
||||
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,
|
||||
|
|
@ -204,10 +212,6 @@ export const clearSearch = () => (dispatch, getState) => {
|
|||
});
|
||||
}
|
||||
|
||||
const checkFilterValue = (value) => {
|
||||
return Array.isArray(value) ? (value.length === 0 ? [""] : value) : [value];
|
||||
}
|
||||
|
||||
export const addFilter = (filter) => (dispatch, getState) => {
|
||||
filter.value = checkFilterValue(filter.value);
|
||||
const instance = getState().getIn([ 'search', 'instance']);
|
||||
|
|
@ -228,3 +232,8 @@ export const editSavedSearch = instance => {
|
|||
}
|
||||
};
|
||||
|
||||
export const refreshFilterOptions = () => {
|
||||
return {
|
||||
type: REFRESH_FILTER_OPTIONS
|
||||
}
|
||||
}
|
||||
|
|
@ -56,6 +56,18 @@ export const filtersMap = {
|
|||
[FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/click', options: ISSUE_OPTIONS },
|
||||
}
|
||||
|
||||
export const addElementToFiltersMap = (
|
||||
category = FilterCategory.METADATA,
|
||||
key,
|
||||
type = FilterType.MULTIPLE,
|
||||
operator = 'is',
|
||||
operatorOptions = filterOptions.stringOperators,
|
||||
icon = 'filters/metadata'
|
||||
) => {
|
||||
console.log('addElementToFiltersMap', category, key, type, operator, operatorOptions, icon)
|
||||
filtersMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon }
|
||||
}
|
||||
|
||||
|
||||
export const getMetaDataFilter = (key) => {
|
||||
const METADATA_FILTER = { key: key, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: capitalize(key), operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata' }
|
||||
|
|
@ -106,11 +118,15 @@ export default Record({
|
|||
},
|
||||
})
|
||||
|
||||
// const getOperatorDefault = (type) => {
|
||||
// if (type === MISSING_RESOURCE) return 'true';
|
||||
// if (type === SLOW_SESSION) return 'true';
|
||||
// if (type === CLICK_RAGE) return 'true';
|
||||
// if (type === CLICK) return 'on';
|
||||
|
||||
// return 'is';
|
||||
// }
|
||||
export const generateFilterOptions = (filtersMap) => {
|
||||
const _options = {};
|
||||
Object.keys(filtersMap).forEach(key => {
|
||||
const filter = filtersMap[key];
|
||||
if (_options.hasOwnProperty(filter.category)) {
|
||||
_options[filter.category].push(filter);
|
||||
} else {
|
||||
_options[filter.category] = [filter];
|
||||
}
|
||||
});
|
||||
return _options;
|
||||
}
|
||||
|
|
@ -138,7 +138,7 @@ export default Record({
|
|||
startedAt,
|
||||
duration,
|
||||
userNumericHash: hashString(session.userId || session.userAnonymousId || session.userUuid || session.userID || session.userUUID || ""),
|
||||
userDisplayName: session.userId || session.userAnonymousId || 'Anonymous User',
|
||||
userDisplayName: session.userId || session.userAnonymousId || session.userID || 'Anonymous User',
|
||||
firstResourceTime,
|
||||
issues: issuesList,
|
||||
sessionId: sessionId || sessionID,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue