feat(ui) - insights - wip

This commit is contained in:
Shekar Siri 2023-01-09 16:13:06 +01:00
parent 2b8e008a18
commit 9b558d444a
13 changed files with 244 additions and 74 deletions

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Icon } from 'UI';
interface Props {
item: any;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
function InsightItem(props: Props) {
const { item, onClick = () => {} } = props;
return (
<div
className="flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer"
onClick={onClick}
>
<Icon name={item.icon} size={20} className="mr-2" color={item.iconColor} />
<div className="mx-1 font-medium">{item.ratio}</div>
<div className="mx-1">on</div>
<div className="mx-1 bg-gray-100 px-2 rounded">Update</div>
<div className="mx-1">increased by</div>
<div className="font-medium text-red">{item.increase}</div>
</div>
);
}
export default InsightItem;

View file

@ -0,0 +1,31 @@
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import React from 'react';
import InsightItem from './InsightItem';
const data = [
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' },
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'yello' },
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'green' },
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'gray' },
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' },
];
interface Props {}
function InsightsCard(props: Props) {
const { metricStore } = useStore();
const metric = metricStore.instance;
const clickHanddler = (e: React.MouseEvent<HTMLDivElement>) => {
console.log(e);
};
return (
<div>
{data.map((item) => (
<InsightItem item={item} onClick={clickHanddler} />
))}
</div>
);
}
export default observer(InsightsCard);

View file

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

View file

@ -13,12 +13,13 @@ import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
import { debounce } from 'App/utils';
import useIsMounted from 'App/hooks/useIsMounted'
import { FilterKey } from 'Types/filter/filterType';
import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS } from 'App/constants/card';
import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS, INSIGHTS } from 'App/constants/card';
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
import SessionWidget from '../Sessions/SessionWidget';
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors';
import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard'
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
interface Props {
metric: any;
@ -193,6 +194,10 @@ function WidgetChart(props: Props) {
)
}
if (metricType === INSIGHTS) {
return <InsightsCard />
}
return <div>Unknown metric type</div>;
}
return (

View file

@ -1,5 +1,5 @@
import React from 'react';
import { metricOf, issueOptions } from 'App/constants/filterOptions';
import React, { useEffect, useState } from 'react';
import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
@ -18,9 +18,11 @@ import {
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
} from 'App/constants/card';
import { clickmapFilter, eventKeys } from 'App/types/filter/newFilter';
import { eventKeys } from 'App/types/filter/newFilter';
import { renderClickmapThumbnail } from './renderMap';
import Widget from 'App/mstore/types/widget';
interface Props {
history: any;
match: any;
@ -37,68 +39,45 @@ function WidgetForm(props: Props) {
const { metricStore, dashboardStore } = useStore();
const isSaving = metricStore.isSaving;
const metric: any = metricStore.instance;
const [initialInstance, setInitialInstance] = useState();
const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
const tableOptions = metricOf.filter((i) => i.type === 'table');
const isTable = metric.metricType === 'table';
const isTable = metric.metricType === TABLE;
const isClickmap = metric.metricType === CLICKMAP;
const isFunnel = metric.metricType === 'funnel';
const isFunnel = metric.metricType === FUNNEL;
const isInsights = metric.metricType === INSIGHTS;
const canAddSeries = metric.series.length < 3;
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length;
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(
metric.metricType
);
const excludeFilterKeys = isClickmap ? eventKeys : []
const excludeFilterKeys = isClickmap ? eventKeys : [];
useEffect(() => {
if (!!metric && !initialInstance) {
setInitialInstance(metric.toJson());
}
}, [metric]);
const writeOption = ({ value, name }: { value: any; name: any }) => {
value = Array.isArray(value) ? value : value.value;
const obj: any = { [name]: value };
if (name === 'metricValue') {
obj.metricValue = value;
if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) {
obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all');
}
}
if (name === 'metricType') {
switch (value) {
case TIMESERIES:
obj.metricOf = timeseriesOptions[0].value;
obj.viewType = 'lineChart';
break;
case TABLE:
obj.metricOf = tableOptions[0].value;
obj.viewType = 'table';
break;
case FUNNEL:
obj.metricOf = 'sessionCount';
break;
case ERRORS:
case RESOURCE_MONITORING:
case PERFORMANCE:
case WEB_VITALS:
obj.viewType = 'chart';
break;
case CLICKMAP:
obj.viewType = 'chart';
if (value !== CLICKMAP) {
metric.series[0].filter.removeFilter(0);
}
if (metric.series[0].filter.filters.length < 1) {
metric.series[0].filter.addFilter({
...clickmapFilter,
value: [''],
});
}
break;
}
}
metricStore.merge(obj);
};
@ -112,10 +91,16 @@ function WidgetForm(props: Props) {
}
}
const savedMetric = await metricStore.save(metric);
setInitialInstance(metric.toJson())
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId));
dashboardStore.addWidgetToDashboard(dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [savedMetric.metricId]);
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
);
dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
}
@ -134,6 +119,11 @@ function WidgetForm(props: Props) {
}
};
const undoChnages = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
return (
<div className="p-6">
<div className="form-group">
@ -142,7 +132,7 @@ function WidgetForm(props: Props) {
<MetricTypeDropdown onSelect={writeOption} />
<MetricSubtypeDropdown onSelect={writeOption} />
{metric.metricOf === FilterKey.ISSUE && (
{metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && (
<>
<span className="mx-3">issue type</span>
<Select
@ -156,6 +146,20 @@ function WidgetForm(props: Props) {
</>
)}
{metric.metricType === INSIGHTS && (
<>
<span className="mx-3">issue category</span>
<Select
name="metricValue"
options={issueCategories}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder="All Categories"
/>
</>
)}
{metric.metricType === 'table' &&
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
<>
@ -183,8 +187,8 @@ function WidgetForm(props: Props) {
{!isPredefined && (
<div className="form-group">
<div className="flex items-center font-medium py-2">
{`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`}
{!isTable && !isFunnel && !isClickmap && (
{`${isTable || isFunnel || isClickmap || isInsights ? 'Filter by' : 'Chart Series'}`}
{!isTable && !isFunnel && !isClickmap && !isInsights && (
<Button
className="ml-2"
variant="text-primary"
@ -198,14 +202,14 @@ function WidgetForm(props: Props) {
{metric.series.length > 0 &&
metric.series
.slice(0, isTable || isFunnel || isClickmap ? 1 : metric.series.length)
.slice(0, isTable || isFunnel || isClickmap || isInsights ? 1 : metric.series.length)
.map((series: any, index: number) => (
<div className="mb-2" key={series.name}>
<FilterSeries
supportsEmpty={!isClickmap}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={isTable || isClickmap}
hideHeader={isTable || isClickmap || isInsights}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
@ -226,13 +230,20 @@ function WidgetForm(props: Props) {
title="Cannot save funnel metric without at least 2 events"
disabled={!cannotSaveFunnel}
>
<Button variant="primary" onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
{metric.exists()
? 'Update'
: parseInt(dashboardId) > 0
? 'Create & Add to Dashboard'
: 'Create'}
</Button>
<div className="flex items-center">
<Button variant="primary" onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
{metric.exists()
? 'Update'
: parseInt(dashboardId) > 0
? 'Create & Add to Dashboard'
: 'Create'}
</Button>
{metric.exists() && metric.hasChanged && (
<Button onClick={undoChnages} variant="text" icon="arrow-counterclockwise" className="ml-2">
Undo
</Button>
)}
</div>
</Tooltip>
<div className="flex items-center">
{metric.exists() && (

View file

@ -43,6 +43,7 @@ function MetricTypeDropdown(props: Props) {
const onChange = (type: string) => {
metricStore.changeType(type);
};
return (
<Select
name="metricType"

View file

@ -13,6 +13,17 @@ import Breadcrumb from 'Shared/Breadcrumb';
import { FilterKey } from 'Types/filter/filterType';
import { Prompt } from 'react-router';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import {
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
} from 'App/constants/card';
interface Props {
history: any;
@ -110,10 +121,11 @@ function WidgetView(props: Props) {
</div>
<WidgetPreview className="mt-8" name={widget.name} />
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
<>
{(widget.metricType === 'table' || widget.metricType === 'timeseries' || widget.metricType === 'clickMap') && <WidgetSessions className="mt-8" />}
{widget.metricType === 'funnel' && <FunnelIssues />}
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) && <WidgetSessions className="mt-8" />}
{widget.metricType === FUNNEL && <FunnelIssues />}
</>
)}
</NoContent>

File diff suppressed because one or more lines are too long

View file

@ -21,6 +21,7 @@ export const WEB_VITALS = 'webVitals';
export const USER_PATH = 'userPath';
export const RETENTION = 'retention';
export const FEATURE_ADOPTION = 'featureAdoption';
export const INSIGHTS = 'insights';
export const TYPES: CardType[] = [
{
@ -222,4 +223,10 @@ export const TYPES: CardType[] = [
description: 'Find the adoption of your all features in your app.',
slug: FEATURE_ADOPTION,
},
{
title: 'Insights',
icon: 'lightbulb',
description: 'Find the adoption of your all features in your app.',
slug: INSIGHTS,
},
];

View file

@ -1,4 +1,4 @@
import { FilterKey, IssueType } from 'Types/filter/filterType';
import { FilterKey, IssueType, IssueCategory } from 'Types/filter/filterType';
// TODO remove text property from options
export const options = [
{ key: 'on', label: 'on', value: 'on' },
@ -118,6 +118,13 @@ export const issueOptions = [
{ label: 'Error', value: IssueType.JS_EXCEPTION },
]
export const issueCategories = [
{ label: 'Resources', value: IssueCategory.RESOURCES },
{ label: 'Network', value: IssueCategory.NETWORK },
{ label: 'Click Rage', value: IssueCategory.CLICK_RAGE },
{ label: 'Errors', value: IssueCategory.ERRORS },
]
export default {
options,
baseOperators,
@ -130,6 +137,7 @@ export default {
metricTypes,
metricOf,
issueOptions,
issueCategories,
methodOptions,
pageUrlOperators,
}

View file

@ -3,7 +3,18 @@ import Widget from './types/widget';
import { metricService, errorService } from 'App/services';
import { toast } from 'react-toastify';
import Error from './types/error';
import { TIMESERIES, TABLE, FUNNEL, ERRORS, RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS } from 'App/constants/card';
import {
TIMESERIES,
TABLE,
FUNNEL,
ERRORS,
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
} from 'App/constants/card';
import { clickmapFilter } from 'App/types/filter/newFilter';
export default class MetricStore {
isLoading: boolean = false;
@ -19,18 +30,20 @@ export default class MetricStore {
sessionsPage: number = 1;
sessionsPageSize: number = 10;
listView?: boolean = true
clickMapFilter: boolean = false
listView?: boolean = true;
clickMapFilter: boolean = false;
clickMapSearch = ''
clickMapLabel = ''
clickMapSearch = '';
clickMapLabel = '';
constructor() {
makeAutoObservable(this);
}
get sortedWidgets() {
return [...this.metrics].sort((a, b) => this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified)
return [...this.metrics].sort((a, b) =>
this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified
);
}
// State Actions
@ -44,35 +57,79 @@ export default class MetricStore {
}
setClickMaps(val: boolean) {
this.clickMapFilter = val
this.clickMapFilter = val;
}
changeClickMapSearch(val: string, label: string) {
this.clickMapSearch = val
this.clickMapLabel = label
this.clickMapSearch = val;
this.clickMapLabel = label;
}
merge(object: any) {
Object.assign(this.instance, object);
this.instance.updateKey('hasChanged', true);
merge(obj: any, updateChangeFlag: boolean = true) {
const type = obj.metricType;
// handle metricType change
if (obj.hasOwnProperty('metricType') && type !== this.instance.metricType) {
this.changeType(type);
}
// handle metricValue change
if (obj.hasOwnProperty('metricValue') && obj.metricValue !== this.instance.metricValue) {
if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) {
obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all');
}
}
Object.assign(this.instance, obj);
this.instance.updateKey('hasChanged', updateChangeFlag);
}
changeType(value: string) {
const obj: any = { metricType: value};
const obj: any = { metricType: value };
obj.series = this.instance.series
obj['metricValue'] = [];
if (value === TABLE || value === TIMESERIES) {
obj['viewType'] = 'table';
}
if (value === TIMESERIES) {
obj['viewType'] = 'lineChart';
}
if (value === ERRORS || value === RESOURCE_MONITORING || value === PERFORMANCE || value === WEB_VITALS) {
if (
value === ERRORS ||
value === RESOURCE_MONITORING ||
value === PERFORMANCE ||
value === WEB_VITALS ||
value === CLICKMAP
) {
obj['viewType'] = 'chart';
}
}
if (value === FUNNEL) {
obj['metricOf'] = 'sessionCount';
}
this.instance.update(obj)
if (value === INSIGHTS) {
obj['metricOf'] = 'issueCategories';
obj['viewType'] = 'list';
}
if (value === CLICKMAP) {
obj.series = obj.series.slice(0, 1)
if (this.instance.metricType !== CLICKMAP) {
obj.series[0].filter.removeFilter(0);
}
if (obj.series[0] && obj.series[0].filter.filters.length < 1) {
obj.series[0].filter.addFilter({
...clickmapFilter,
value: [''],
});
}
}
this.instance.update(obj);
}
reset(id: string) {

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-arrow-counterclockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View file

@ -149,6 +149,13 @@ export enum IssueType {
JS_EXCEPTION = 'js_exception',
}
export enum IssueCategory {
RESOURCES = 'resources',
NETWORK = 'network',
CLICK_RAGE = 'click_rage',
ERRORS = 'errors'
}
export enum FilterType {
STRING = 'STRING',
ISSUE = 'ISSUE',