feat(ui) - customm metrics ui review and changes

This commit is contained in:
Shekar Siri 2022-02-07 14:41:35 +01:00
parent 2fa444a26c
commit 3a08033f16
18 changed files with 150 additions and 71 deletions

View file

@ -47,7 +47,7 @@ const Section = ({ index, title, description, content }) => (
const integrationsRoute = client(CLIENT_TABS.INTEGRATIONS);
const AlertForm = props => {
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions } = props;
const { instance, slackChannels, webhooks, loading, onDelete, deleting, triggerOptions, metricId } = props;
const write = ({ target: { value, name } }) => props.edit({ [ name ]: value })
const writeOption = (e, { name, value }) => props.edit({ [ name ]: value });
const onChangeOption = (e, { checked, name }) => props.edit({ [ name ]: checked })
@ -70,8 +70,7 @@ const AlertForm = props => {
const unit = metric ? metric.unit : '';
const isThreshold = instance.detectionMethod === 'threshold';
console.log('triggerOptions', triggerOptions)
console.log('AlertForm', instance.query);
return (
<Form className={ cn("p-6", stl.wrapper)} style={{ width: '580px' }} onSubmit={() => props.onSubmit(instance)} id="alert-form">

View file

@ -72,7 +72,7 @@ function AlertFormModal(props) {
size="medium"
content={ showModal &&
<AlertForm
metricId={ props.metricId }
metricId={ metricId }
edit={props.edit}
slackChannels={slackChannels}
webhooks={hooks}

View file

@ -3,7 +3,7 @@ import cn from 'classnames';
import withPageTitle from 'HOCs/withPageTitle';
import withPermissions from 'HOCs/withPermissions'
import { setPeriod, setPlatform, fetchMetadataOptions } from 'Duck/dashboard';
import { NoContent } from 'UI';
import { NoContent, Icon } from 'UI';
import { WIDGET_KEYS } from 'Types/dashboard';
import CustomMetrics from 'Shared/CustomMetrics';
import SessionListModal from 'Shared/CustomMetrics/SessionListModal';
@ -58,6 +58,13 @@ const menuList = [
icon: "info-square",
label: getStatusLabel(OVERVIEW),
active: status === OVERVIEW,
},
{
key: OVERVIEW,
section: 'metrics',
icon: "sliders",
label: getStatusLabel(CUSTOM_METRICS),
active: status === CUSTOM_METRICS,
},
{
key: ERRORS_N_CRASHES,
@ -87,6 +94,8 @@ function getStatusLabel(status) {
switch(status) {
case OVERVIEW:
return "Overview";
case CUSTOM_METRICS:
return "Custom Metrics";
case PERFORMANCE:
return "Performance";
case ERRORS_N_CRASHES:
@ -189,7 +198,7 @@ export default class Dashboard extends React.PureComponent {
<div>
<div className={ cn(styles.header, "flex items-center w-full") }>
<MetricsFilters />
<CustomMetrics />
{ activeWidget && <SessionListModal activeWidget={activeWidget} /> }
</div>
<div className="">
@ -206,8 +215,21 @@ export default class Dashboard extends React.PureComponent {
</div>
</WidgetSection>
<WidgetSection title="Custom Metrics" type="customMetrics" className="mb-4">
<div className={ cn("gap-4", { 'grid grid-cols-2' : !comparing })} ref={this.list[CUSTOM_METRICS]}>
<WidgetSection
title="Custom Metrics"
type="customMetrics"
className="mb-4"
description={
<div className="flex items-center">
<div className="mr-4 text-sm flex items-center font-normal">
<Icon name="info" size="12" className="mr-2" />
Custom Metrics are not supported for comparison.
</div>
<CustomMetrics />
</div>
}
>
<div className={cn("gap-4 grid grid-cols-2")} ref={this.list[CUSTOM_METRICS]}>
<CustomMetricsWidgets onClickEdit={(e) => null}/>
</div>
</WidgetSection>

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { Loader, NoContent, Icon } from 'UI';
import { Loader, NoContent, Icon, Popup } from 'UI';
import { Styles } from '../../common';
import { ResponsiveContainer, AreaChart, XAxis, YAxis, CartesianGrid, Area, Tooltip } from 'recharts';
import { LineChart, Line, Legend } from 'recharts';
@ -89,25 +89,14 @@ function CustomMetricWidget(props: Props) {
props.setActiveWidget({ widget: metric, startTimestamp, endTimestamp, timestamp: event.activePayload[0].payload.timestamp, index })
}
// const onAlertClick = () => {
// props.setShowAlerts(true)
// props.setAlertMetricId(metric.metricId)
// }
return (
<div className={stl.wrapper}>
<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}>
<Icon name="trash" size="14" />
</div>
<div className="cursor-pointer mr-6" onClick={() => props.edit(metric)}>
<Icon name="pencil" size="14" />
</div>
<div className="cursor-pointer" onClick={props.onAlertClick}>
<Icon name="bell-plus" size="14" />
</div>
<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} />
</div>
</div>
<div>
@ -170,4 +159,19 @@ function CustomMetricWidget(props: Props) {
export default connect(state => ({
period: state.getIn(['dashboard', 'period']),
}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget })(CustomMetricWidget);
}), { remove, setShowAlerts, setAlertMetricId, edit, setActiveWidget })(CustomMetricWidget);
const WidgetIcon = ({ className = '', tooltip = '', icon, onClick }) => (
<Popup
size="small"
trigger={
<div className={className} onClick={onClick}>
<Icon name={icon} size="14" />
</div>
}
content={tooltip}
position="top center"
inverted
/>
)

View file

@ -125,7 +125,7 @@ function CustomMetricWidget(props: Props) {
allowDecimals={false}
label={{
...Styles.axisLabelLeft,
value: "Number of Errors"
value: "Number of Sessions"
}}
/>
<Legend />

View file

@ -3,11 +3,13 @@ import { connect } from 'react-redux';
import { fetchList } from 'Duck/customMetrics';
import CustomMetricWidget from './CustomMetricWidget';
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
import { init as initAlert } from 'Duck/alerts';
interface Props {
fetchList: Function;
list: any;
onClickEdit: (e) => void;
initAlert: Function;
}
function CustomMetricsWidgets(props: Props) {
const { list } = props;
@ -25,7 +27,10 @@ function CustomMetricsWidgets(props: Props) {
<CustomMetricWidget
metric={item}
onClickEdit={props.onClickEdit}
onAlertClick={(e) => setActiveMetricId(item.metricId)}
onAlertClick={(e) => {
setActiveMetricId(item.metricId)
props.initAlert({ left: item.series.first().seriesId })
}}
/>
))}
@ -40,4 +45,4 @@ function CustomMetricsWidgets(props: Props) {
export default connect(state => ({
list: state.getIn(['customMetrics', 'list']),
}), { fetchList })(CustomMetricsWidgets);
}), { fetchList, initAlert })(CustomMetricsWidgets);

View file

@ -1,7 +1,7 @@
import { connect } from 'react-redux';
import cn from 'classnames';
import withToggle from 'HOCs/withToggle';
import { IconButton, SlideModal, NoContent } from 'UI';
import { IconButton, SlideModal, NoContent, Popup } from 'UI';
import { updateAppearance } from 'Duck/user';
import { WIDGET_LIST } from 'Types/dashboard';
import stl from './addWidgets.css';
@ -76,13 +76,21 @@ export default class AddWidgets extends React.PureComponent {
}
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
<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,8 +2,11 @@ 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, addSeries } from 'Duck/customMetrics';
import { edit as editMetric, save, addSeries, remove } from 'Duck/customMetrics';
import CustomMetricWidgetPreview from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview';
import { confirm } from 'UI/Confirmation';
import { toast } from 'react-toastify';
import cn from 'classnames';
interface Props {
metric: any;
@ -12,6 +15,7 @@ interface Props {
loading: boolean;
addSeries: (series?) => void;
onClose: () => void;
remove: (id) => Promise<void>;
}
function CustomMetricForm(props: Props) {
@ -52,7 +56,23 @@ function CustomMetricForm(props: Props) {
};
const save = () => {
props.save(metric).then(props.onClose);
props.save(metric).then(() => {
toast.success(metric.exists() ? 'Updated succesfully.' : 'Created succesfully.');
props.onClose()
});
}
const deleteHandler = async () => {
if (await confirm({
header: 'Custom Metric',
confirmButton: 'Delete',
confirmation: `Are you sure you want to delete ${metric.name}`
})) {
props.remove(metric.metricId).then(() => {
toast.success('Deleted succesfully.');
props.onClose();
});
}
}
return (
@ -78,7 +98,7 @@ function CustomMetricForm(props: Props) {
<div className="form-group">
<label className="font-medium">Metric Type</label>
<div className="flex items-center">
<span className="bg-white p-1 px-2 border rounded">Timeseries</span>
<span className="bg-white p-1 px-2 border rounded" style={{ height: '30px'}}>Timeseries</span>
<span className="mx-2 color-gray-medium">of</span>
<div>
<SegmentSelection
@ -89,7 +109,7 @@ function CustomMetricForm(props: Props) {
onSelect={ changeConditionTab }
value={{ value: metric.type }}
list={ [
{ name: 'Session Count', value: 'session_count' },
{ name: 'Series 1', value: 'session_count' },
{ name: 'Session Percentage', value: 'session_percentage' },
]}
/>
@ -98,7 +118,7 @@ function CustomMetricForm(props: Props) {
</div>
<div className="form-group">
<label className="font-medium">Series</label>
<label className="font-medium">Chart Series</label>
{metric.series && metric.series.size > 0 && metric.series.map((series: any, index: number) => (
<div className="mb-2">
<FilterSeries
@ -111,19 +131,26 @@ function CustomMetricForm(props: Props) {
))}
</div>
<div className="flex justify-end">
<IconButton type="button" onClick={addSeries} primaryText label="SERIES" icon="plus" />
<div className={cn("flex justify-end -my-4", {'disabled' : metric.series.size > 2})}>
<IconButton hover type="button" onClick={addSeries} primaryText label="SERIES" icon="plus" />
</div>
<div className="my-4" />
<div className="my-8" />
<CustomMetricWidgetPreview metric={metric} />
</div>
<div className="fixed border-t w-full bottom-0 px-5 py-2 bg-white">
<Button loading={loading} primary disabled={!metric.validate()}>
{ `${metric.exists() ? 'Update' : 'Create'}` }
</Button>
<div className="flex items-center fixed border-t w-full bottom-0 px-5 py-2 bg-white">
<div className="mr-auto">
<Button loading={loading} primary disabled={!metric.validate()}>
{ `${metric.exists() ? 'Update' : 'Create'}` }
</Button>
<Button type="button" className="ml-3" outline hover plain onClick={props.onClose}>Cancel</Button>
</div>
<div>
<Button type="button" className="ml-3" outline hover plain onClick={deleteHandler}>Delete</Button>
</div>
</div>
</Form>
);
@ -132,4 +159,4 @@ function CustomMetricForm(props: Props) {
export default connect(state => ({
metric: state.getIn(['customMetrics', 'instance']),
loading: state.getIn(['customMetrics', 'saveRequest', 'loading']),
}), { editMetric, save, addSeries })(CustomMetricForm);
}), { editMetric, save, addSeries, remove })(CustomMetricForm);

View file

@ -15,12 +15,12 @@ function CustomMetrics(props: Props) {
return (
<div className="self-start">
<IconButton outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
<IconButton plain outline icon="plus" label="CREATE METRIC" onClick={() => props.init()} />
<SlideModal
title={
<div className="flex items-center">
<span className="mr-3">{ 'Custom Metric' }</span>
<span className="mr-3">{ metric && metric.exists() ? 'Update Custom Metric' : 'Create Custom Metric' }</span>
</div>
}
isDisplayed={ !!metric }

View file

@ -104,7 +104,7 @@ function FilterSeries(props: Props) {
onChangeEventsOrder={onChangeEventsOrder}
/>
): (
<div className="color-gray-medium">Add user event or filter to build the series.</div>
<div className="color-gray-medium">Add user event or filter to define the series by clicking Add Step.</div>
)}
</div>
<div className="px-5 border-t h-12 flex items-center">

View file

@ -28,16 +28,22 @@ function SeriesName(props: Props) {
// const { name } = props;
return (
<div className="font-medium flex items-center">
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2"
value={name} readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
<div className="flex items-center">
{ editing ? (
<input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2"
value={name}
// readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
/>
) : (
<div className="text-base">{name}</div>
)}
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
</div>
);

View file

@ -49,7 +49,7 @@ function SessionListModal(props: Props) {
<SlideModal
title={ activeWidget && (
<div className="flex items-center">
<div className="mr-auto">{ 'Custom Metric: ' + activeWidget.widget.name } </div>
<div className="mr-auto">{ activeWidget.widget.name } </div>
</div>
)}
isDisplayed={ !!activeWidget }
@ -64,7 +64,7 @@ function SessionListModal(props: Props) {
<TimezoneDropdown />
</div>
<div className="flex items-center ml-6">
<span className="mr-2 color-gray-medium">Sort By</span>
<span className="mr-2 color-gray-medium">Series</span>
<Dropdown
className={stl.dropdown}
direction="left"
@ -82,7 +82,8 @@ function SessionListModal(props: Props) {
</div>
<NoContent
show={ !loading && (filteredSessions.length === 0 || filteredSessions.size === 0 )}
title="No recordings found."
title="No recordings found!"
icon="exclamation-circle"
>
{ filteredSessions.map(session => <SessionItem key={ session.sessionId } session={ session } />) }
</NoContent>

View file

@ -87,7 +87,7 @@ function SaveSearchModal(props: Props) {
</Button>
<Button className={ stl.cancelButton } marginRight onClick={ closeHandler }>{ 'Cancel' }</Button>
</div>
{ savedSearch && <Button className={ stl.cancelButton } marginRight onClick={ onDelete }>
{ savedSearch && <Button noPadding className={ stl.cancelButton } marginRight onClick={ onDelete }>
<Icon name="trash" size="18" />
</Button> }
</Modal.Actions>

View file

@ -89,7 +89,10 @@
}
&.plain {
background-color: transparent;
background-color: transparent !important;
color: $teal !important;
box-shadow: none !important;
padding: 0 10px !important;
&:hover {
background-color: $active-blue;
}

View file

@ -33,7 +33,7 @@ const defaultInstance = CustomMetric({
name: 'New',
series: List([
{
name: 'Session Count',
name: 'Series 1',
filter: new Filter({ filters: [], eventsOrder: 'and' }),
},
])

View file

@ -1,3 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-x" viewBox="0 0 16 16">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-x-lg" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M13.854 2.146a.5.5 0 0 1 0 .708l-11 11a.5.5 0 0 1-.708-.708l11-11a.5.5 0 0 1 .708 0Z"/>
<path fill-rule="evenodd" d="M2.146 2.146a.5.5 0 0 0 0 .708l11 11a.5.5 0 0 0 .708-.708l-11-11a.5.5 0 0 0-.708 0Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 323 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-sliders" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View file

@ -6,7 +6,7 @@ import { capitalize } from 'App/utils';
const countryOptions = Object.keys(countries).map(i => ({ text: countries[i], value: i }));
const ISSUE_OPTIONS = [
{ text: 'Click Range', value: 'click_rage' },
{ text: 'Click Rage', value: 'click_rage' },
{ text: 'Dead Click', value: 'dead_click' },
{ text: 'Excessive Scrolling', value: 'excessive_scrolling' },
{ text: 'Bad Request', value: 'bad_request' },
@ -40,7 +40,7 @@ export const filtersMap = {
[FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
[FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/rev-id' },
[FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/referrer' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/duration' },
[FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/duration' },
[FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/country', options: countryOptions },
// [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
[FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },