feat(ui) - custom metrics
This commit is contained in:
parent
5f958ede98
commit
45db3fa1d4
13 changed files with 162 additions and 73 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -99,9 +99,11 @@ function FilterSeries(props: Props) {
|
|||
<div className="p-5">
|
||||
{ series.filter.filters.size > 0 ? (
|
||||
<FilterList
|
||||
filters={series.filter.filters.toJS()}
|
||||
// filters={series.filter.filters.toJS()}
|
||||
filter={series.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
/>
|
||||
): (
|
||||
<div className="color-gray-medium">Add user event or filter to build the series.</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="flex items-start mr-auto">
|
||||
<div className="mt-1 w-6 h-6 text-xs flex justify-center rounded-full bg-gray-light-shade mr-2">{filterIndex+1}</div>
|
||||
{ !isFilter && <div className="mt-1 w-6 h-6 text-xs flex justify-center rounded-full bg-gray-light-shade mr-2">{filterIndex+1}</div> }
|
||||
<FilterSelection filter={filter} onFilterClick={replaceFilter} />
|
||||
<FilterOperator filter={filter} onChange={onOperatorChange} className="mx-2 flex-shrink-0"/>
|
||||
<FilterValue filter={filter} onUpdate={onUpdate} />
|
||||
</div>
|
||||
<div className="flex self-start mt-2">
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
className="cursor-pointer p-1"
|
||||
onClick={props.onRemoveFilter}
|
||||
>
|
||||
<Icon name="close" size="18" />
|
||||
<Icon name="trash" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="mb-2 text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm">Events Order</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
// onSelect={onChangeEventsOrder }
|
||||
onSelect={() => null }
|
||||
// value={{ value: series.filter.eventsOrder }}
|
||||
value={{ value: 'and' }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{filters.map((filter, filterIndex) => (
|
||||
<FilterItem
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
))}
|
||||
{ hasEvents && (
|
||||
<>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="mb-2 text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm">Events Order</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
// className="my-3"
|
||||
onSelect={props.onChangeEventsOrder}
|
||||
// onSelect={() => null }
|
||||
value={{ value: filter.eventsOrder }}
|
||||
// value={{ value: 'and' }}
|
||||
list={ [
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
{ name: 'THEN', value: 'then' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{filters.map((filter, filterIndex) => filter.isEvent ? (
|
||||
<FilterItem
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
): null)}
|
||||
<div className='mb-2' />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* <div>Filters</div>
|
||||
{filters.filter(f => !f.isEvent).map((filter, filterIndex) => (
|
||||
<FilterItem
|
||||
filterIndex={filter.index}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
))} */}
|
||||
{hasFilters && (
|
||||
<>
|
||||
<div className='border-t -mx-5 mb-2' />
|
||||
<div className="mb-2 text-sm color-gray-medium mr-auto">FILTERS</div>
|
||||
{filters.map((filter, filterIndex) => !filter.isEvent ? (
|
||||
<FilterItem
|
||||
isFilter={true}
|
||||
filterIndex={filterIndex}
|
||||
filter={filter}
|
||||
onUpdate={(filter) => props.onUpdateFilter(filterIndex, filter)}
|
||||
onRemoveFilter={() => onRemoveFilter(filterIndex) }
|
||||
/>
|
||||
): null)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<FilterValueDropdown
|
||||
multiple={true}
|
||||
value={value}
|
||||
filter={filter}
|
||||
options={filter.options}
|
||||
onChange={(e, { name, value }) => onSelect(e, { value }, valueIndex)}
|
||||
/>
|
||||
)
|
||||
case FilterType.DURATION:
|
||||
return (
|
||||
<FilterDuration
|
||||
// onChange={ this.onDurationChange }
|
||||
onChange={ onDurationChange }
|
||||
// onEnterPress={ this.handleClose }
|
||||
// onBlur={this.handleClose}
|
||||
onBlur={handleBlur}
|
||||
minDuration={ filter.value[0] }
|
||||
maxDuration={ filter.value[1] }
|
||||
/>
|
||||
|
|
@ -81,12 +111,17 @@ function FilterValue(props: Props) {
|
|||
)
|
||||
}
|
||||
}
|
||||
console.log('durationValues', durationValues)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{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)
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="border bg-white rounded mt-4">
|
||||
<div className="p-3">
|
||||
<div className="p-5">
|
||||
<FilterList
|
||||
filters={appliedFilter.filter.filters.toJS()}
|
||||
// filters={appliedFilter.filter.filters.toJS()}
|
||||
filter={appliedFilter.filter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t px-3 py-2 flex items-center">
|
||||
<div className="border-t px-5 py-1 flex items-center -mx-2">
|
||||
<div>
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
|
|
@ -78,7 +101,7 @@ function SessionSearch(props) {
|
|||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<SaveFilterButton />
|
||||
<Button>CLEAR STEPS</Button>
|
||||
<Button onClick={clearSearch}>CLEAR STEPS</Button>
|
||||
<Button plain>SAVE FUNNEL</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
5
frontend/app/constants/platformOptions.js
Normal file
5
frontend/app/constants/platformOptions.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default [
|
||||
{ value: 'desktop', text: 'Desktop' },
|
||||
{ value: 'mobile', text: 'Mobile' },
|
||||
{ value: 'tablet', text: 'Tablet' },
|
||||
]
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export enum FilterType {
|
|||
MULTIPLE = "MULTIPLE",
|
||||
COUNTRY = "COUNTRY",
|
||||
DROPDOWN = "DROPDOWN",
|
||||
MULTIPLE_DROPDOWN = "MULTIPLE_DROPDOWN",
|
||||
};
|
||||
|
||||
export enum FilterKey {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue