diff --git a/frontend/app/components/Dashboard/Dashboard.js b/frontend/app/components/Dashboard/Dashboard.js index 7d2a2f37b..718501081 100644 --- a/frontend/app/components/Dashboard/Dashboard.js +++ b/frontend/app/components/Dashboard/Dashboard.js @@ -6,6 +6,7 @@ import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard'; import { NoContent } from 'UI'; import { WIDGET_KEYS } from 'Types/dashboard'; import CustomMetrics from 'Shared/CustomMetrics'; +import SessionListModal from 'Shared/CustomMetrics/SessionListModal'; import { MissingResources, @@ -188,6 +189,7 @@ export default class Dashboard extends React.PureComponent {
+
{ return params } -interface Period { - rangeName: string; -} - interface Props { metric: any; // loading?: boolean; data?: any; showSync?: boolean; compare?: boolean; - period?: Period; + period?: any; onClickEdit: (e) => void; remove: (id) => void; setShowAlerts: (showAlerts) => void; setAlertMetricId: (id) => void; onAlertClick: (e) => void; + edit: (setDefault?) => void; + setActiveWidget: (widget) => void; } function CustomMetricWidget(props: Props) { - const { metric, showSync, compare, period = { rangeName: LAST_24_HOURS} } = props; + const { metric, showSync, compare, period } = props; const [loading, setLoading] = useState(false) const [data, setData] = useState({ chart: [] }) @@ -51,7 +49,6 @@ function CustomMetricWidget(props: Props) { const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' } useEffect(() => { - // dataWrapper: (p, period) => SessionsImpactedBySlowRequests({ chart: p}) // .update("chart", getChartFormatter(period)) @@ -64,7 +61,7 @@ function CustomMetricWidget(props: Props) { // console.log('data', data); // const _data = data[0].map(CustomMetric).update("chart", getChartFormatter(period)).toJS(); const _data = getChartFormatter(period)(data[0]); - console.log('__data', _data) + // console.log('__data', _data) setData({ chart: _data }); } }).finally(() => setLoading(false)); @@ -80,6 +77,12 @@ function CustomMetricWidget(props: Props) { } } + const clickHandler = (event, index) => { + const timestamp = event.activePayload[0].payload.timestamp; + const { startTimestamp, endTimestamp } = getStartAndEndTimestampsByDensity(timestamp, period.start, period.end, params.density); + props.setActiveWidget({ widget: metric, startTimestamp, endTimestamp, timestamp: event.activePayload[0].payload.timestamp, index }) + } + // const onAlertClick = () => { // props.setShowAlerts(true) // props.setAlertMetricId(metric.metricId) @@ -90,10 +93,10 @@ function CustomMetricWidget(props: Props) {
{metric.name + ' ' + metric.metricId}
-
+
-
+
props.edit(metric)}>
@@ -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 (
props.save(metric)} + onSubmit={save} >
@@ -61,7 +68,7 @@ function CustomMetricForm(props: Props) { className="text-lg" name="name" style={{ fontSize: '18px', padding: '10px', fontWeight: '600'}} - // value={ instance && instance.name } + value={ metric.name } onChange={ write } placeholder="Metric Title" id="name-field" @@ -104,7 +111,7 @@ function CustomMetricForm(props: Props) {
- +
@@ -113,8 +120,8 @@ function CustomMetricForm(props: Props) {
-
@@ -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