feat(ui) - custom metric edit and other changes

This commit is contained in:
Shekar Siri 2022-02-06 13:46:46 +01:00
parent 1cde82e27a
commit 22aeb14afd
13 changed files with 219 additions and 76 deletions

View file

@ -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 {
<div className={ cn(styles.header, "flex items-center w-full") }>
<MetricsFilters />
<CustomMetrics />
<SessionListModal />
</div>
<div className="">
<NoContent

View file

@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Icon } from 'UI';
import { widgetHOC, Styles } from '../../common';
import { Styles } from '../../common';
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
import CustomMetricWidgetHoc from '../../common/CustomMetricWidgetHoc';
// import CustomMetricWidgetHoc from '../../common/CustomMetricWidgetHoc';
import stl from './CustomMetricWidget.css';
import { getChartFormatter } from 'Types/dashboard/helper';
import { remove, setAlertMetricId } from 'Duck/customMetrics';
import { getChartFormatter, getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { edit, remove, setAlertMetricId, setActiveWidget } from 'Duck/customMetrics';
import { confirm } from 'UI/Confirmation';
import APIClient from 'App/api_client';
import { setShowAlerts } from 'Duck/dashboard';
@ -23,25 +23,23 @@ const customParams = rangeName => {
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<any>({ 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) {
<div className="flex items-center mb-10 p-2">
<div className="font-medium">{metric.name + ' ' + metric.metricId}</div>
<div className="ml-auto flex items-center">
<div className="cursor-pointer mr-6" onClick={deleteHandler}>
<div className="cursor-pointer mr-6" onClick={deleteHandler}>
<Icon name="trash" size="14" />
</div>
<div className="cursor-pointer mr-6">
<div className="cursor-pointer mr-6" onClick={() => props.edit(metric)}>
<Icon name="pencil" size="14" />
</div>
<div className="cursor-pointer" onClick={props.onAlertClick}>
@ -112,6 +115,7 @@ function CustomMetricWidget(props: Props) {
data={ data.chart }
margin={Styles.chartMargins}
syncId={ showSync ? "impactedSessionsBySlowPages" : undefined }
onClick={clickHandler}
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
@ -131,6 +135,7 @@ function CustomMetricWidget(props: Props) {
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fill={compare ? 'url(#colorCountCompare)' : 'url(#colorCount)'}
// onClick={clickHandler}
/>
</AreaChart>
</ResponsiveContainer>
@ -141,4 +146,6 @@ function CustomMetricWidget(props: Props) {
);
}
export default connect(null, { remove, setShowAlerts, setAlertMetricId })(CustomMetricWidget);
export default connect(state => ({
period: state.getIn(['dashboard', 'period']),
}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget })(CustomMetricWidget);

View file

@ -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])

View file

@ -5,8 +5,6 @@ import { Icon } from 'UI';
interface Props {
}
const CustomMetricWidgetHoc = ({ ...rest }: Props) => BaseComponent => {
console.log('CustomMetricWidgetHoc', rest);
return (
<div className={stl.wrapper}>
<div className="flex items-center mb-10 p-2">

View file

@ -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<void>;
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 (
<Form
className="relative"
onSubmit={() => props.save(metric)}
onSubmit={save}
>
<div className="p-5 pb-20" style={{ height: 'calc(100vh - 60px)', overflowY: 'auto' }}>
<div className="form-group">
@ -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) {
</div>
<div className="flex justify-end">
<IconButton onClick={addSeries} primaryText label="SERIES" icon="plus" />
<IconButton type="button" onClick={addSeries} primaryText label="SERIES" icon="plus" />
</div>
<div className="my-4" />
@ -113,8 +120,8 @@ function CustomMetricForm(props: Props) {
</div>
<div className="fixed border-t w-full bottom-0 px-5 py-2 bg-white">
<Button loading={loading} primary>
Save
<Button loading={loading} primary disabled={!metric.validate()}>
{ `${metric.exists() ? 'Update' : 'Create'}` }
</Button>
</div>
</Form>
@ -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);
}), { editMetric, save, addSeries })(CustomMetricForm);

View file

@ -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 (
<div className="self-start">
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => {
setShowModal(true);
// props.edit({ name: 'New', series: [{ name: '', filter: {} }], type: '' });
}} />
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
<SlideModal
title={
@ -30,12 +25,12 @@ function CustomMetrics(props: Props) {
<span className="mr-3">{ 'Custom Metric' }</span>
</div>
}
isDisplayed={ showModal }
onClose={ () => setShowModal(false)}
isDisplayed={ !!metric }
onClose={ () => props.init(null, false)}
// size="medium"
content={ (showModal || metric) && (
content={ (!!metric) && (
<div style={{ backgroundColor: '#f6f6f6' }}>
<CustomMetricForm metric={metric} />
<CustomMetricForm metric={metric} onClose={() => props.init(null, false)} />
</div>
)}
/>
@ -46,4 +41,5 @@ function CustomMetrics(props: Props) {
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
alertInstance: state.getIn(['alerts', 'instance']),
}), { edit })(CustomMetrics);
showModal: state.getIn(['customMetrics', 'showModal']),
}), { edit, init })(CustomMetrics);

View file

@ -0,0 +1,5 @@
.wrapper {
padding: 0 20px;
background-color: #f6f6f6;
min-height: calc(100vh - 59px);
}

View file

@ -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 (
<SlideModal
title={ activeWidget && (
<div className="flex items-center">
<span className="mr-3">{ 'Custom Metric: ' + activeWidget.widget.name } </span>
</div>
)}
isDisplayed={ !!activeWidget }
onClose={ () => props.setActiveWidget(null)}
// size="medium"
content={ activeWidget && (
<div className="p-5">
<NoContent
show={ !loading && (list.length === 0 || list.size === 0 )}
title="No recordings found."
>
<div className={ stl.wrapper }>
{ list && list.map(session => <SessionItem key={ session.sessionId } session={ session } />) }
</div>
</NoContent>
</div>
)}
/>
);
}
export default connect(state => ({
loading: state.getIn(['customMetrics', 'sessionListRequest', 'loading']),
list: state.getIn(['customMetrics', 'sessionList']),
activeWidget: state.getIn(['customMetrics', 'activeWidget']),
}), { fetchSessionList, setActiveWidget })(SessionListModal);

View file

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

View file

@ -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;
};

View file

@ -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,
});
}

View file

@ -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() {

View file

@ -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 }));
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 };
};