diff --git a/frontend/app/components/BugFinder/CustomFilters/FilterModal.js b/frontend/app/components/BugFinder/CustomFilters/FilterModal.js index 5c0cfbf21..c5ab474be 100644 --- a/frontend/app/components/BugFinder/CustomFilters/FilterModal.js +++ b/frontend/app/components/BugFinder/CustomFilters/FilterModal.js @@ -68,7 +68,7 @@ export default class FilterModal extends React.PureComponent { this.props.addAttribute(filter, _in >= 0 ? _in : _index); } else { logger.log('Adding Event', filter) - const _index = filterType === 'event' ? index : undefined; // should add new one if coming from fitlers + const _index = filterType === 'event' ? index : undefined; // should add new one if coming from filters this.props.addEvent(filter, false, _index); } diff --git a/frontend/app/components/BugFinder/SessionList/SessionList.js b/frontend/app/components/BugFinder/SessionList/SessionList.js index 0aa4b9afb..fa0d282be 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionList.js +++ b/frontend/app/components/BugFinder/SessionList/SessionList.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { Loader, NoContent, Message, Icon, Button, LoadMoreButton } from 'UI'; import { applyFilter, addAttribute, addEvent } from 'Duck/filters'; +import { fetchSessions } from 'Duck/search'; import SessionItem from 'Shared/SessionItem'; import SessionListHeader from './SessionListHeader'; import { KEYS } from 'Types/filter/customFilter'; @@ -21,7 +22,8 @@ var timeoutId; }), { applyFilter, addAttribute, - addEvent + addEvent, + fetchSessions, }) export default class SessionList extends React.PureComponent { state = { @@ -53,7 +55,8 @@ export default class SessionList extends React.PureComponent { timeout = () => { timeoutId = setTimeout(function () { if (this.props.shouldAutorefresh) { - this.props.applyFilter(); + // this.props.applyFilter(); + this.props.fetchSessions(); } this.timeout(); }.bind(this), AUTOREFRESH_INTERVAL); diff --git a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js index 2662c5a04..36e2bc35b 100644 --- a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js +++ b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js @@ -4,6 +4,7 @@ import styles from './funnelSaveModal.css'; import { edit, save, fetchList as fetchFunnelsList } from 'Duck/funnels'; @connect(state => ({ + filter: state.getIn(['search', 'instance']), funnel: state.getIn(['funnels', 'instance']), loading: state.getIn([ 'funnels', 'saveRequest', 'loading' ]) || state.getIn([ 'funnels', 'updateRequest', 'loading' ]), @@ -29,7 +30,7 @@ export default class FunnelSaveModal extends React.PureComponent { onSave = () => { const { funnel, closeHandler } = this.props; if (funnel.name.trim() === '') return; - this.props.save(funnel).then(function() { + this.props.save({ ...funnel, filter: filter }).then(function() { this.props.fetchFunnelsList(); this.props.closeHandler(); }.bind(this)); @@ -38,7 +39,6 @@ export default class FunnelSaveModal extends React.PureComponent { render() { const { show, - appliedFilter, closeHandler, loading, funnel diff --git a/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js b/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js index f76c28065..cbe8c9546 100644 --- a/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js +++ b/frontend/app/components/shared/EventFilter/FilterModal/FilterModal.js @@ -67,7 +67,7 @@ export default class FilterModal extends React.PureComponent { this.props.addAttribute(filter, _in >= 0 ? _in : _index); } else { logger.log('Adding Event', filter) - const _index = filterType === 'event' ? index : undefined; // should add new one if coming from fitlers + const _index = filterType === 'event' ? index : undefined; // should add new one if coming from filters this.props.addEvent(filter, false, _index); } diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index 0cd9f0513..16be3add8 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -101,7 +101,7 @@ function FilterAutoComplete(props: Props) { setTimeout(() => { setShowModal(false) }, 150) } + onBlur={ () => setTimeout(() => { setShowModal(false) }, 200) } onFocus={ () => setShowModal(true)} value={ query } autoFocus={ true } diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index eb10c5553..a5300dc44 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -12,7 +12,7 @@ interface Props { onRemoveFilter: () => void; isFilter?: boolean; } -function FitlerItem(props: Props) { +function FilterItem(props: Props) { const { isFilter = false, filterIndex, filter } = props; const replaceFilter = (filter) => { @@ -53,7 +53,10 @@ function FitlerItem(props: Props) { className="mx-2 flex-shrink-0" value={filter.operator} /> - + { !(filter.operator === "isAny" || filter.operator === "onAny") && ( + + )} +
void, filterSearchList: any, + metaOptions: any, } function FilterModal(props: Props) { - const { filters, onFilterClick = () => null, filterSearchList } = props; + const { filters, metaOptions, onFilterClick = () => null, filterSearchList } = props; const hasFilerSearchList = filterSearchList && Object.keys(filterSearchList).length > 0; + + const allFilters = Object.assign({}, filtersMap); + if (metaOptions.size > 0) { + metaOptions.forEach((option) => { + if (option.key) { + allFilters[option.key] = { + category: FilterKey.METADATA, + key: option.key, + name: option.key, + label: option.key, + }; + } + }); + } + console.log('allFilters', allFilters); const onFilterSearchClick = (filter) => { const _filter = filtersMap[filter.type]; @@ -71,5 +88,6 @@ function FilterModal(props: Props) { export default connect(state => ({ filters: state.getIn([ 'filters', 'filterList' ]), - filterSearchList: state.getIn([ 'search', 'filterSearchList' ]) + filterSearchList: state.getIn([ 'search', 'filterSearchList' ]), + metaOptions: state.getIn([ 'customFields', 'list' ]), }))(FilterModal); \ No newline at end of file diff --git a/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx b/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx index cc622b82a..79e4d4aa1 100644 --- a/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx +++ b/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx @@ -9,8 +9,6 @@ interface Props { function FilterSource(props: Props) { const { filter } = props; - console.log('FilterSource', filter.source); - const onChange = ({ target: { value, name } }) => { props.onUpdate({ ...filter, [name]: [value] }) } @@ -24,6 +22,7 @@ function FilterSource(props: Props) { className={stl.inputField} value={filter.source[0]} onBlur={onChange} + onChange={onChange} type="number" /> ) diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 6bda44668..8be5d257a 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -58,6 +58,7 @@ function FilterValue(props: Props) { case FilterType.DROPDOWN: return ( + setshowModal(true)} primaryText label="SAVE FUNNEL" icon="zoom-in" + /> + + setshowModal(false)} + /> +
+ ) +} diff --git a/frontend/app/components/shared/SaveFunnelButton/index.ts b/frontend/app/components/shared/SaveFunnelButton/index.ts new file mode 100644 index 000000000..246df92ff --- /dev/null +++ b/frontend/app/components/shared/SaveFunnelButton/index.ts @@ -0,0 +1 @@ +export { default } from './SaveFunnelButton'; \ No newline at end of file diff --git a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx index 9b28451c7..58869a115 100644 --- a/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx +++ b/frontend/app/components/shared/SavedSearch/components/SavedSearchDropdown/SavedSearchDropdown.tsx @@ -32,7 +32,7 @@ function Row ({ name, onClick, onClickEdit, onDelete }) { function SavedSearchDropdown(props: Props) { const onClick = (item) => { props.applySavedSearch(item) - props.edit(item.filter) + // props.edit(item.filter) props.onClose() } diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 9b9dafd51..ebe4926a7 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -6,6 +6,7 @@ import SaveFilterButton from 'Shared/SaveFilterButton'; import { connect } from 'react-redux'; import { IconButton, Button } from 'UI'; import { edit } from 'Duck/search'; +import SaveFunnelButton from '../SaveFunnelButton'; interface Props { appliedFilter: any; @@ -84,7 +85,8 @@ function SessionSearch(props) {
- + + {/* */}
diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js index 51fcca4a5..b88e9b808 100644 --- a/frontend/app/constants/filterOptions.js +++ b/frontend/app/constants/filterOptions.js @@ -3,6 +3,11 @@ export const options = [ key: 'is', text: 'is', value: 'is' + }, + { + key: 'isAny', + text: 'is any', + value: 'isAny' }, { key: 'isNot', text: 'is not', @@ -112,7 +117,7 @@ export const options = [ ]; const filterKeys = ['is', 'isNot']; -const stringFilterKeys = ['is', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains']; +const stringFilterKeys = ['is', 'isAny', 'isNot', 'contains', 'startsWith', 'endsWith', 'notContains']; const targetFilterKeys = ['on', 'notOn', 'onAny']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js index 7a611dbca..cad648839 100644 --- a/frontend/app/duck/funnels.js +++ b/frontend/app/duck/funnels.js @@ -266,13 +266,14 @@ export const fetchIssueTypes = () => { } export const save = (instance) => { - const url = instance.exists() - ? `/funnels/${ instance[idKey] }` + const _instance = instance instanceof Funnel ? instance : Funnel(instance); + const url = _instance.exists() + ? `/funnels/${ _instance[idKey] }` : `/funnels`; return { - types: array(instance.exists() ? SAVE : UPDATE), - call: client => client.post(url, instance.toData()), + types: array(_instance.exists() ? SAVE : UPDATE), + call: client => client.post(url, _instance.toData()), } } diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index d2924f12d..42f9ca327 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -1,11 +1,8 @@ import { List, Map } from 'immutable'; -import ErrorInfo, { RESOLVED, UNRESOLVED, IGNORED } from 'Types/errorInfo'; -import CustomMetric, { FilterSeries } from 'Types/customMetric' -import { createFetch, fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud'; +import { fetchListType, fetchType, saveType, removeType, editType, createRemove } from './funcTools/crud'; import { createRequestReducer, ROOT_KEY } from './funcTools/request'; import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools'; import Filter from 'Types/filter'; -import NewFilter from 'Types/filter/newFilter'; import SavedFilter from 'Types/filter/savedFilter'; import { errors as errorsRoute, isRoute } from "App/routes"; import { fetchList as fetchSessionList } from './sessions'; @@ -53,18 +50,14 @@ function reducer(state = initialState, action = {}) { return state.mergeIn(['instance'], action.instance); case APPLY: return action.fromUrl - ? state.set('instance', - Filter(action.filter) - // .set('events', state.getIn([ 'instance', 'events' ])) - ) + ? state.set('instance', Filter(action.filter)) : state.mergeIn(['instance'], action.filter); case success(SAVE): return updateItemInList(updateInstance(state, action.data), action.data); - // return state.mergeIn([ 'instance' ], action.data); case success(REMOVE): return state.update('list', list => list.filter(item => item.searchId !== action.id)); case success(FETCH): - return state.set("instance", ErrorInfo(action.data)); + return state.set("instance", action.data); case success(FETCH_LIST): const { data } = action; return state.set("list", List(data.map(SavedFilter))); @@ -106,7 +99,6 @@ const filterMap = ({value, key, operator, sourceOperator, source, custom, isEven }); const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => { - console.log('reduceThenFetchResource', args); dispatch(actionCreator(...args)); const filter = getState().getIn([ 'search', 'instance']).toData(); filter.filters = filter.filters.map(filterMap); @@ -131,15 +123,18 @@ export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ })); export const applySavedSearch = (filter) => (dispatch, getState) => { - // console.log('applySavedSearch', filter); -// export const applySavedSearch = (filter) => ({ - dispatch(edit(filter ? filter.filter : new Filter({ fitlers: []}))); + dispatch(edit(filter ? filter.filter : new Filter({ filters: []}))); return dispatch({ type: APPLY_SAVED_SEARCH, filter, }) }; +export const fetchSessions = (filter) => (dispatch, getState) => { + const _filter = filter ? filter : getState().getIn([ 'search', 'instance']); + return dispatch(applyFilter(_filter)); +}; + export const updateSeries = (index, series) => ({ type: UPDATE, index, diff --git a/frontend/app/styles/semantic.css b/frontend/app/styles/semantic.css index cf6034861..06930c3e9 100644 --- a/frontend/app/styles/semantic.css +++ b/frontend/app/styles/semantic.css @@ -313,4 +313,8 @@ a:hover { .ui.toggle.checkbox { min-height: 20px !important; +} + +.ui.search.dropdown>input.search { + bottom: 0 !important; } \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 3b24acc2d..b682b76d4 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -46,13 +46,12 @@ export const filtersMap = { [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'UserAnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' }, // PERFORMANCE - [FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER }, - [FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint Time', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/lcpt', isEvent: true }, - // [FilterKey.TIME_BETWEEN_EVENTS]: { key: FilterKey.TIME_BETWEEN_EVENTS, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'Time Between Events', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/click' }, - [FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/ttfb', isEvent: true }, - [FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/cpu-load', isEvent: true }, - [FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'is', operatorOptions: filterOptions.stringOperators, sourceOperatorOptions: filterOptions.customOperators, source: [], icon: 'filters/memory-load', isEvent: true }, - [FilterKey.FETCH_FAILED]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Fetch Failed', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch-failed', isEvent: true }, + [FilterKey.DOM_COMPLETE]: { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, + [FilterKey.LARGEST_CONTENTFUL_PAINT_TIME]: { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, + [FilterKey.TTFB]: { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, + [FilterKey.AVG_CPU_LOAD]: { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, + [FilterKey.AVG_MEMORY_USAGE]: { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', operator: 'isAny', operatorOptions: filterOptions.stringOperators, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '=', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, + [FilterKey.FETCH_FAILED]: { key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Fetch Failed', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch-failed', isEvent: true }, [FilterKey.ISSUE]: { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/click', options: ISSUE_OPTIONS }, }