Merge pull request #681 from openreplay/url-search

feat(ui) - support search filters in query parameters
This commit is contained in:
Shekar Siri 2022-08-17 19:51:02 +02:00 committed by GitHub
commit eb3aca0ae4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 368 additions and 129 deletions

View file

@ -5,6 +5,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton';
import { connect } from 'react-redux';
import { Button } from 'UI';
import { edit, addFilter } from 'Duck/search';
import SessionSearchQueryParamHandler from 'Shared/SessionSearchQueryParamHandler';
interface Props {
appliedFilter: any;
@ -19,7 +20,7 @@ function SessionSearch(props: Props) {
const onAddFilter = (filter: any) => {
props.addFilter(filter);
}
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
const newFilters = appliedFilter.filters.map((_filter: any, i: any) => {
@ -31,10 +32,10 @@ function SessionSearch(props: Props) {
});
props.edit({
...appliedFilter,
filters: newFilters,
...appliedFilter,
filters: newFilters,
});
}
};
const onRemoveFilter = (filterIndex: any) => {
const newFilters = appliedFilter.filters.filter((_filter: any, i: any) => {
@ -44,51 +45,59 @@ function SessionSearch(props: Props) {
props.edit({
filters: newFilters,
});
}
};
const onChangeEventsOrder = (e: any, { value }: any) => {
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}
saveRequestPayloads={saveRequestPayloads}
/>
</div>
return (
<>
<SessionSearchQueryParamHandler />
{hasEvents || hasFilters ? (
<div className="border bg-white rounded mt-4">
<div className="p-5">
<FilterList
filter={appliedFilter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
saveRequestPayloads={saveRequestPayloads}
/>
</div>
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
>
{/* <IconButton primaryText label="ADD STEP" icon="plus" /> */}
<Button
variant="text-primary"
className="mr-2"
// onClick={() => setshowModal(true)}
icon="plus">
ADD STEP
</Button>
</FilterSelection>
<div className="border-t px-5 py-1 flex items-center -mx-2">
<div>
<FilterSelection filter={undefined} onFilterClick={onAddFilter}>
{/* <IconButton primaryText label="ADD STEP" icon="plus" /> */}
<Button
variant="text-primary"
className="mr-2"
// onClick={() => setshowModal(true)}
icon="plus"
>
ADD STEP
</Button>
</FilterSelection>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
</div>
<div className="ml-auto flex items-center">
<SaveFilterButton />
</div>
</div>
</div>
) : <></>;
) : (
<></>
)}
</>
);
}
export default connect((state: any) => ({
saveRequestPayloads: state.getIn(['site', 'active', 'saveRequestPayloads']),
appliedFilter: state.getIn([ 'search', 'instance' ]),
}), { edit, addFilter })(SessionSearch);
export default connect(
(state: any) => ({
saveRequestPayloads: state.getIn(['site', 'active', 'saveRequestPayloads']),
appliedFilter: state.getIn(['search', 'instance']),
}),
{ edit, addFilter }
)(SessionSearch);

View file

@ -0,0 +1,121 @@
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import { connect } from 'react-redux';
import { addFilterByKeyAndValue, addFilter } from 'Duck/search';
import { getFilterKeyTypeByKey, setQueryParamKeyFromFilterkey } from 'Types/filter/filterType';
import { FilterCategory, FilterKey } from 'Types/filter/filterType';
import { filtersMap } from 'Types/filter/newFilter';
const allowedQueryKeys = [
'userId',
'userid',
'uid',
'usera',
'clk',
'inp',
'loc',
'os',
'browser',
'device',
'platform',
'revid',
'country',
'ref',
'sort',
'order',
'ce',
'sa',
'err',
'iss',
// PERFORMANCE
'domc',
'lcp',
'ttfb',
'acpu',
'amem',
'ff',
];
interface Props {
appliedFilter: any;
addFilterByKeyAndValue: typeof addFilterByKeyAndValue;
addFilter: typeof addFilter;
}
const SessionSearchQueryParamHandler = React.memo((props: Props) => {
const { appliedFilter } = props;
const history = useHistory();
const createUrlQuery = (filters: any) => {
const query: any = {};
filters.forEach((filter: any) => {
if (filter.value.length > 0) {
const _key = setQueryParamKeyFromFilterkey(filter.key);
if (_key) {
let str = `${filter.operator}|${filter.value.join('|')}`;
if (filter.hasSource) {
str = `${str}^${filter.sourceOperator}|${filter.source.join('|')}`;
}
query[_key] = str;
} else {
let str = `${filter.operator}|${filter.value.join('|')}`;
query[filter.key] = str;
}
}
});
return query;
};
const addFilter = ([key, value]: [any, any]): void => {
if (value !== '') {
const filterKey = getFilterKeyTypeByKey(key);
const tmp = value.split('^');
const valueArr = tmp[0].split('|');
const operator = valueArr.shift();
const sourceArr = tmp[1] ? tmp[1].split('|') : [];
const sourceOperator = sourceArr.shift();
// TODO validate operator
if (filterKey) {
props.addFilterByKeyAndValue(filterKey, valueArr, operator, sourceOperator, sourceArr);
} else {
console.warn(`Filter key ${key} not found`);
}
}
};
const applyFilterFromQuery = () => {
const entires = getQueryObject(history.location.search);
if (entires.length > 0) {
entires.forEach(addFilter);
}
};
const generateUrlQuery = () => {
const query: any = createUrlQuery(appliedFilter.filters);
// const queryString = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&');
const queryString = new URLSearchParams(query).toString();
history.replace({ search: queryString });
};
useEffect(applyFilterFromQuery, []);
useEffect(generateUrlQuery, [appliedFilter]);
return <></>;
});
export default connect(
(state: any) => ({
appliedFilter: state.getIn(['search', 'instance']),
}),
{ addFilterByKeyAndValue, addFilter }
)(SessionSearchQueryParamHandler);
function getQueryObject(search: any) {
const queryParams = Object.fromEntries(
Object.entries(Object.fromEntries(new URLSearchParams(search)))
);
return Object.entries(queryParams);
}

View file

@ -0,0 +1 @@
export { default } from './SessionSearchQueryParamHandler';

View file

@ -315,13 +315,17 @@ export const addFilter = (filter) => (dispatch, getState) => {
};
export const addFilterByKeyAndValue =
(key, value, operator = undefined) =>
(key, value, operator = undefined, sourceOperator = undefined, source = undefined) =>
(dispatch, getState) => {
let defaultFilter = filtersMap[key];
defaultFilter.value = value;
if (operator) {
defaultFilter.operator = operator;
}
if (defaultFilter.hasSource && source && sourceOperator) {
defaultFilter.sourceOperator = sourceOperator;
defaultFilter.source = source;
}
dispatch(addFilter(defaultFilter));
};

View file

@ -1,97 +1,201 @@
export enum FilterCategory {
INTERACTIONS = "Interactions",
GEAR = "Gear",
RECORDING_ATTRIBUTES = "Recording Attributes",
JAVASCRIPT = "Javascript",
USER = "User Identification",
METADATA = "Session & User Metadata",
PERFORMANCE = "Performance",
INTERACTIONS = 'Interactions',
GEAR = 'Gear',
RECORDING_ATTRIBUTES = 'Recording Attributes',
JAVASCRIPT = 'Javascript',
USER = 'User Identification',
METADATA = 'Session & User Metadata',
PERFORMANCE = 'Performance',
}
export const setQueryParamKeyFromFilterkey = (filterKey: string) => {
switch (filterKey) {
case FilterKey.USERID:
return 'uid';
case FilterKey.USERANONYMOUSID:
return 'usera';
case FilterKey.CLICK:
return 'clk';
case FilterKey.INPUT:
return 'inp';
case FilterKey.LOCATION:
return 'loc';
case FilterKey.USER_OS:
return 'os';
case FilterKey.USER_BROWSER:
return 'browser';
case FilterKey.USER_DEVICE:
return 'device';
case FilterKey.PLATFORM:
return 'platform';
case FilterKey.REVID:
return 'revid';
case FilterKey.USER_COUNTRY:
return 'country';
case FilterKey.REFERRER:
return 'ref';
case FilterKey.CUSTOM:
return 'ce';
case FilterKey.STATEACTION:
return 'sa';
case FilterKey.ERROR:
return 'err';
case FilterKey.ISSUE:
return 'iss';
// PERFORMANCE
case FilterKey.DOM_COMPLETE:
return 'domc';
case FilterKey.LARGEST_CONTENTFUL_PAINT_TIME:
return 'lcp';
case FilterKey.TTFB:
return 'ttfb';
case FilterKey.AVG_CPU_LOAD:
return 'acpu';
case FilterKey.AVG_MEMORY_USAGE:
return 'amem';
case FilterKey.FETCH_FAILED:
return 'ff';
}
};
export const getFilterKeyTypeByKey = (key: string) => {
switch (key) {
case 'userId':
case 'uid':
case 'userid':
return FilterKey.USERID;
case 'usera':
return FilterKey.USERANONYMOUSID;
case 'clk':
return FilterKey.CLICK;
case 'inp':
return FilterKey.INPUT;
case 'loc':
return FilterKey.LOCATION;
case 'os':
return FilterKey.USER_OS;
case 'browser':
return FilterKey.USER_BROWSER;
case 'device':
return FilterKey.USER_DEVICE;
case 'platform':
return FilterKey.PLATFORM;
case 'revid':
return FilterKey.REVID;
case 'country':
return FilterKey.USER_COUNTRY;
case 'ref':
return FilterKey.REFERRER;
case 'ce':
return FilterKey.CUSTOM;
case 'sa':
return FilterKey.STATEACTION;
case 'err':
return FilterKey.ERROR;
case 'iss':
return FilterKey.ISSUE;
// PERFORMANCE
case 'domc':
return FilterKey.DOM_COMPLETE;
case 'lcp':
return FilterKey.LARGEST_CONTENTFUL_PAINT_TIME;
case 'ttfb':
return FilterKey.TTFB;
case 'acpu':
return FilterKey.AVG_CPU_LOAD;
case 'amem':
return FilterKey.AVG_MEMORY_USAGE;
case 'ff':
return FilterKey.FETCH_FAILED;
}
};
export enum IssueType {
CLICK_RAGE = "click_rage",
DEAD_CLICK = "dead_click",
EXCESSIVE_SCROLLING = "excessive_scrolling",
BAD_REQUEST = "bad_request",
MISSING_RESOURCE = "missing_resource",
MEMORY = "memory",
CPU = "cpu",
SLOW_RESOURCE = "slow_resource",
SLOW_PAGE_LOAD = "slow_page_load",
CRASH = "crash",
CUSTOM = "custom",
JS_EXCEPTION = "js_exception",
CLICK_RAGE = 'click_rage',
DEAD_CLICK = 'dead_click',
EXCESSIVE_SCROLLING = 'excessive_scrolling',
BAD_REQUEST = 'bad_request',
MISSING_RESOURCE = 'missing_resource',
MEMORY = 'memory',
CPU = 'cpu',
SLOW_RESOURCE = 'slow_resource',
SLOW_PAGE_LOAD = 'slow_page_load',
CRASH = 'crash',
CUSTOM = 'custom',
JS_EXCEPTION = 'js_exception',
}
export enum FilterType {
STRING = "STRING",
ISSUE = "ISSUE",
BOOLEAN = "BOOLEAN",
NUMBER = "NUMBER",
NUMBER_MULTIPLE = "NUMBER_MULTIPLE",
DURATION = "DURATION",
MULTIPLE = "MULTIPLE",
SUB_FILTERS = "SUB_FILTERS",
COUNTRY = "COUNTRY",
DROPDOWN = "DROPDOWN",
MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN",
AUTOCOMPLETE_LOCAL = "AUTOCOMPLETE_LOCAL",
};
STRING = 'STRING',
ISSUE = 'ISSUE',
BOOLEAN = 'BOOLEAN',
NUMBER = 'NUMBER',
NUMBER_MULTIPLE = 'NUMBER_MULTIPLE',
DURATION = 'DURATION',
MULTIPLE = 'MULTIPLE',
SUB_FILTERS = 'SUB_FILTERS',
COUNTRY = 'COUNTRY',
DROPDOWN = 'DROPDOWN',
MULTIPLE_DROPDOWN = 'MULTIPLE_DROPDOWN',
AUTOCOMPLETE_LOCAL = 'AUTOCOMPLETE_LOCAL',
}
export enum FilterKey {
ERROR = "ERROR",
MISSING_RESOURCE = "MISSING_RESOURCE",
SLOW_SESSION = "SLOW_SESSION",
CLICK_RAGE = "CLICK_RAGE",
CLICK = "CLICK",
INPUT = "INPUT",
LOCATION = "LOCATION",
VIEW = "VIEW",
CONSOLE = "CONSOLE",
METADATA = "METADATA",
CUSTOM = "CUSTOM",
URL = "URL",
USER_BROWSER = "USERBROWSER",
USER_OS = "USEROS",
USER_DEVICE = "USERDEVICE",
PLATFORM = "PLATFORM",
DURATION = "DURATION",
REFERRER = "REFERRER",
USER_COUNTRY = "USERCOUNTRY",
JOURNEY = "JOURNEY",
REQUEST = "REQUEST",
GRAPHQL = "GRAPHQL",
STATEACTION = "STATEACTION",
REVID = "REVID",
USERANONYMOUSID = "USERANONYMOUSID",
USERID = "USERID",
ISSUE = "ISSUE",
EVENTS_COUNT = "EVENTS_COUNT",
UTM_SOURCE = "UTM_SOURCE",
UTM_MEDIUM = "UTM_MEDIUM",
UTM_CAMPAIGN = "UTM_CAMPAIGN",
DOM_COMPLETE = "DOM_COMPLETE",
LARGEST_CONTENTFUL_PAINT_TIME = "LARGEST_CONTENTFUL_PAINT_TIME",
TIME_BETWEEN_EVENTS = "TIME_BETWEEN_EVENTS",
TTFB = "TTFB",
AVG_CPU_LOAD = "AVG_CPU_LOAD",
AVG_MEMORY_USAGE = "AVG_MEMORY_USAGE",
FETCH_FAILED = "FETCH_FAILED",
FETCH = "FETCH",
FETCH_URL = "FETCH_URL",
FETCH_STATUS_CODE = "FETCH_STATUS_CODE",
FETCH_METHOD = "FETCH_METHOD",
FETCH_DURATION = "FETCH_DURATION",
FETCH_REQUEST_BODY = "FETCH_REQUEST_BODY",
FETCH_RESPONSE_BODY = "FETCH_RESPONSE_BODY",
ERROR = 'ERROR',
MISSING_RESOURCE = 'MISSING_RESOURCE',
SLOW_SESSION = 'SLOW_SESSION',
CLICK_RAGE = 'CLICK_RAGE',
CLICK = 'CLICK',
INPUT = 'INPUT',
LOCATION = 'LOCATION',
VIEW = 'VIEW',
CONSOLE = 'CONSOLE',
METADATA = 'METADATA',
CUSTOM = 'CUSTOM',
URL = 'URL',
USER_BROWSER = 'USERBROWSER',
USER_OS = 'USEROS',
USER_DEVICE = 'USERDEVICE',
PLATFORM = 'PLATFORM',
DURATION = 'DURATION',
REFERRER = 'REFERRER',
USER_COUNTRY = 'USERCOUNTRY',
JOURNEY = 'JOURNEY',
REQUEST = 'REQUEST',
GRAPHQL = 'GRAPHQL',
STATEACTION = 'STATEACTION',
REVID = 'REVID',
USERANONYMOUSID = 'USERANONYMOUSID',
USERID = 'USERID',
ISSUE = 'ISSUE',
EVENTS_COUNT = 'EVENTS_COUNT',
UTM_SOURCE = 'UTM_SOURCE',
UTM_MEDIUM = 'UTM_MEDIUM',
UTM_CAMPAIGN = 'UTM_CAMPAIGN',
GRAPHQL_NAME = "GRAPHQL_NAME",
GRAPHQL_METHOD = "GRAPHQL_METHOD",
GRAPHQL_REQUEST_BODY = "GRAPHQL_REQUEST_BODY",
GRAPHQL_RESPONSE_BODY = "GRAPHQL_RESPONSE_BODY",
DOM_COMPLETE = 'DOM_COMPLETE',
LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME',
TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS',
TTFB = 'TTFB',
AVG_CPU_LOAD = 'AVG_CPU_LOAD',
AVG_MEMORY_USAGE = 'AVG_MEMORY_USAGE',
FETCH_FAILED = 'FETCH_FAILED',
FETCH = 'FETCH',
FETCH_URL = 'FETCH_URL',
FETCH_STATUS_CODE = 'FETCH_STATUS_CODE',
FETCH_METHOD = 'FETCH_METHOD',
FETCH_DURATION = 'FETCH_DURATION',
FETCH_REQUEST_BODY = 'FETCH_REQUEST_BODY',
FETCH_RESPONSE_BODY = 'FETCH_RESPONSE_BODY',
GRAPHQL_NAME = 'GRAPHQL_NAME',
GRAPHQL_METHOD = 'GRAPHQL_METHOD',
GRAPHQL_REQUEST_BODY = 'GRAPHQL_REQUEST_BODY',
GRAPHQL_RESPONSE_BODY = 'GRAPHQL_RESPONSE_BODY',
SESSIONS = 'SESSIONS',
ERRORS = 'js_exception'
}
ERRORS = 'js_exception',
}

View file

@ -11,7 +11,7 @@ export const filters = [
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Input', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
{ key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
{ key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
{ key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
// { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
{ key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },