From 45db3fa1d4eca40e575abe0270af035d0a583227 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 25 Jan 2022 15:03:53 +0530 Subject: [PATCH] feat(ui) - custom metrics --- .../app/components/BugFinder/DateRange.js | 4 +- .../BugFinder/Filters/SortDropdown.js | 2 +- .../FilterSeries/FilterSeries.tsx | 4 +- .../shared/Filters/FilterItem/FilterItem.tsx | 9 +- .../shared/Filters/FilterList/FilterList.tsx | 96 +++++++++++-------- .../Filters/FilterValue/FilterValue.tsx | 47 +++++++-- .../FilterValueDropdown.tsx | 3 +- .../shared/SessionSearch/SessionSearch.tsx | 31 +++++- frontend/app/constants/index.js | 1 + frontend/app/constants/platformOptions.js | 5 + frontend/app/duck/search.js | 26 ++--- frontend/app/types/filter/filterType.ts | 1 + frontend/app/types/filter/newFilter.js | 6 +- 13 files changed, 162 insertions(+), 73 deletions(-) create mode 100644 frontend/app/constants/platformOptions.js diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js index 60e98ffa1..8f16b5207 100644 --- a/frontend/app/components/BugFinder/DateRange.js +++ b/frontend/app/components/BugFinder/DateRange.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { applyFilter } from 'Duck/filters'; +// import { applyFilter } from 'Duck/filters'; +import { applyFilter } from 'Duck/search'; import { fetchList as fetchFunnelsList } from 'Duck/funnels'; import DateRangeDropdown from 'Shared/DateRangeDropdown'; @@ -12,6 +13,7 @@ import DateRangeDropdown from 'Shared/DateRangeDropdown'; }) export default class DateRange extends React.PureComponent { onDateChange = (e) => { + console.log('onDateChange', e); this.props.fetchFunnelsList(e.rangeValue) this.props.applyFilter(e) } diff --git a/frontend/app/components/BugFinder/Filters/SortDropdown.js b/frontend/app/components/BugFinder/Filters/SortDropdown.js index 80f88a0d7..cdcc2e468 100644 --- a/frontend/app/components/BugFinder/Filters/SortDropdown.js +++ b/frontend/app/components/BugFinder/Filters/SortDropdown.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import { Dropdown } from 'semantic-ui-react'; import { Icon } from 'UI'; import { sort } from 'Duck/sessions'; -import { applyFilter } from 'Duck/filters'; +import { applyFilter } from 'Duck/search'; import stl from './sortDropdown.css'; @connect(null, { sort, applyFilter }) diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx index 2fafe112f..40214855c 100644 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/shared/CustomMetrics/FilterSeries/FilterSeries.tsx @@ -99,9 +99,11 @@ function FilterSeries(props: Props) {
{ series.filter.filters.size > 0 ? ( ): (
Add user event or filter to build the series.
diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 2b0231da6..97e09d190 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -9,9 +9,10 @@ interface Props { filter: any; // event/filter onUpdate: (filter) => void; onRemoveFilter: () => void; + isFilter?: boolean; } function FitlerItem(props: Props) { - const { filterIndex, filter, onUpdate } = props; + const { isFilter = false, filterIndex, filter, onUpdate } = props; const replaceFilter = (filter) => { onUpdate(filter); @@ -45,17 +46,17 @@ function FitlerItem(props: Props) { return (
-
{filterIndex+1}
+ { !isFilter &&
{filterIndex+1}
}
- +
diff --git a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx index 5bab3e6b4..cd775178d 100644 --- a/frontend/app/components/shared/Filters/FilterList/FilterList.tsx +++ b/frontend/app/components/shared/Filters/FilterList/FilterList.tsx @@ -3,12 +3,17 @@ import FilterItem from '../FilterItem'; import { SegmentSelection } from 'UI'; interface Props { - filters: any[]; // event/filter + // filters: any[]; // event/filter + filter?: any; // event/filter onUpdateFilter: (filterIndex, filter) => void; onRemoveFilter: (filterIndex) => void; + onChangeEventsOrder: (e, { name, value }) => void; } function FilterList(props: Props) { - const { filters } = props; + const { filter } = props; + const filters = filter.filters.toJS() + const hasEvents = filter.filters.filter(i => i.isEvent).size > 0; + const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0; const onRemoveFilter = (filterIndex) => { const newFilters = filters.filter((_filter, i) => { @@ -20,45 +25,56 @@ function FilterList(props: Props) { return (
-
-
EVENTS
-
-
Events Order
- null } - // value={{ value: series.filter.eventsOrder }} - value={{ value: 'and' }} - list={ [ - { name: 'AND', value: 'and' }, - { name: 'OR', value: 'or' }, - { name: 'THEN', value: 'then' }, - ]} - /> -
-
- {filters.map((filter, filterIndex) => ( - props.onUpdateFilter(filterIndex, filter)} - onRemoveFilter={() => onRemoveFilter(filterIndex) } - /> - ))} + { hasEvents && ( + <> +
+
EVENTS
+
+
Events Order
+ null } + value={{ value: filter.eventsOrder }} + // value={{ value: 'and' }} + list={ [ + { name: 'AND', value: 'and' }, + { name: 'OR', value: 'or' }, + { name: 'THEN', value: 'then' }, + ]} + /> +
+
+ {filters.map((filter, filterIndex) => filter.isEvent ? ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ): null)} +
+ + )} - {/*
Filters
- {filters.filter(f => !f.isEvent).map((filter, filterIndex) => ( - props.onUpdateFilter(filterIndex, filter)} - onRemoveFilter={() => onRemoveFilter(filterIndex) } - /> - ))} */} + {hasFilters && ( + <> +
+
FILTERS
+ {filters.map((filter, filterIndex) => !filter.isEvent ? ( + props.onUpdateFilter(filterIndex, filter)} + onRemoveFilter={() => onRemoveFilter(filterIndex) } + /> + ): null)} + + )}
); } diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 4a129c365..2983febc8 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import FilterAutoComplete from '../FilterAutoComplete'; import { FilterType } from 'Types/filter/filterType'; import FilterValueDropdown from '../FilterValueDropdown'; @@ -10,6 +10,7 @@ interface Props { } function FilterValue(props: Props) { const { filter } = props; + const [durationValues, setDurationValues] = useState({ minDuration: 0, maxDuration: 0 }); const onAddValue = () => { const newValues = filter.value.concat("") @@ -31,6 +32,25 @@ function FilterValue(props: Props) { props.onUpdate({ ...filter, value: newValues }) } + const onDurationChange = (newValues) => { + console.log('durationValues', durationValues) + // setDurationValues({ ...durationValues }); + setDurationValues({ ...durationValues, ...newValues }); + } + + const handleBlur = (e) => { + // const { filter, onChange } = props; + if (filter.type === FilterType.DURATION) { + const { maxDuration, minDuration, key } = filter; + if (maxDuration || minDuration) return; + if (maxDuration !== durationValues.maxDuration || + minDuration !== durationValues.minDuration) { + // onChange(e, { name: 'value', value: [this.state.minDuration, this.state.maxDuration] }); + props.onUpdate({ ...filter, value: [durationValues.minDuration, durationValues.maxDuration] }); + } + } + } + const renderValueFiled = (value, valueIndex) => { switch(filter.type) { case FilterType.ISSUE: @@ -43,12 +63,22 @@ function FilterValue(props: Props) { onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)} /> ) + case FilterType.MULTIPLE_DROPDOWN: + return ( + onSelect(e, { value }, valueIndex)} + /> + ) case FilterType.DURATION: return ( @@ -81,12 +111,17 @@ function FilterValue(props: Props) { ) } } + console.log('durationValues', durationValues) return (
- {filter.value && filter.value.map((value, valueIndex) => ( - renderValueFiled(value, valueIndex) - ))} + { filter.type === FilterType.DURATION ? ( + renderValueFiled(filter.value, 0) + ) : ( + filter.value && filter.value.map((value, valueIndex) => ( + renderValueFiled(value, valueIndex) + )) + )}
); } diff --git a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx index eda66f4ef..248fcabed 100644 --- a/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx +++ b/frontend/app/components/shared/Filters/FilterValueDropdown/FilterValueDropdown.tsx @@ -11,9 +11,10 @@ interface Props { className?: string; options: any[]; search?: boolean; + multiple?: boolean; } function FilterValueDropdown(props: Props) { - const { search = false, options, onChange, value, className = '' } = props; + const { multiple = false, search = false, options, onChange, value, className = '' } = props; // const options = [] return ( diff --git a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx index 7483e70b6..4bd50d609 100644 --- a/frontend/app/components/shared/SessionSearch/SessionSearch.tsx +++ b/frontend/app/components/shared/SessionSearch/SessionSearch.tsx @@ -57,17 +57,40 @@ function SessionSearch(props) { }); } + const onChangeEventsOrder = (e, { name, value }) => { + props.edit({ + ...appliedFilter.toData(), + filter: { + ...appliedFilter.filter.toData(), + eventsOrder: value, + } + }); + } + + const clearSearch = () => { + props.edit({ + ...appliedFilter.toData(), + filter: { + ...appliedFilter.filter.toData(), + filters: [], + } + }); + } + + return (
-
+
-
+
- +
diff --git a/frontend/app/constants/index.js b/frontend/app/constants/index.js index ba6f53cf0..8ba18841a 100644 --- a/frontend/app/constants/index.js +++ b/frontend/app/constants/index.js @@ -10,6 +10,7 @@ export { default as alertConditions } from './alertConditions'; export { default as alertMetrics } from './alertMetrics'; export { default as regions } from './regions'; export { default as links } from './links'; +export { default as platformOptions } from './platformOptions'; export { DAYS as SCHEDULE_DAYS, HOURS as SCHEDULE_HOURS, diff --git a/frontend/app/constants/platformOptions.js b/frontend/app/constants/platformOptions.js new file mode 100644 index 000000000..46747ea2e --- /dev/null +++ b/frontend/app/constants/platformOptions.js @@ -0,0 +1,5 @@ +export default [ + { value: 'desktop', text: 'Desktop' }, + { value: 'mobile', text: 'Mobile' }, + { value: 'tablet', text: 'Tablet' }, +] \ No newline at end of file diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index fee81b122..0aa2afe78 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -26,6 +26,7 @@ const SAVE = saveType(name); const EDIT = editType(name); const REMOVE = removeType(name); const UPDATE = `${name}/UPDATE`; +const APPLY = `${name}/APPLY`; const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`; function chartWrapper(chart = []) { @@ -49,6 +50,13 @@ function reducer(state = initialState, action = {}) { switch (action.type) { case EDIT: return state.set('instance', FilterSeries(action.instance)); + case APPLY: + return action.fromUrl + ? state.set('instance', + Filter(action.filter) + // .set('events', state.getIn([ 'instance', 'events' ])) + ) + : state.mergeIn([ 'instance', 'filter' ], action.filter); case success(SAVE): return state.set([ 'instance' ], CustomMetric(action.data)); case success(REMOVE): @@ -85,13 +93,7 @@ const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getStat dispatch(actionCreator(...args)); const filter = getState().getIn([ 'search', 'instance', 'filter' ]).toData(); filter.filters = filter.filters.map(filterMap); - // console.log('filter', filter) - - // let filter = appliedFilter - // .update('filters', list => list.map(f => f.set('value', f.value || '*')) - // .map(filterMap)); - - // const filter.filters = getState().getIn([ 'instance', 'filter' ]).get('filters').map(filterMap).toJS(); + filter.isNew = true // TODO remove this line return isRoute(ERRORS_ROUTE, window.location.pathname) ? dispatch(fetchErrorsList(filter)) @@ -105,11 +107,11 @@ export const edit = reduceThenFetchResource((instance) => ({ export const remove = createRemove(name); -// export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ -// type: APPLY, -// filter, -// fromUrl, -// })); +export const applyFilter = reduceThenFetchResource((filter, fromUrl=false) => ({ + type: APPLY, + filter, + fromUrl, +})); export const updateSeries = (index, series) => ({ type: UPDATE, diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 03722fc09..f1e2c33a9 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -6,6 +6,7 @@ export enum FilterType { MULTIPLE = "MULTIPLE", COUNTRY = "COUNTRY", DROPDOWN = "DROPDOWN", + MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN", }; export enum FilterKey { diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index bc706668f..1e27ab5b5 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -1,6 +1,6 @@ import Record from 'Types/Record'; import { FilterType, FilterKey } from './filterType' -import { countries } from 'App/constants'; +import { countries, platformOptions } from 'App/constants'; const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i })); @@ -194,11 +194,11 @@ export const filtersMap = { [FilterKey.USER_OS]: { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: 'gear', label: 'User OS', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/os' }, [FilterKey.USER_BROWSER]: { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: 'gear', label: 'User Browser', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/browser' }, [FilterKey.USER_DEVICE]: { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: 'gear', label: 'User Device', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/device' }, - [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/platform' }, + [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: 'gear', label: 'Platform', operator: 'is', operatorOptions: filterOptions, icon: 'filters/platform', options: platformOptions }, [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: 'gear', label: 'RevId', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/rev-id' }, [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: 'recording_attributes', label: 'Referrer', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/referrer' }, - [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.NUMBER, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, + [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: 'recording_attributes', label: 'Duration', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/duration' }, [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.DROPDOWN, category: 'recording_attributes', label: 'User Country', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/country', options: countryOptions }, [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: 'javascript', label: 'Console', operator: 'is', operatorOptions: stringFilterOptions, icon: 'filters/console' },