feat(ui) - custom metric widgets active inactive

This commit is contained in:
Shekar Siri 2022-02-07 18:34:28 +01:00
parent fece5f1bca
commit 3e7634c043
8 changed files with 87 additions and 111 deletions

View file

@ -18,11 +18,11 @@ export default class AddWidgets extends React.PureComponent {
const { appearance } = this.props;
const newAppearance = appearance.setIn([ 'dashboard', widgetKey ], true);
this.props.switchOpen(false);
this.props.updateAppearance(newAppearance)
this.props.updateAppearance(newAppearance)
}
render() {
const { appearance, disabled } = this.props;
const { appearance } = this.props;
const avaliableWidgets = WIDGET_LIST.filter(({ key, type }) => !appearance.dashboard[ key ] && type === this.props.type );
return (
@ -44,46 +44,6 @@ export default class AddWidgets extends React.PureComponent {
</div>
}
</OutsideClickDetectingDiv>
<SlideModal
title="Add Widget"
size="middle"
isDisplayed={ false }
content={ this.props.open &&
<NoContent
title="No Widgets Left"
size="small"
show={ avaliableWidgets.length === 0}
>
{ avaliableWidgets.map(({ key, name, description, thumb }) => (
<div className={ cn(stl.widgetCard) } key={ key } >
<div className="flex justify-between items-center flex-1">
<div className="mr-10">
<h4>{ name }</h4>
</div>
<IconButton
className="flex-shrink-0"
onClick={ this.makeAddHandler(key) }
circle
outline
icon="plus"
style={{ width: '24px', height: '24px'}}
/>
</div>
</div>
))}
</NoContent>
}
onClose={ this.props.switchOpen }
/>
<IconButton
circle
size="small"
icon="plus"
outline
onClick={ this.props.switchOpen }
disabled={ disabled || avaliableWidgets.length === 0 } //TODO disabled after Custom fields filtering
/>
</div>
);
}

View file

@ -4,7 +4,7 @@ import withPageTitle from 'HOCs/withPageTitle';
import withPermissions from 'HOCs/withPermissions'
import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard';
import { NoContent, Icon } from 'UI';
import { WIDGET_KEYS } from 'Types/dashboard';
import { WIDGET_KEYS, WIDGET_LIST } from 'Types/dashboard';
import CustomMetrics from 'Shared/CustomMetrics';
import SessionListModal from 'Shared/CustomMetrics/SessionListModal';
@ -124,6 +124,7 @@ function isInViewport(el) {
platform: state.getIn([ 'dashboard', 'platform' ]),
dashboardAppearance: state.getIn([ 'user', 'account', 'appearance', 'dashboard' ]),
activeWidget: state.getIn(['customMetrics', 'activeWidget']),
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
}), { setPeriod, setPlatform, fetchMetadataOptions })
@withPageTitle('Metrics - OpenReplay')
@withRouter
@ -144,6 +145,10 @@ export default class Dashboard extends React.PureComponent {
pageSection: 'metrics',
};
getWidgetsByKey = (widgetType) => {
return WIDGET_LIST.filter(({ key, type }) => !this.props.appearance.dashboard[ key ] && type === widgetType);
}
componentDidMount() {
const { history, location } = this.props;
// TODO check the hash navigato it
@ -209,7 +214,13 @@ export default class Dashboard extends React.PureComponent {
icon
empty
>
<WidgetSection title="Overview" type="overview" className="mb-4" description="(Average Values)">
<WidgetSection
title="Overview"
type="overview"
className="mb-4"
description="(Average Values)"
widgets={this.getWidgetsByKey(OVERVIEW)}
>
<div className="grid grid-cols-4 gap-4" ref={this.list[OVERVIEW]}>
<OverviewWidgets isOverview />
</div>
@ -217,8 +228,9 @@ export default class Dashboard extends React.PureComponent {
<WidgetSection
title="Custom Metrics"
type="customMetrics"
type={CUSTOM_METRICS}
className="mb-4"
widgets={[]}
description={
<div className="flex items-center">
{comparing && (
@ -236,7 +248,7 @@ export default class Dashboard extends React.PureComponent {
</div>
</WidgetSection>
<WidgetSection title="Errors" className="mb-4" type="errors">
<WidgetSection title="Errors" className="mb-4" type="errors" widgets={this.getWidgetsByKey(ERRORS_N_CRASHES)}>
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[ERRORS_N_CRASHES]}>
{ dashboardAppearance.impactedSessionsByJsErrors && <WidgetHolder Component={SessionsAffectedByJSErrors} /> }
{ dashboardAppearance.errorsPerDomains && <WidgetHolder Component={ErrorsPerDomain} /> }
@ -250,7 +262,7 @@ export default class Dashboard extends React.PureComponent {
</div>
</WidgetSection>
<WidgetSection title="Performance" type="performance" className="mb-4">
<WidgetSection title="Performance" type="performance" className="mb-4" widgets={this.getWidgetsByKey(PERFORMANCE)}>
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[PERFORMANCE]}>
{ dashboardAppearance.speedLocation && <WidgetHolder Component={SpeedIndexLocation} /> }
{ dashboardAppearance.crashes && <WidgetHolder Component={Crashes} /> }
@ -270,7 +282,7 @@ export default class Dashboard extends React.PureComponent {
</div>
</WidgetSection>
<WidgetSection title="Resources" type="resources" className="mb-4">
<WidgetSection title="Resources" type="resources" className="mb-4" widgets={this.getWidgetsByKey(RESOURCES)}>
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[RESOURCES]}>
{ dashboardAppearance.resourcesCountByType && <WidgetHolder Component={BreakdownOfLoadedResources} /> }
{ dashboardAppearance.resourcesLoadingTime && <WidgetHolder Component={ResourceLoadingTime} /> }

View file

@ -7,8 +7,8 @@ function WidgetSection({ className, title, children, description, type }) {
<div className={cn(className, 'rounded p-4 bg-gray-light-shade')}>
<div className="mb-4 flex items-center">
<div className="flex items-center">
<div className="text-2xl mr-3">{title}</div>
<AddWidgets type={type} />
<div className="text-2xl mr-3">{title}</div>
{/* <AddWidgets type={type} /> */}
</div>
{description && <div className="ml-auto color-gray-darkest font-medium text-sm">{description}</div> }
</div>

View file

@ -7,7 +7,7 @@ import { LineChart, Line, Legend } from 'recharts';
import { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period';
import stl from './CustomMetricWidget.css';
import { getChartFormatter, getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { edit, remove, setAlertMetricId, setActiveWidget } from 'Duck/customMetrics';
import { edit, remove, setAlertMetricId, setActiveWidget, updateActiveState } from 'Duck/customMetrics';
import { confirm } from 'UI/Confirmation';
import APIClient from 'App/api_client';
import { setShowAlerts } from 'Duck/dashboard';
@ -37,6 +37,7 @@ interface Props {
onAlertClick: (e) => void;
edit: (setDefault?) => void;
setActiveWidget: (widget) => void;
updateActiveState: (metricId, state) => void;
}
function CustomMetricWidget(props: Props) {
const { metric, showSync, compare, period } = props;
@ -89,6 +90,10 @@ function CustomMetricWidget(props: Props) {
props.setActiveWidget({ widget: metric, startTimestamp, endTimestamp, timestamp: event.activePayload[0].payload.timestamp, index })
}
const updateActiveState = (metricId, state) => {
props.updateActiveState(metricId, state);
}
return (
<div className={stl.wrapper}>
<div className="flex items-center mb-10 p-2">
@ -96,7 +101,7 @@ function CustomMetricWidget(props: Props) {
<div className="ml-auto flex items-center">
<WidgetIcon className="cursor-pointer mr-6" icon="bell-plus" tooltip="Set Alert" onClick={props.onAlertClick} />
<WidgetIcon className="cursor-pointer mr-6" icon="pencil" tooltip="Edit Metric" onClick={() => props.edit(metric)} />
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={deleteHandler} />
<WidgetIcon className="cursor-pointer" icon="close" tooltip="Hide Metric" onClick={() => updateActiveState(metric.metricId, false)} />
</div>
</div>
<div>
@ -159,7 +164,7 @@ function CustomMetricWidget(props: Props) {
export default connect(state => ({
period: state.getIn(['dashboard', 'period']),
}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget })(CustomMetricWidget);
}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget, updateActiveState })(CustomMetricWidget);
const WidgetIcon = ({ className = '', tooltip = '', icon, onClick }) => (

View file

@ -21,7 +21,7 @@ function CustomMetricsWidgets(props: Props) {
return (
<>
{list.map((item: any) => (
{list.filter(item => item.active).map((item: any) => (
<CustomMetricWidget
metric={item}
onClickEdit={props.onClickEdit}

View file

@ -6,34 +6,68 @@ import { updateAppearance } from 'Duck/user';
import { WIDGET_LIST } from 'Types/dashboard';
import stl from './addWidgets.css';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
import { updateActiveState } from 'Duck/customMetrics';
const CUSTOM_METRICS = 'custom_metrics';
@connect(state => ({
appearance: state.getIn([ 'user', 'account', 'appearance' ]),
customMetrics: state.getIn(['customMetrics', 'list']),
}), {
updateAppearance,
updateAppearance, updateActiveState,
})
@withToggle()
export default class AddWidgets extends React.PureComponent {
makeAddHandler = widgetKey => () => {
const { appearance } = this.props;
const newAppearance = appearance.setIn([ 'dashboard', widgetKey ], true);
if (this.props.type === CUSTOM_METRICS) {
this.props.updateActiveState(widgetKey, true);
} else {
const { appearance } = this.props;
const newAppearance = appearance.setIn([ 'dashboard', widgetKey ], true);
this.props.updateAppearance(newAppearance)
}
this.props.switchOpen(false);
this.props.updateAppearance(newAppearance)
}
getCustomMetricWidgets = () => {
return this.props.customMetrics.filter(i => !i.active).map(item => ({
type: CUSTOM_METRICS,
key: item.metricId,
name: item.name,
}))
}
render() {
const { appearance, disabled } = this.props;
const avaliableWidgets = WIDGET_LIST.filter(({ key, type }) => !appearance.dashboard[ key ] && type === this.props.type );
const { disabled, widgets, type } = this.props;
// const widgets = WIDGET_LIST.filter(({ key, type }) => !appearance.dashboard[ key ] && type === this.props.type );
const filteredWidgets = type === CUSTOM_METRICS ? this.getCustomMetricWidgets() : widgets;
return (
<div className="relative">
<Popup
trigger={
<IconButton
circle
size="small"
icon="plus"
outline
onClick={ this.props.switchOpen }
disabled={ filteredWidgets.length === 0 } //TODO disabled after Custom fields filtering
/>
}
content={ `Add a metric to this section.` }
size="tiny"
inverted
position="top center"
/>
<OutsideClickDetectingDiv onClickOutside={() => this.props.switchOpen(false)}>
{this.props.open &&
<div
className={cn(stl.menuWrapper, 'absolute border rounded z-10 bg-white w-auto')}
style={{ minWidth: '200px', top: '30px'}}
>
{avaliableWidgets.map(w => (
{filteredWidgets.map(w => (
<div
className={cn(stl.menuItem, 'whitespace-pre cursor-pointer')}
onClick={this.makeAddHandler(w.key)}
@ -44,54 +78,6 @@ export default class AddWidgets extends React.PureComponent {
</div>
}
</OutsideClickDetectingDiv>
<SlideModal
title="Add Widget"
size="middle"
isDisplayed={ false }
content={ this.props.open &&
<NoContent
title="No Widgets Left"
size="small"
show={ avaliableWidgets.length === 0}
>
{ avaliableWidgets.map(({ key, name, description, thumb }) => (
<div className={ cn(stl.widgetCard) } key={ key } >
<div className="flex justify-between items-center flex-1">
<div className="mr-10">
<h4>{ name }</h4>
</div>
<IconButton
className="flex-shrink-0"
onClick={ this.makeAddHandler(key) }
circle
outline
icon="plus"
style={{ width: '24px', height: '24px'}}
/>
</div>
</div>
))}
</NoContent>
}
onClose={ this.props.switchOpen }
/>
<Popup
trigger={
<IconButton
circle
size="small"
icon="plus"
outline
onClick={ this.props.switchOpen }
disabled={ disabled || avaliableWidgets.length === 0 } //TODO disabled after Custom fields filtering
/>
}
content={ `Add a metric to this section.` }
size="tiny"
inverted
position="top center"
/>
</div>
);
}

View file

@ -2,13 +2,13 @@ import React from 'react'
import cn from 'classnames'
import AddWidgets from '../AddWidgets';
function WidgetSection({ className, title, children, description, type }) {
function WidgetSection({ className, title, children, description, type, widgets = [] }) {
return (
<div className={cn(className, 'rounded p-4 bg-gray-light-shade')}>
<div className="mb-4 flex items-center">
<div className="flex items-center">
<div className="text-2xl mr-3">{title}</div>
<AddWidgets type={type} />
<AddWidgets type={type} widgets={widgets} />
</div>
{description && <div className="ml-auto color-gray-darkest font-medium text-sm">{description}</div> }
</div>

View file

@ -13,6 +13,7 @@ const FETCH_LIST = fetchListType(name);
const FETCH_SESSION_LIST = fetchListType(`${name}/FETCH_SESSION_LIST`);
const FETCH = fetchType(name);
const SAVE = saveType(name);
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`;
@ -72,6 +73,8 @@ function reducer(state = initialState, action = {}) {
case success(FETCH_LIST):
const { data } = action;
return state.set("list", List(data.map(CustomMetric)));
// case success(UPDATE_ACTIVE_STATE):
// return updateItemInList(updateInstance(state, action.data), action.data);
case success(FETCH_SESSION_LIST):
return state.set("sessionList", List(action.data.map(item => ({ ...item, sessions: item.sessions.map(Session) }))));
case SET_ACTIVE_WIDGET:
@ -161,4 +164,14 @@ export const setActiveWidget = (widget) => (dispatch, getState) => {
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());
});
}