change(ui): customMetrics cleanup

This commit is contained in:
Shekar Siri 2024-09-16 18:26:35 +05:30
parent d4d836ad24
commit f2e103ad08
8 changed files with 2 additions and 755 deletions

View file

@ -1,17 +0,0 @@
import React from 'react';
import { IconButton } from 'UI';
import { connect } from 'react-redux';
import { edit, init } from 'Duck/customMetrics';
interface Props {
init: (instance?, setDefault?) => void;
}
function CustomMetrics(props: Props) {
return (
<div className="self-start">
<IconButton plain outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
</div>
);
}
export default connect(null, { edit, init })(CustomMetrics);

View file

@ -1,29 +0,0 @@
.wrapper {
padding: 20px;
background-color: #f6f6f6;
min-height: calc(100vh - 59px);
}
.dropdown {
display: flex !important;
padding: 4px 6px;
border-radius: 3px;
color: $gray-darkest;
font-weight: 500;
&:hover {
background-color: $gray-light;
}
}
.dropdownTrigger {
padding: 4px 8px;
border-radius: 3px;
&:hover {
background-color: $gray-light;
}
}
.dropdownIcon {
margin-top: 2px;
margin-left: 3px;
}

View file

@ -1,125 +0,0 @@
import React, { useEffect, useState } from 'react';
import { SlideModal, NoContent, Dropdown, Icon, TimezoneDropdown, Loader } from 'UI';
import SessionItem from 'Shared/SessionItem';
import stl from './SessionListModal.module.css';
import { connect } from 'react-redux';
import { fetchSessionList, setActiveWidget } from 'Duck/customMetrics';
import { DateTime } from 'luxon';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
interface Props {
loading: boolean;
list: any;
fetchSessionList: (params) => void;
activeWidget: any;
setActiveWidget: (widget) => void;
}
function SessionListModal(props: Props) {
const { activeWidget, loading, list } = props;
const [seriesOptions, setSeriesOptions] = useState([
{ text: 'All', value: 'all' },
]);
const [activeSeries, setActiveSeries] = useState('all');
useEffect(() => {
if (!activeWidget || !activeWidget.widget) return;
props.fetchSessionList({
metricId: activeWidget.widget.metricId,
startDate: activeWidget.startTimestamp,
endDate: activeWidget.endTimestamp,
filters: activeWidget.filters || [],
});
}, [activeWidget]);
useEffect(() => {
if (!list) return;
const seriesOptions = list.map(item => ({
text: item.seriesName,
value: item.seriesId,
}));
setSeriesOptions([
{ text: 'All', value: 'all' },
...seriesOptions,
]);
}, [list]);
const getListSessionsBySeries = (seriesId) => {
const arr: any = []
list.forEach(element => {
if (seriesId === 'all') {
const sessionIds = arr.map(i => i.sessionId);
arr.push(...element.sessions.filter(i => !sessionIds.includes(i.sessionId)));
} else {
if (element.seriesId === seriesId) {
arr.push(...element.sessions)
}
}
});
return arr;
}
const writeOption = (e, { name, value }) => setActiveSeries(value);
const filteredSessions = getListSessionsBySeries(activeSeries);
const startTime = DateTime.fromMillis(activeWidget.startTimestamp).toFormat('LLL dd, yyyy HH:mm a');
const endTime = DateTime.fromMillis(activeWidget.endTimestamp).toFormat('LLL dd, yyyy HH:mm a');
return (
<SlideModal
title={ activeWidget && (
<div className="flex items-center">
<div className="mr-auto">{ activeWidget.widget.name } </div>
</div>
)}
isDisplayed={ !!activeWidget }
onClose={ () => props.setActiveWidget(null)}
content={ activeWidget && (
<div className={ stl.wrapper }>
<div className="mb-6 flex items-center">
<div className="mr-auto">Showing all sessions between <span className="font-medium">{startTime}</span> and <span className="font-medium">{endTime}</span> </div>
<div className="flex items-center ml-6">
<div className="flex items-center">
<span className="mr-2 color-gray-medium">Timezone</span>
<TimezoneDropdown />
</div>
{ activeWidget.widget.metricType !== 'table' && (
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Series</span>
<Dropdown
className={stl.dropdown}
direction="left"
options={ seriesOptions }
name="change"
value={ activeSeries }
onChange={ writeOption }
id="change-dropdown"
// icon={null}
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
/>
</div>
)}
{/* <span className="mr-2 color-gray-medium">Series</span> */}
</div>
</div>
<Loader loading={loading}>
<NoContent
show={ !loading && (filteredSessions.length === 0 )}
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_RESULTS} size={60} />
<div className="mt-6 text-2xl">No recordings found!</div>
</div>
}
// animatedIcon="no-results"
>
{ filteredSessions.map(session => <SessionItem key={ session.sessionId } session={ session } />) }
</NoContent>
</Loader>
</div>
)}
/>
);
}
export default connect(state => ({
loading: state.getIn(['customMetrics', 'fetchSessionList', 'loading']),
list: state.getIn(['customMetrics', 'sessionList']),
// activeWidget: state.getIn(['customMetrics', 'activeWidget']),
}), { fetchSessionList, setActiveWidget })(SessionListModal);

View file

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

View file

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

View file

@ -1,203 +0,0 @@
import { List, Map } from 'immutable';
import CustomMetric, { FilterSeries } from 'Types/customMetric'
import { fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud';
import { createRequestReducer, ROOT_KEY } from './funcTools/request';
import { array, success, createListUpdater, mergeReducers } from './funcTools/tools';
import Filter from 'Types/filter';
import Session from 'Types/session';
const name = "custom_metric";
const idKey = "metricId";
const FETCH_LIST = fetchListType(name);
const FETCH_SESSION_LIST = fetchListType(`${name}/FETCH_SESSION_LIST`);
const FETCH = fetchType(name);
const SAVE = saveType(name);
const ADD_SERIES = `${name}/ADD_SERIES`;
const REMOVE_SERIES = `${name}/REMOVE_SERIES`;
const ADD_SERIES_FILTER_FILTER = `${name}/ADD_SERIES_FILTER_FILTER`;
const REMOVE_SERIES_FILTER_FILTER = `${name}/REMOVE_SERIES_FILTER_FILTER`;
const EDIT_SERIES_FILTER = `${name}/EDIT_SERIES_FILTER`;
const EDIT_SERIES_FILTER_FILTER = `${name}/EDIT_SERIES_FILTER_FILTER`;
const UPDATE_ACTIVE_STATE = saveType(`${name}/UPDATE_ACTIVE_STATE`);
const EDIT = editType(name);
const INIT = `${name}/INIT`;
const SET_ACTIVE_WIDGET = `${name}/SET_ACTIVE_WIDGET`;
const REMOVE = removeType(name);
const UPDATE_SERIES = `${name}/UPDATE_SERIES`;
const updateItemInList = createListUpdater(idKey);
const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
? state.mergeIn([ "instance" ], instance)
: state;
const defaultInstance = CustomMetric({
name: 'New',
series: List([
{
name: 'Series 1',
filter: new Filter({ filters: List(), eventsOrder: 'then' }),
},
])
})
const initialState = Map({
list: List(),
sessionList: List(),
alertMetricId: null,
instance: null,
activeWidget: null,
});
// Metric - Series - [] - filters
function reducer(state = initialState, action = {}) {
switch (action.type) {
// Custom Metric
case INIT:
return state.set('instance', action.instance);
case EDIT:
return state.mergeIn([ 'instance' ], action.instance);
case ADD_SERIES:
const series = new FilterSeries(action.series);
return state.updateIn([ 'instance', 'series' ], list => list.push(series));
case REMOVE_SERIES:
return state.updateIn([ 'instance', 'series' ], list => list.delete(action.index));
case UPDATE_SERIES:
return state.mergeIn(['instance', 'series', action.index], action.series);
// Custom Metric - Series - Filters
case EDIT_SERIES_FILTER:
return state.mergeIn(['instance', 'series', action.seriesIndex, 'filter'], action.filter);
// Custom Metric - Series - Filter - Filters
case EDIT_SERIES_FILTER_FILTER:
return state.updateIn([ 'instance', 'series', action.seriesIndex, 'filter', 'filters' ], filters => filters.set(action.filterIndex, action.filter));
case ADD_SERIES_FILTER_FILTER:
return state.updateIn([ 'instance', 'series', action.seriesIndex, 'filter', 'filters' ], filters => filters.push(action.filter));
case REMOVE_SERIES_FILTER_FILTER:
return state.updateIn([ 'instance', 'series', action.seriesIndex, 'filter', 'filters' ], filters => filters.delete(action.index));
case success(SAVE):
return updateItemInList(updateInstance(state, action.data), action.data);
case success(REMOVE):
return state.update('list', list => list.filter(item => item.metricId !== action.id));
case success(FETCH):
return state.set("instance", CustomMetric(action.data));
case success(FETCH_LIST):
const { data } = action;
return state.set("list", List(data.map(CustomMetric)));
case success(FETCH_SESSION_LIST):
return state.set("sessionList", List(action.data.map(item => ({ ...item, sessions: item.sessions.map(s => new Session(s)) }))));
case SET_ACTIVE_WIDGET:
return state.set("activeWidget", action.widget).set('sessionList', List());
}
return state;
}
export default mergeReducers(
reducer,
createRequestReducer({
[ ROOT_KEY ]: FETCH_LIST,
fetch: FETCH,
save: SAVE,
fetchSessionList: FETCH_SESSION_LIST,
}),
);
export const edit = createEdit(name);
export const remove = createRemove(name);
export function fetch(id) {
return {
id,
types: array(FETCH),
call: c => c.get(`/errors/${id}`),
}
}
export const save = (instance) => (dispatch, getState) => {
return dispatch({
types: SAVE.array,
call: client => client.post( `/${ instance.exists() ? name + 's/' + instance[idKey] : name + 's'}`, instance.toSaveData()),
}).then(() => {
dispatch(fetchList());
});
};
export function fetchList() {
return {
types: array(FETCH_LIST),
call: client => client.get(`/${name}s`),
};
}
export const init = (instance = null, forceNull = false) => (dispatch, getState) => {
dispatch({
type: INIT,
instance: forceNull ? null : (instance || defaultInstance),
});
}
export const fetchSessionList = (params) => (dispatch, getState) => {
dispatch({
types: array(FETCH_SESSION_LIST),
call: client => client.post(`/custom_metrics/${params.metricId}/sessions`, { ...params }),
});
}
export const setActiveWidget = (widget) => (dispatch, getState) => {
return dispatch({
type: SET_ACTIVE_WIDGET,
widget,
});
}
export const updateActiveState = (metricId, state) => (dispatch, getState) => {
return dispatch({
types: UPDATE_ACTIVE_STATE.array,
call: client => client.post(`/custom_metrics/${metricId}/status`, { active: state }),
metricId
}).then(() => {
dispatch(fetchList());
});
}
export const editSeriesFilter = (seriesIndex, filter) => (dispatch, getState) => {
return dispatch({
type: EDIT_SERIES_FILTER,
seriesIndex,
filter,
});
}
export const addSeriesFilterFilter = (seriesIndex, filter) => (dispatch, getState) => {
return dispatch({
type: ADD_SERIES_FILTER_FILTER,
seriesIndex,
filter,
});
}
export const removeSeriesFilterFilter = (seriesIndex, filterIndex) => (dispatch, getState) => {
return dispatch({
type: REMOVE_SERIES_FILTER_FILTER,
seriesIndex,
index: filterIndex,
});
}
export const editSeriesFilterFilter = (seriesIndex, filterIndex, filter) => (dispatch, getState) => {
return dispatch({
type: EDIT_SERIES_FILTER_FILTER,
seriesIndex,
filterIndex,
filter,
});
}

View file

@ -1,375 +0,0 @@
import { List, Map, Set } from 'immutable';
import { errors as errorsRoute } from "App/routes";
import Filter from 'Types/filter';
import SavedFilter from 'Types/filter/savedFilter';
import Event from 'Types/filter/event';
import CustomFilter from 'Types/filter/customFilter';
import withRequestState, { RequestTypes } from './requestStateCreator';
import { fetch as fetchFunnel, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered } from './funnels';
const ERRORS_ROUTE = errorsRoute();
const FETCH_LIST = new RequestTypes('funnelFilters/FETCH_LIST');
const FETCH_FILTER_OPTIONS = new RequestTypes('funnelFilters/FETCH_FILTER_OPTIONS');
const SET_FILTER_OPTIONS = 'funnelFilters/SET_FILTER_OPTIONS';
const SAVE = new RequestTypes('funnelFilters/SAVE');
const REMOVE = new RequestTypes('funnelFilters/REMOVE');
const RESET = 'funnelFilters/RESET';
const SET_SEARCH_QUERY = 'funnelFilters/SET_SEARCH_QUERY';
const SET_ACTIVE = 'funnelFilters/SET_ACTIVE';
const SET_ACTIVE_KEY = 'funnelFilters/SET_ACTIVE_KEY';
const APPLY = 'funnelFilters/APPLY';
const ADD_CUSTOM_FILTER = 'funnelFilters/ADD_CUSTOM_FILTER';
const REMOVE_CUSTOM_FILTER = 'funnelFilters/REMOVE_CUSTOM_FILTER';
const RESET_KEY = 'funnelFilters/RESET_KEY';
const ADD_EVENT = 'funnelFilters/ADD_EVENT';
const EDIT_EVENT = 'funnelFilters/EDIT_EVENT';
const REMOVE_EVENT = 'funnelFilters/REMOVE_EVENT';
const MOVE_EVENT = 'funnelFilters/MOVE_EVENT';
const CLEAR_EVENTS = 'funnelFilters/CLEAR_EVENTS';
const TOGGLE_FILTER_MODAL = 'funnelFilters/TOGGLE_FILTER_MODAL';
const ADD_ATTRIBUTE = 'funnelFilters/ADD_ATTRIBUTE';
const EDIT_ATTRIBUTE = 'funnelFilters/EDIT_ATTRIBUTE';
const REMOVE_ATTRIBUTE = 'funnelFilters/REMOVE_ATTRIBUTE';
const SET_ACTIVE_FLOW = 'funnelFilters/SET_ACTIVE_FLOW';
const SET_INITIAL_FILTER = 'funnelFilters/SET_INITIAL_FILTER';
const initialState = Map({
activeFilter: null,
list: List(),
appliedFilter: Filter(),
activeFilterKey: null,
saveModalOpen: false,
customFilters: Map(),
searchQuery: '',
activeFlow: null,
filterOptions: Map({
USEROS: Set(),
USERBROWSER: Set(),
USERDEVICE: Set(),
REFERRER: Set(),
USERCOUNTRY: Set(),
}),
});
let hasFilterOptions = false;
const updateList = (state, instance) => state.update('list', (list) => {
const index = list.findIndex(item => item.filterId === instance.filterId);
return (index >= 0
? list.mergeIn([ index ], instance)
: list.push(instance)
);
});
const reducer = (state = initialState, action = {}) => {
let optionsMap = null;
switch (action.type) {
case FETCH_FILTER_OPTIONS.SUCCESS:
// return state.mergeIn(['filterOptions', action.key], fromJS(action.data).map(item => ({text: item, value: item})));
optionsMap = state.getIn(['filterOptions', action.key]).map(i => i.value).toJS();
return state.mergeIn(['filterOptions', action.key], Set(action.data.filter(i => !optionsMap.includes(i.value))));
case SET_FILTER_OPTIONS:
// optionsMap = state.getIn(['filterOptions', action.key]);
// optionsMap = optionsMap ? optionsMap.map(i => i.value).toJS() : []
// return state.mergeIn(['filterOptions', action.key], Set(action.filterOption.filter(i => !optionsMap.includes(i.value))));
const tmp = {}
let _state = state;
action.filters.forEach(f => {
if (f.type && f.value && f.value.length > 0) {
tmp[f.type] = tmp[f.type] ? tmp[f.type].concat(f.value) : f.value
}
})
Object.keys(tmp).forEach(f => {
const options = List(tmp[f]).map(i => ({type: i, value: i})) // TODO should get the unique items
_state = _state.mergeIn(['filterOptions', f], options);
})
return _state;
case FETCH_LIST.SUCCESS:
return state;
case SAVE.SUCCESS:
return updateList(state, SavedFilter(action.data))
.set('saveModalOpen', false);
case REMOVE.SUCCESS:
return state.update(
'list',
list => list
.filter(filter => filter.filterId !== action.id),
).set('activeFilter', null);
case SET_ACTIVE:
return state.set('activeFilter', action.filter);
case SET_ACTIVE_FLOW:
return state.set('activeFlow', action.flow);
case SET_ACTIVE_KEY:
return state.set('activeFilterKey', action.filterKey);
case APPLY:
return action.fromUrl
? state.set('appliedFilter',
Filter(action.filter)
// .set('events', state.getIn([ 'appliedFilter', 'events' ]))
)
: state.mergeIn(['instance', 'filter'], action.filter);
case ADD_CUSTOM_FILTER:
return state.update('customFilters', vars => vars.set(action.filter, action.value));
case REMOVE_CUSTOM_FILTER:
return state.update('customFilters', vars => vars.remove(action.filterKey));
case RESET_KEY:
if (action.key === 'rangeValue') {
return state
.removeIn([ 'appliedFilter', 'rangeValue' ])
.removeIn([ 'appliedFilter', 'startDate' ])
.removeIn([ 'appliedFilter', 'endDate' ]);
} else if (action.key === 'duration') {
return state
.removeIn([ 'appliedFilter', 'minDuration' ])
.removeIn([ 'appliedFilter', 'maxDuration' ]);
}
return state.removeIn([ 'appliedFilter', action.key ]);
case ADD_EVENT:
const eventValue = action.event.value;
const event = Event(action.event).set('value', eventValue);
if (action.index >= 0) // replacing an event
return state.setIn([ 'appliedFilter', 'events', action.index ], event)
else
return state.updateIn([ 'appliedFilter', 'events' ], list => action.single
? List([ event ])
: list.push(event));
case REMOVE_EVENT:
return state.removeIn([ 'appliedFilter', 'events', action.index ]);
case EDIT_EVENT:
return state.mergeIn([ 'appliedFilter', 'events', action.index], action.filter);
case TOGGLE_FILTER_MODAL:
return state.set('saveModalOpen', action.show);
case MOVE_EVENT:
const { fromI, toI } = action;
return state
.updateIn([ 'appliedFilter', 'events' ], list =>
list.remove(fromI).insert(toI, list.get(fromI)));
case CLEAR_EVENTS:
return state.setIn([ 'appliedFilter', 'events' ], List())
.setIn([ 'appliedFilter', 'filters' ], List())
.set('searchQuery', '');
case ADD_ATTRIBUTE:
const filter = CustomFilter(action.filter);
if (action.index >= 0) // replacing the filter
return state.setIn([ 'appliedFilter', 'filters', action.index], filter);
else
return state.updateIn([ 'appliedFilter', 'filters'], filters => filters.push(filter));
case EDIT_ATTRIBUTE:
return state.setIn([ 'appliedFilter', 'filters', action.index, action.key ], action.value );
case REMOVE_ATTRIBUTE:
return state.removeIn([ 'appliedFilter', 'filters', action.index ]);
case SET_SEARCH_QUERY:
return state.set('searchQuery', action.query);
case RESET:
return state.set('appliedFilter', Filter({}))
default:
return state;
}
};
export default withRequestState({
_: [ REMOVE ],
fetchListRequest: FETCH_LIST,
saveRequest: SAVE,
fetchFilterOptions: FETCH_FILTER_OPTIONS,
}, reducer);
const eventMap = ({value, type, key, operator, source, custom}) => ({value, type, key, operator, source, custom});
const filterMap = ({value, type, key, operator, source, custom }) => ({value: Array.isArray(value) ? value: [value], custom, type, key, operator, source});
const reduceThenFetchResource = actionCreator => (...args) => (dispatch, getState) => {
const action = actionCreator(...args);
dispatch(action);
const appliedFilters = getState().getIn([ 'funnelFilters', 'appliedFilter' ]);
const filter = appliedFilters
.update('events', list => list.map(event => event.set('value', event.value || '*')).map(eventMap))
.toJS();
filter.filters = getState().getIn([ 'funnelFilters', 'appliedFilter', 'filters' ])
.map(filterMap).toJS();
if (action.funnelId) {
dispatch(fetchFunnel(action.funnelId))
dispatch(fetchInsights(action.funnelId, filter))
dispatch(fetchIssuesFiltered(action.funnelId, filter))
dispatch(fetchSessionsFiltered(action.funnelId, filter))
}
}
export function editAttribute(index, key, value) {
return {
type: EDIT_ATTRIBUTE,
index,
key,
value,
};
}
export function addAttribute(filter, index) {
return {
type: ADD_ATTRIBUTE,
filter,
index
};
}
export function removeAttribute(index) {
return {
type: REMOVE_ATTRIBUTE,
index,
};
}
export function fetchList(range) {
return {
types: FETCH_LIST.toArray(),
call: client => client.get(`/funnels`),
};
}
export function fetchFilterOptions(filter, q) {
return {
types: FETCH_FILTER_OPTIONS.toArray(),
call: client => client.get('/sessions/filters/search', { q, type: filter.type }),
key: filter.key
};
}
export function setFilterOptions(filters) {
return {
type: SET_FILTER_OPTIONS,
filters
}
}
export function save(instance) {
return {
types: SAVE.toArray(),
call: client => client.post('/filters', instance.toData()),
instance,
};
}
export function remove(id) {
return {
types: REMOVE.toArray(),
call: client => client.delete(`/filters/${ id }`),
id,
};
}
export function setActive(filter) {
return {
type: SET_ACTIVE,
filter,
};
}
export function setActiveFlow(flow) {
return {
type: SET_ACTIVE_FLOW,
flow,
};
}
export function setActiveKey(filterKey) {
return {
type: SET_ACTIVE_KEY,
filterKey,
};
}
export const addCustomFilter = reduceThenFetchResource((filter, value) => ({
type: ADD_CUSTOM_FILTER,
filter,
value,
}));
export const removeCustomFilter = reduceThenFetchResource(filterKey => ({
type: REMOVE_CUSTOM_FILTER,
filterKey,
}));
export const applyFilter = reduceThenFetchResource((filter, funnelId, fromUrl=false) => ({
type: APPLY,
filter,
funnelId,
fromUrl,
}));
export const setInitialFilters = () => (dispatch, getState) => {
return dispatch({
type: APPLY,
filter: getState().getIn(['funnels', 'instance', 'filter'])
})
}
export const applySavedFilter = reduceThenFetchResource((filter, fromUrl=false) => ({
type: APPLY,
filter,
fromUrl,
}));
export const resetFilterKey = reduceThenFetchResource(key => ({
type: RESET_KEY,
key,
}));
export const clearEvents = reduceThenFetchResource(() => ({
type: CLEAR_EVENTS,
}));
export function addEvent(event, single = false, index) {
return {
type: ADD_EVENT,
event,
single,
index
};
}
export const removeEvent = reduceThenFetchResource((index, funnelId) => ({
type: REMOVE_EVENT,
index,
funnelId
}));
export function moveEvent(fromI, toI) {
return {
type: MOVE_EVENT,
fromI,
toI,
};
}
export const editEvent = reduceThenFetchResource((index, filter, funnelId) => ({
type: EDIT_EVENT,
index,
filter,
funnelId
}))
export function toggleFilterModal(show) {
return {
type: TOGGLE_FILTER_MODAL,
show,
};
}
export function setSearchQuery(query) {
return {
type: SET_SEARCH_QUERY,
query
}
}
export function resetFunnelFilters() {
return {
type: RESET
}
}

View file

@ -9,7 +9,6 @@ import site from './site';
import customFields from './customField';
import integrations from './integrations';
import errors from './errors';
import customMetrics from './customMetrics';
import search from './search';
import liveSearch from './liveSearch';
@ -20,13 +19,12 @@ const rootReducer = combineReducers({
site,
customFields,
errors,
customMetrics,
search,
liveSearch,
...integrations,
...sources,
...sources
});
export type RootStore = ReturnType<typeof rootReducer>
export default rootReducer
export default rootReducer;