feat(ui) - assist filters with pagination

This commit is contained in:
Shekar Siri 2022-06-16 16:48:25 +02:00
parent 1a5c50cefa
commit 133714a4cb
22 changed files with 100 additions and 100 deletions

View file

@ -17,7 +17,7 @@ const DropdownChips = ({
}
const onSelect = ({ value }) => {
const newSlected = selected.concat(value);
const newSlected = selected.concat(value.value);
onChange(newSlected)
};

View file

@ -129,6 +129,7 @@ function DashboardView(props: RouteComponentProps<Props>) {
plain
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
/>
</div>
<div className="mx-4" />

View file

@ -7,7 +7,7 @@ import FunnelIssuesSort from '../FunnelIssuesSort';
import FunnelIssuesList from '../FunnelIssuesList';
import { DateTime } from 'luxon';
import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted'
import useIsMounted from 'App/hooks/useIsMounted';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function FunnelIssues() {

View file

@ -66,6 +66,7 @@ function WidgetPreview(props: Props) {
<SelectDateRange
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
/>
</div>
</div>

View file

@ -8,7 +8,7 @@ const toMs = value => value !== '' ? value * 1000 * 60 : null
export default class FilterDuration extends React.PureComponent {
state = { focused: false }
onChange = (e, { name, value }) => {
onChange = ({ target: { name, value }}) => {
const { onChange } = this.props;
if (typeof onChange === 'function') {
onChange({

View file

@ -65,6 +65,7 @@ function FilterItem(props: Props) {
onChange={onSourceOperatorChange}
className="mx-2 flex-shrink-0"
value={filter.sourceOperator}
isDisabled={filter.operatorDisabled}
/>
<FilterSource filter={filter} onUpdate={props.onUpdate} />
</>
@ -78,6 +79,7 @@ function FilterItem(props: Props) {
onChange={onOperatorChange}
className="mx-2 flex-shrink-0"
value={filter.operator}
isDisabled={filter.operatorDisabled}
/>
{ canShowValues && (<FilterValue filter={filter} onUpdate={props.onUpdate} />) }
</>

View file

@ -51,9 +51,8 @@ function FilterModal(props: Props) {
} = props;
const hasSearchQuery = searchQuery && searchQuery.length > 0;
const showSearchList = isMainSearch && searchQuery.length > 0;
console.log('filters', props.filters)
const onFilterSearchClick = (filter) => {
const onFilterSearchClick = (filter: any) => {
const _filter = filtersMap[filter.type];
_filter.value = [filter.value];
onFilterClick(_filter);

View file

@ -57,9 +57,10 @@ interface Props {
className?: string;
options?: any;
value?: string;
isDisabled?: boolean;
}
function FilterOperator(props: Props) {
const { options, value, onChange, className = '' } = props;
const { options, value, onChange, isDisabled = false, className = '' } = props;
return (
<div className="mx-2">
@ -68,6 +69,7 @@ function FilterOperator(props: Props) {
options={options}
styles={dropdownStyles}
placeholder="Select"
isDisabled={isDisabled}
defaultValue={ value }
onChange={({ value }: any) => onChange(null, { name: 'operator', value })}
/>

View file

@ -44,7 +44,10 @@ function FilterSelection(props: Props) {
</OutsideClickDetectingDiv>
{showModal && (
<div className="absolute left-0 border shadow rounded bg-white z-50">
<FilterModal onFilterClick={onFilterClick} filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList } />
<FilterModal
onFilterClick={onFilterClick}
filters={isRoute(ASSIST_ROUTE, window.location.pathname) ? props.filterListLive : props.filterList }
/>
</div>
)}
</div>
@ -52,7 +55,7 @@ function FilterSelection(props: Props) {
}
export default connect((state: any) => ({
filters: state.getIn([ 'search', 'filterList' ]),
liveFilters: state.getIn([ 'search', 'filterListLive' ]),
filterList: state.getIn([ 'search', 'filterList' ]),
filterListLive: state.getIn([ 'search', 'filterListLive' ]),
isLive: state.getIn([ 'sessions', 'activeTab' ]).type === 'live',
}), { })(FilterSelection);

View file

@ -57,17 +57,16 @@ function FilterValue(props: Props) {
}
const getParms = (key: any) => {
let params = {};
let params: any = { type: filter.key };
switch (filter.category) {
case FilterCategory.METADATA:
params = { type: FilterKey.METADATA, key: key };
default:
params = { type: filter.key };
}
if (isRoute(ASSIST_ROUTE, window.location.pathname)) {
params = { ...params, live: true };
}
return params;
}

View file

@ -62,7 +62,7 @@ const dropdownStyles = {
...provided, opacity, transition,
display: 'flex',
alignItems: 'center',
height: '26px',
height: '20px',
};
}
}
@ -70,7 +70,7 @@ interface Props {
filter: any; // event/filter
// options: any[];
value: string;
onChange: (e, { name, value }) => void;
onChange: (value: any) => void;
className?: string;
options: any[];
search?: boolean;
@ -94,7 +94,7 @@ function FilterValueDropdown(props: Props) {
options={ options }
name="issue_type"
defaultValue={ value }
onChange={ onChange }
onChange={ (value: any) => onChange(value.value) }
placeholder="Select"
styles={dropdownStyles}
/>

View file

@ -1,14 +1,13 @@
import React, { useEffect } from 'react';
import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux';
import { NoContent, Loader, Pagination } from 'UI';
import { List } from 'immutable';
import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters';
import { applyFilter } from 'Duck/liveSearch';
import { FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue, updateCurrentPage, updateSort } from 'Duck/liveSearch';
import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch';
import Select from 'Shared/Select';
import SortOrderButton from 'Shared/SortOrderButton';
import { capitalize } from 'App/utils';
@ -18,42 +17,38 @@ const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
const PER_PAGE = 10;
interface Props {
loading: Boolean,
loading: boolean,
metaListLoading: boolean
list: List<any>,
fetchLiveList: () => Promise<void>,
applyFilter: () => void,
filters: any,
addAttribute: (obj: any) => void,
// fetchLiveList: () => Promise<void>,
applyFilter: (filter: any) => void,
filter: any,
// addAttribute: (obj: any) => void,
addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
updateCurrentPage: (page: number) => void,
currentPage: number,
totla: number,
metaList: any,
updateSort: (sort: any) => void,
sort: any,
total: number,
}
function LiveSessionList(props: Props) {
const { loading, filters, list, currentPage, metaList = [], sort } = props;
const { loading, metaListLoading, filter, list, currentPage, total, metaList = [], sort } = props;
var timeoutId: any;
const { filters } = filter;
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list);
const sortOptions = metaList.map((i: any) => ({
text: capitalize(i), label: i
label: capitalize(i), value: i
})).toJS();
// useEffect(() => {
// if (filters.size === 0) {
// props.addFilterByKeyAndValue(FilterKey.USERID, '');
// }
// }, []);
useEffect(() => {
if (metaList.size === 0 || !!sort.field) return;
if (metaListLoading || metaList.size === 0 || !!filter.sort) return;
if (sortOptions[0]) {
props.updateSort({ field: sortOptions[0].value });
props.applyFilter({ sort: sortOptions[0].value });
}
}, [metaList]);
}, [metaListLoading]);
// useEffect(() => {
// const filteredSessions = filters.size > 0 ? props.list.filter(session => {
@ -77,7 +72,7 @@ function LiveSessionList(props: Props) {
// }, [filters, list]);
useEffect(() => {
props.fetchLiveList();
props.applyFilter({ ...filter});
timeout();
return () => {
clearTimeout(timeoutId)
@ -93,13 +88,12 @@ function LiveSessionList(props: Props) {
}
const onSortChange = ({ value }: any) => {
value = value.value
props.updateSort({ field: value });
props.applyFilter({ sort: value.value });
}
const timeout = () => {
timeoutId = setTimeout(() => {
props.fetchLiveList();
props.applyFilter({ ...filter});
timeout();
}, AUTOREFRESH_INTERVAL);
}
@ -110,10 +104,10 @@ function LiveSessionList(props: Props) {
<div className="flex items-baseline">
<h3 className="text-2xl capitalize">
<span>Live Sessions</span>
<span className="ml-2 font-normal color-gray-medium">{sessions.size}</span>
<span className="ml-2 font-normal color-gray-medium">{total}</span>
</h3>
<LiveSessionReloadButton />
<LiveSessionReloadButton onClick={() => props.applyFilter({ ...filter }) } />
</div>
<div className="flex items-center">
<div className="flex items-center ml-6 mr-4">
@ -122,12 +116,12 @@ function LiveSessionList(props: Props) {
plain
right
options={sortOptions}
defaultValue={sort.field}
// defaultValue={sort.field}
onChange={onSortChange}
value={sort.field}
value={sortOptions.find((i: any) => i.value === filter.sort) || sortOptions[0]}
/>
</div>
<SortOrderButton onChange={(state: any) => props.updateSort({ order: state })} sortOrder={sort.order} />
<SortOrderButton onChange={(state: any) => props.applyFilter({ order: state })} sortOrder={filter.order} />
</div>
</div>
@ -140,10 +134,10 @@ function LiveSessionList(props: Props) {
}
image={<img src="/assets/img/live-sessions.png"
style={{ width: '70%', marginBottom: '30px' }}/>}
show={ !loading && sessions && sessions.size === 0}
show={ !loading && list.size === 0}
>
<Loader loading={ loading }>
{sessions.map(session => (
{list.map(session => (
<SessionItem
key={ session.sessionId }
session={ session }
@ -157,7 +151,7 @@ function LiveSessionList(props: Props) {
<div className="w-full flex items-center justify-center py-6">
<Pagination
page={currentPage}
totalPages={Math.ceil(sessions.size / PER_PAGE)}
totalPages={Math.ceil(total / PER_PAGE)}
onPageChange={(page: any) => props.updateCurrentPage(page)}
limit={PER_PAGE}
/>
@ -172,17 +166,16 @@ export default withPermissions(['ASSIST_LIVE'])(connect(
(state: any) => ({
list: state.getIn(['liveSearch', 'list']),
loading: state.getIn([ 'liveSearch', 'fetchList', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
metaListLoading: state.getIn([ 'customFields', 'fetchRequest', 'loading' ]),
filter: state.getIn([ 'liveSearch', 'instance' ]),
total: state.getIn([ 'liveSearch', 'total' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
sort: state.getIn(['liveSearch', 'sort']),
}),
{
fetchLiveList,
applyFilter,
addAttribute,
addFilterByKeyAndValue,
updateCurrentPage,
updateSort,
}
)(LiveSessionList));

View file

@ -1,19 +1,19 @@
import React from 'react'
import ReloadButton from '../ReloadButton'
import { connect } from 'react-redux'
import { fetchSessions } from 'Duck/liveSearch'
// import { fetchSessions } from 'Duck/liveSearch'
interface Props {
loading: boolean
fetchSessions: typeof fetchSessions
onClick: () => void
}
function LiveSessionReloadButton(props: Props) {
const { loading } = props
const { loading, onClick } = props
return (
<ReloadButton loading={loading} onClick={() => props.fetchSessions()} className="cursor-pointer" />
<ReloadButton loading={loading} onClick={onClick} className="cursor-pointer" />
)
}
export default connect(state => ({
export default connect((state: any) => ({
loading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
}), { fetchSessions })(LiveSessionReloadButton)
}))(LiveSessionReloadButton)

View file

@ -1,11 +1,9 @@
import React from 'react';
import FilterList from 'Shared/Filters/FilterList';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { Button } from 'UI';
import { edit, addFilter } from 'Duck/liveSearch';
import SaveFunnelButton from '../SaveFunnelButton';
interface Props {
appliedFilter: any;

View file

@ -16,9 +16,9 @@ interface Props {
[x:string]: any;
}
export default function({ name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', ...rest }: Props) {
const defaultSelected = options.find(o => o.value === defaultValue) || options[0];
const defaultSelected = defaultValue ? (options.find(o => o.value === defaultValue) || options[0]): null;
const customStyles = {
option: (provided, state) => ({
option: (provided: any, state: any) => ({
...provided,
whiteSpace: 'nowrap',
transition: 'all 0.3s',

View file

@ -10,11 +10,12 @@ interface Props {
period: any,
onChange: (data: any) => void;
disableCustom?: boolean;
right?: boolean;
[x: string]: any;
}
function SelectDateRange(props: Props) {
const [isCustom, setIsCustom] = React.useState(false);
const { period, disableCustom = false, ...rest } = props;
const { right = false, period, disableCustom = false, ...rest } = props;
let selectedValue = DATE_RANGE_OPTIONS.find((obj: any) => obj.value === period.rangeName)
const options = DATE_RANGE_OPTIONS.filter((obj: any) => disableCustom ? obj.value !== CUSTOM_RANGE : true);
@ -54,7 +55,7 @@ function SelectDateRange(props: Props) {
<OutsideClickDetectingDiv
onClickOutside={() => setIsCustom(false)}
>
<div className="absolute top-0 mt-10 z-40 right-0" style={{
<div className={ cn("absolute top-0 mt-10 z-40", { 'right-0' : right })} style={{
width: '770px',
// margin: 'auto 50vh 0',
// transform: 'translateX(-50%)'

View file

@ -50,7 +50,7 @@ function ListingVisibility(props) {
type="number"
name="count"
placeholder="E.g 10"
style={{ height: '38px', width: '100%'}}
// style={{ height: '38px', width: '100%'}}
onChange={(e, { value }) => {
changeSettings({ count: value })
}}

View file

@ -14,7 +14,7 @@ function Input(props: Props) {
return (
<div className={cn({ "relative" : icon || leadingButton }, wrapperClassName)}>
{icon && <Icon name={icon} className="absolute top-0 bottom-0 my-auto ml-4" size="14" />}
<input className={ cn("p-2 border border-gray-light bg-white h-10 w-full rounded", className, { 'pl-10' : icon }) } {...rest} />
<input style={{ height: '36px'}} className={ cn("p-2 border border-gray-light bg-white w-full rounded", className, { 'pl-10' : icon }) } {...rest} />
{ leadingButton && <div className="absolute top-0 bottom-0 right-0">{ leadingButton }</div> }
</div>
);

View file

@ -70,6 +70,7 @@ export const fetchList = (siteId) => (dispatch, getState) => {
dispatch(refreshFilterOptions());
});
}
export const fetchSources = () => {
return {
types: array(FETCH_SOURCES),

View file

@ -3,7 +3,6 @@ import { fetchListType, fetchType, editType } from './funcTools/crud';
import { createRequestReducer } from './funcTools/request';
import { mergeReducers, success } from './funcTools/tools';
import Filter from 'Types/filter';
// import { fetchList as fetchSessionList } from './sessions';
import { liveFiltersMap, filtersMap } from 'Types/filter/newFilter';
import { filterMap, checkFilterValue, hasFilterApplied } from './search';
import Session from 'Types/session';
@ -17,29 +16,24 @@ const EDIT = editType(name);
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
const APPLY = `${name}/APPLY`;
const UPDATE_CURRENT_PAGE = `${name}/UPDATE_CURRENT_PAGE`;
const UPDATE_SORT = `${name}/UPDATE_SORT`;
const FETCH_SESSION_LIST = fetchListType(`${name}/FETCH_SESSION_LIST`);
const initialState = Map({
list: List(),
instance: new Filter({ filters: [] }),
instance: new Filter({ filters: [], sort: '' }),
filterSearchList: {},
currentPage: 1,
sort: {
order: 'asc',
field: ''
}
});
function reducer(state = initialState, action = {}) {
switch (action.type) {
case APPLY:
return state.mergeIn(['instance'], action.filter).set('currentPage', 1);
case EDIT:
return state.mergeIn(['instance'], action.instance);
return state.mergeIn(['instance'], action.instance).set('currentPage', 1);
case UPDATE_CURRENT_PAGE:
return state.set('currentPage', action.page);
case UPDATE_SORT:
return state.mergeIn(['sort'], action.sort);
case FETCH_SESSION_LIST:
case success(FETCH_SESSION_LIST):
const { sessions, total } = action.data;
const list = List(sessions).map(Session);
return state
@ -72,7 +66,6 @@ const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getStat
dispatch(actionCreator(...args));
const filter = getState().getIn([ 'liveSearch', 'instance']).toData();
filter.filters = filter.filters.map(filterMap);
filter.limit = 10;
filter.page = getState().getIn([ 'liveSearch', 'currentPage']);
@ -91,10 +84,9 @@ export const edit = reduceThenFetchResource((instance) => ({
instance,
}));
export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
export const applyFilter = reduceThenFetchResource((filter) => ({
type: APPLY,
filter,
fromUrl,
}));
export const fetchSessions = (filter) => (dispatch, getState) => {
@ -125,7 +117,7 @@ export const addFilter = (filter) => (dispatch, getState) => {
}
export const addFilterByKeyAndValue = (key, value, operator = undefined) => (dispatch, getState) => {
let defaultFilter = filtersMap[key];
let defaultFilter = liveFiltersMap[key];
defaultFilter.value = value;
if (operator) {
defaultFilter.operator = operator;
@ -133,19 +125,10 @@ export const addFilterByKeyAndValue = (key, value, operator = undefined) => (dis
dispatch(addFilter(defaultFilter));
}
export function updateCurrentPage(page) {
return {
type: UPDATE_CURRENT_PAGE,
page,
};
}
export function updateSort(sort) {
return {
type: UPDATE_SORT,
sort,
};
}
export const updateCurrentPage = reduceThenFetchResource((page, fromUrl=false) => ({
type: UPDATE_CURRENT_PAGE,
page,
}));
export function fetchFilterSearch(params) {
params.live = true

View file

@ -120,7 +120,7 @@ export const checkFilterValue = (value) => {
return Array.isArray(value) ? (value.length === 0 ? [""] : value) : [value];
}
export const filterMap = ({category, value, key, operator, sourceOperator, source, custom, isEvent, filters }) => ({
export const filterMap = ({category, value, key, operator, sourceOperator, source, custom, isEvent, filters, sort, order }) => ({
value: checkValues(key, value),
custom,
type: category === FilterCategory.METADATA ? FilterKey.METADATA : key,

View file

@ -6,7 +6,7 @@ import { capitalize } from 'App/utils';
const countryOptions = Object.keys(countries).map(i => ({ label: countries[i], value: i }));
const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }]
export const metaFilter = { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata' };
// export const metaFilter = { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata' };
export const filters = [
{ key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
@ -29,7 +29,7 @@ export const filters = [
]},
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'StateAction', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
{ key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
// FILTERS
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
@ -63,9 +63,16 @@ export const liveFiltersMap = filters.reduce((acc, filter) => {
if (
filter.category !== FilterCategory.INTERACTIONS &&
filter.category !== FilterCategory.JAVASCRIPT &&
filter.category !== FilterCategory.PERFORMANCE
filter.category !== FilterCategory.PERFORMANCE &&
filter.key !== FilterKey.DURATION &&
filter.key !== FilterKey.REFERRER
) {
acc[filter.key] = filter;
acc[filter.key].operator = 'contains';
acc[filter.key].operatorDisabled = true;
if (filter.key === FilterKey.PLATFORM) {
acc[filter.key].operator = 'is';
}
}
return acc
}, {});
@ -98,12 +105,19 @@ export const addElementToFiltersMap = (
export const addElementToLiveFiltersMap = (
category = FilterCategory.METADATA,
key,
type = FilterType.STRING,
type = FilterType.MULTIPLE,
operator = 'contains',
operatorOptions = containsFilters,
icon = 'filters/metadata'
) => {
liveFiltersMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true }
liveFiltersMap[key] = {
key, type, category, label: capitalize(key),
operator: operator,
operatorOptions,
icon,
operatorDisabled: true,
isLive: true
}
}
export default Record({
@ -132,11 +146,13 @@ export default Record({
operator: '',
operatorOptions: [],
operatorDisabled: false,
isEvent: false,
index: 0,
options: [],
filters: [],
}, {
keyKey: "_key",
fromJS: ({ value, type, subFilter = false, ...filter }) => {
@ -189,6 +205,7 @@ export const generateLiveFilterOptions = (map) => {
Object.keys(map).filter(i => map[i].isLive).forEach(key => {
const filter = map[key];
filter.operator = 'contains';
filter.operatorDisabled = true;
if (filterSection.hasOwnProperty(filter.category)) {
filterSection[filter.category].push(filter);
} else {