@@ -112,6 +115,7 @@ function CustomMetricWidget(props: Props) {
data={ data.chart }
margin={Styles.chartMargins}
syncId={ showSync ? "impactedSessionsBySlowPages" : undefined }
+ onClick={clickHandler}
>
{gradientDef}
@@ -131,6 +135,7 @@ function CustomMetricWidget(props: Props) {
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
+ // onClick={clickHandler}
/>
@@ -141,4 +146,6 @@ function CustomMetricWidget(props: Props) {
);
}
-export default connect(null, { remove, setShowAlerts, setAlertMetricId })(CustomMetricWidget);
\ No newline at end of file
+export default connect(state => ({
+ period: state.getIn(['dashboard', 'period']),
+}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget })(CustomMetricWidget);
\ No newline at end of file
diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx
index 246d54a02..de93de03d 100644
--- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx
+++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx
@@ -47,17 +47,17 @@ function CustomMetricWidget(props: Props) {
const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' }
useEffect(() => {
- new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
- .then(response => response.json())
- .then(({ errors, data }) => {
- if (errors) {
- console.log('err', errors)
- } else {
- const _data = getChartFormatter(period)(data[0]);
- // console.log('__data', _data)
- setData({ chart: _data });
- }
- }).finally(() => setLoading(false));
+ // new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() })
+ // .then(response => response.json())
+ // .then(({ errors, data }) => {
+ // if (errors) {
+ // console.log('err', errors)
+ // } else {
+ // const _data = getChartFormatter(period)(data[0]);
+ // // console.log('__data', _data)
+ // setData({ chart: _data });
+ // }
+ // }).finally(() => setLoading(false));
}, [metric])
diff --git a/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx
index ba4a2726a..89a8b1231 100644
--- a/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx
+++ b/frontend/app/components/Dashboard/Widgets/common/CustomMetricWidgetHoc/CustomMetricWidgetHoc.tsx
@@ -5,8 +5,6 @@ import { Icon } from 'UI';
interface Props {
}
const CustomMetricWidgetHoc = ({ ...rest }: Props) => BaseComponent => {
-
- console.log('CustomMetricWidgetHoc', rest);
return (
diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx
index 10f1b3296..34ba8aa92 100644
--- a/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx
+++ b/frontend/app/components/shared/CustomMetrics/CustomMetricForm/CustomMetricForm.tsx
@@ -2,20 +2,23 @@ import React from 'react';
import { Form, SegmentSelection, Button, IconButton } from 'UI';
import FilterSeries from '../FilterSeries';
import { connect } from 'react-redux';
-import { edit as editMetric, save } from 'Duck/customMetrics';
+import { edit as editMetric, save, addSeries } from 'Duck/customMetrics';
import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
interface Props {
metric: any;
editMetric: (metric) => void;
- save: (metric) => void;
+ save: (metric) => Promise
;
loading: boolean;
+ addSeries: (series?) => void;
+ onClose: () => void;
}
function CustomMetricForm(props: Props) {
const { metric, loading } = props;
const addSeries = () => {
+ props.addSeries();
const newSeries = {
name: `Series ${metric.series.size + 1}`,
type: '',
@@ -45,13 +48,17 @@ function CustomMetricForm(props: Props) {
const write = ({ target: { value, name } }) => props.editMetric({ ...metric, [ name ]: value })
const changeConditionTab = (e, { name, value }) => {
- props.editMetric({ ...metric, [ 'type' ]: value })
+ props.editMetric({[ 'type' ]: value });
};
+ const save = () => {
+ props.save(metric).then(props.onClose);
+ }
+
return (
@@ -124,4 +131,4 @@ function CustomMetricForm(props: Props) {
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
loading: state.getIn(['customMetrics', 'saveRequest', 'loading']),
-}), { editMetric, save })(CustomMetricForm);
\ No newline at end of file
+}), { editMetric, save, addSeries })(CustomMetricForm);
\ No newline at end of file
diff --git a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx
index 158ef73c5..74a7099e1 100644
--- a/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx
+++ b/frontend/app/components/shared/CustomMetrics/CustomMetrics.tsx
@@ -1,28 +1,23 @@
-import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
+// import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
import React, { useState } from 'react';
-import { IconButton, SlideModal } from 'UI'
+import { IconButton, SlideModal } from 'UI';
import CustomMetricForm from './CustomMetricForm';
import { connect } from 'react-redux';
-import { edit } from 'Duck/customMetrics';
+import { edit, init } from 'Duck/customMetrics';
interface Props {
metric: any;
edit: (metric) => void;
+ instance: any;
+ init: (instance?, setDefault?) => void;
}
function CustomMetrics(props: Props) {
const { metric } = props;
- const [showModal, setShowModal] = useState(false);
-
- const onClose = () => {
- setShowModal(false);
- }
+ // const [showModal, setShowModal] = useState(false);
return (
- {
- setShowModal(true);
- // props.edit({ name: 'New', series: [{ name: '', filter: {} }], type: '' });
- }} />
+ props.init()} />
{ 'Custom Metric' }
}
- isDisplayed={ showModal }
- onClose={ () => setShowModal(false)}
+ isDisplayed={ !!metric }
+ onClose={ () => props.init(null, false)}
// size="medium"
- content={ (showModal || metric) && (
+ content={ (!!metric) && (
-
+ props.init(null, false)} />
)}
/>
@@ -46,4 +41,5 @@ function CustomMetrics(props: Props) {
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
alertInstance: state.getIn(['alerts', 'instance']),
-}), { edit })(CustomMetrics);
\ No newline at end of file
+ showModal: state.getIn(['customMetrics', 'showModal']),
+}), { edit, init })(CustomMetrics);
\ No newline at end of file
diff --git a/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.css b/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.css
new file mode 100644
index 000000000..149727996
--- /dev/null
+++ b/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.css
@@ -0,0 +1,5 @@
+.wrapper {
+ padding: 0 20px;
+ background-color: #f6f6f6;
+ min-height: calc(100vh - 59px);
+ }
\ No newline at end of file
diff --git a/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.tsx b/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.tsx
new file mode 100644
index 000000000..5cd50a8cd
--- /dev/null
+++ b/frontend/app/components/shared/CustomMetrics/SessionListModal/SessionListModal.tsx
@@ -0,0 +1,57 @@
+import React, { useEffect } from 'react';
+import { SlideModal, NoContent } from 'UI';
+import SessionItem from 'Shared/SessionItem';
+import stl from './SessionListModal.css';
+import { connect } from 'react-redux';
+import { fetchSessionList, setActiveWidget } from 'Duck/customMetrics';
+
+interface Props {
+ loading: boolean;
+ list: any;
+ fetchSessionList: (params) => void;
+ activeWidget: any;
+ setActiveWidget: (widget) => void;
+}
+function SessionListModal(props: Props) {
+ const { activeWidget, loading, list } = props;
+ useEffect(() => {
+ if (!activeWidget || !activeWidget.widget) return;
+ props.fetchSessionList({
+ metricId: activeWidget.widget.metricId,
+ startDate: activeWidget.startTimestamp,
+ endDate: activeWidget.endTimestamp
+ });
+ }, [activeWidget]);
+
+ console.log('SessionListModal', activeWidget);
+ return (
+
+ { 'Custom Metric: ' + activeWidget.widget.name }
+
+ )}
+ isDisplayed={ !!activeWidget }
+ onClose={ () => props.setActiveWidget(null)}
+ // size="medium"
+ content={ activeWidget && (
+
+
+
+ { list && list.map(session => ) }
+
+
+
+ )}
+ />
+ );
+}
+
+export default connect(state => ({
+ loading: state.getIn(['customMetrics', 'sessionListRequest', 'loading']),
+ list: state.getIn(['customMetrics', 'sessionList']),
+ activeWidget: state.getIn(['customMetrics', 'activeWidget']),
+}), { fetchSessionList, setActiveWidget })(SessionListModal);
diff --git a/frontend/app/components/shared/CustomMetrics/SessionListModal/index.ts b/frontend/app/components/shared/CustomMetrics/SessionListModal/index.ts
new file mode 100644
index 000000000..75303a134
--- /dev/null
+++ b/frontend/app/components/shared/CustomMetrics/SessionListModal/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionListModal';
\ No newline at end of file
diff --git a/frontend/app/duck/alerts.js b/frontend/app/duck/alerts.js
index 1869db434..952d161b7 100644
--- a/frontend/app/duck/alerts.js
+++ b/frontend/app/duck/alerts.js
@@ -29,7 +29,7 @@ const reducer = (state = initialState, action = {}) => {
// })
// );
case FETCH_TRIGGER_OPTIONS.SUCCESS:
- return state.set('triggerOptions', action.data);
+ return state.set('triggerOptions', action.data.map(({ name, value}) => ({ text: name, value })));
}
return state;
};
diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js
index d13327b5d..4ee587ff8 100644
--- a/frontend/app/duck/customMetrics.js
+++ b/frontend/app/duck/customMetrics.js
@@ -1,18 +1,21 @@
import { List, Map } from 'immutable';
-// import { clean as cleanParams } from 'App/api_client';
import CustomMetric, { FilterSeries } from 'Types/customMetric'
import { createFetch, fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } 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 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 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 SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`;
@@ -21,35 +24,46 @@ function chartWrapper(chart = []) {
return chart.map(point => ({ ...point, count: Math.max(point.count, 0) }));
}
-// const updateItemInList = createListUpdater(idKey);
-// const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ]
-// ? state.mergeIn([ "instance" ], instance)
-// : state;
+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: 'Session Count',
+ filter: new Filter({ filters: [], eventsOrder: 'and' }),
+ },
+ ])
+})
const initialState = Map({
list: List(),
+ sessionList: List(),
alertMetricId: null,
- // instance: null,
- instance: CustomMetric({
- name: 'New',
- series: List([
- {
- name: 'Session Count',
- filter: new Filter({ filters: [] }),
- },
- ])
- }),
+ instance: null,
+ activeWidget: null,
});
// Metric - Series - [] - filters
function reducer(state = initialState, action = {}) {
switch (action.type) {
case EDIT:
- return state.mergeIn([ 'instance' ], action.instance);
- case UPDATE_SERIES:
+ const instance = state.get('instance')
+ if (instance) {
+ return state.mergeIn([ 'instance' ], action.instance);
+ } else {
+ return state.set('instance', action.instance);
+ }
+ case INIT:
+ return state.set('instance', action.instance);
+ case UPDATE_SERIES:
return state.mergeIn(['instance', 'series', action.index], action.series);
case success(SAVE):
- return state.mergeIn([ 'instance' ], action.data);
+ 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.metricId !== action.id));
case success(FETCH):
@@ -57,6 +71,10 @@ function reducer(state = initialState, action = {}) {
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(Session));
+ case SET_ACTIVE_WIDGET:
+ return state.set("activeWidget", action.widget);
}
return state;
}
@@ -66,6 +84,8 @@ export default mergeReducers(
createRequestReducer({
[ ROOT_KEY ]: FETCH_LIST,
fetch: FETCH,
+ save: SAVE,
+ fetchSessionList: FETCH_SESSION_LIST,
}),
);
@@ -89,7 +109,7 @@ export function fetch(id) {
export function save(instance) {
return {
types: SAVE.array,
- call: client => client.post( `/${ name }s`, instance.toSaveData()),
+ call: client => client.post( `/${ instance.exists() ? name + 's/' + instance[idKey] : name + 's'}`, instance.toSaveData()),
};
}
@@ -105,4 +125,39 @@ export function setAlertMetricId(id) {
type: SET_ALERT_METRIC_ID,
id,
};
+}
+
+export const addSeries = (series) => (dispatch, getState) => {
+ const instance = getState().getIn([ 'customMetrics', 'instance' ])
+ const _series = series || new FilterSeries({
+ name: 'New',
+ filter: new Filter({ filters: [], eventsOrder: 'and' }),
+ });
+ return dispatch({
+ type: EDIT,
+ instance: {
+ series: instance.series.push(_series)
+ },
+ });
+}
+
+export const init = (instnace = null, setDefault = true) => (dispatch, getState) => {
+ dispatch({
+ type: INIT,
+ instance: instnace ? instnace : (setDefault ? defaultInstance : null),
+ });
+}
+
+export const fetchSessionList = (params) => (dispatch, getState) => {
+ dispatch({
+ types: array(FETCH_SESSION_LIST),
+ call: client => client.post(`/sessions/search2`, { ...params }),
+ });
+}
+
+export const setActiveWidget = (widget) => (dispatch, getState) => {
+ dispatch({
+ type: SET_ACTIVE_WIDGET,
+ widget,
+ });
}
\ No newline at end of file
diff --git a/frontend/app/types/customMetric.js b/frontend/app/types/customMetric.js
index fb3044b51..e944f08da 100644
--- a/frontend/app/types/customMetric.js
+++ b/frontend/app/types/customMetric.js
@@ -1,6 +1,7 @@
import Record from 'Types/Record';
import { List } from 'immutable';
import Filter from 'Types/filter';
+import { validateName } from 'App/validate';
export const FilterSeries = Record({
seriesId: undefined,
@@ -31,7 +32,7 @@ export default Record({
idKey: 'metricId',
methods: {
validate() {
- return validateName(this.name, { diacritics: true });
+ return validateName(this.name, { empty: false });
},
toSaveData() {
diff --git a/frontend/app/types/dashboard/helper.js b/frontend/app/types/dashboard/helper.js
index 80f406251..b19b787e3 100644
--- a/frontend/app/types/dashboard/helper.js
+++ b/frontend/app/types/dashboard/helper.js
@@ -1,3 +1,8 @@
+export const getDayStartAndEndTimestamps = (date) => {
+ const start = moment(date).startOf('day').valueOf();
+ const end = moment(date).endOf('day').valueOf();
+ return { start, end };
+};
// const getPerformanceDensity = (period) => {
// switch (period) {
@@ -34,4 +39,13 @@ export const getTimeString = (ts, period) => {
};
export const getChartFormatter = period => (data = []) =>
- data.map(({ timestamp, ...rest }) => ({ time: getTimeString(timestamp, period), ...rest }));
\ No newline at end of file
+ data.map(({ timestamp, ...rest }) => ({ time: getTimeString(timestamp, period), ...rest, timestamp }));
+
+export const getStartAndEndTimestampsByDensity = (current, start, end, density) => {
+ const diff = end - start;
+ const step = diff / density;
+ const currentIndex = Math.floor((current - start) / step);
+ const startTimestamp = parseInt(start + currentIndex * step);
+ const endTimestamp = parseInt(startTimestamp + step);
+ return { startTimestamp, endTimestamp };
+};
\ No newline at end of file