Merge pull request #919 from openreplay/ui-insights

feat(ui) - insights
This commit is contained in:
Shekar Siri 2023-01-18 15:09:03 +01:00 committed by GitHub
commit 93a9b10266
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1458 additions and 936 deletions

View file

@ -0,0 +1,114 @@
import { IssueCategory } from 'App/types/filter/filterType';
import React from 'react';
import { Icon } from 'UI';
import cn from 'classnames';
interface Props {
item: any;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
}
function InsightItem(props: Props) {
const { item, onClick = () => {} } = props;
const className =
'flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer';
switch (item.category) {
case IssueCategory.RAGE:
return <RageItem onClick={onClick} item={item} className={className} />;
case IssueCategory.RESOURCES:
return <ResourcesItem onClick={onClick} item={item} className={className} />;
case IssueCategory.ERRORS:
return <ErrorItem onClick={onClick} item={item} className={className} />;
case IssueCategory.NETWORK:
return <NetworkItem onClick={onClick} item={item} className={className} />;
default:
return null;
}
}
export default InsightItem;
function Change({ change, isIncreased }: any) {
return (
<div
className={cn('font-medium flex items-center', {
'text-red': isIncreased,
'text-tealx': !isIncreased,
})}
>
<Icon
name={isIncreased ? 'arrow-up-short' : 'arrow-down-short'}
color={isIncreased ? 'red' : 'tealx'}
size={18}
/>
{change}%
</div>
);
}
function ErrorItem({ item, className, onClick }: any) {
return (
<div className={className} onClick={onClick}>
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
{item.isNew ? (
<>
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
<div className="mx-1">error observed</div>
<div className="mx-1 font-medium color-red">{item.ratio}%</div>
<div className="mx-1">more than other new errors</div>
</>
) : (
<>
<div className="mx-1">Increase</div>
<div className="mx-1">in</div>
<div className="mx-1">{item.name}</div>
<Change change={item.change} isIncreased={item.isIncreased} />
</>
)}
</div>
);
}
function NetworkItem({ item, className, onClick }: any) {
return (
<div className={className} onClick={onClick}>
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
<div className="mx-1">Network request</div>
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
<div className="mx-1">{item.change > 0 ? 'increased' : 'decreased'}</div>
<Change change={item.change} isIncreased={item.isIncreased} />
</div>
);
}
function ResourcesItem({ item, className, onClick }: any) {
return (
<div className={className} onClick={onClick}>
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
<div className="mx-1">{item.change > 0 ? 'Inrease' : 'Decrease'}</div>
<div className="mx-1">in</div>
<div className="mx-1 bg-gray-100 px-2 rounded">{item.name}</div>
<Change change={item.change} isIncreased={item.isIncreased} />
</div>
);
}
function RageItem({ item, className, onClick }: any) {
return (
<div className={className} onClick={onClick}>
<Icon name={item.icon} size={18} className="mr-2" color={item.iconColor} />
<div className="mx-1 bg-gray-100 px-2 rounded">{item.isNew ? item.name : 'Click Rage'}</div>
{item.isNew && <div className="mx-1">has</div>}
{!item.isNew && <div className="mx-1">on</div>}
{item.isNew && <div className="font-medium text-red">{item.ratio}%</div>}
{item.isNew && <div className="mx-1">more clickrage than other raged elements.</div>}
{!item.isNew && (
<>
<div className="mx-1">increase by</div>
<Change change={item.change} isIncreased={item.isIncreased} />
</>
)}
</div>
);
}

View file

@ -0,0 +1,80 @@
import { NoContent } from 'UI';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import React from 'react';
import InsightItem from './InsightItem';
import { NO_METRIC_DATA } from 'App/constants/messages';
import { InishtIssue } from 'App/mstore/types/widget';
import { FilterKey, IssueCategory, IssueType } from 'App/types/filter/filterType';
import { filtersMap } from 'Types/filter/newFilter';
function InsightsCard() {
const { metricStore, dashboardStore } = useStore();
const metric = metricStore.instance;
const drillDownFilter = dashboardStore.drillDownFilter;
const period = dashboardStore.period;
const clickHanddler = (e: React.MouseEvent<HTMLDivElement>, item: InishtIssue) => {
let filter: any = {};
switch (item.category) {
case IssueCategory.RESOURCES:
filter = {
...filtersMap[
item.name === IssueType.MEMORY ? FilterKey.AVG_MEMORY_USAGE : FilterKey.AVG_CPU
],
};
filter.source = [item.oldValue];
filter.value = [];
break;
case IssueCategory.RAGE:
filter = { ...filtersMap[FilterKey.CLICK] };
filter.value = [item.name];
break;
case IssueCategory.NETWORK:
filter = { ...filtersMap[FilterKey.FETCH_URL] };
filter.filters = [
{ ...filtersMap[FilterKey.FETCH_URL], value: [item.name] },
{ ...filtersMap[FilterKey.FETCH_DURATION], value: [item.oldValue] },
];
filter.value = [];
break;
case IssueCategory.ERRORS:
filter = { ...filtersMap[FilterKey.ERROR] };
break;
}
filter.type = filter.key;
delete filter.key;
delete filter.operatorOptions;
delete filter.sourceOperatorOptions;
delete filter.placeholder;
delete filter.sourcePlaceholder;
delete filter.sourceType;
delete filter.sourceUnit;
delete filter.category;
delete filter.icon;
delete filter.label;
delete filter.options;
drillDownFilter.merge({
filters: [filter],
});
};
return (
<NoContent
show={metric.data.issues && metric.data.issues.length === 0}
title={NO_METRIC_DATA}
style={{ padding: '100px 0' }}
>
<div className="overflow-y-auto" style={{ maxHeight: '240px' }}>
{metric.data.issues &&
metric.data.issues.map((item: any) => (
<InsightItem item={item} onClick={(e) => clickHanddler(e, item)} />
))}
</div>
</NoContent>
);
}
export default observer(InsightsCard);

View file

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

View file

@ -2,22 +2,14 @@ import { observer } from 'mobx-react-lite';
import React from 'react';
import { NoContent, Pagination } from 'UI';
import { useStore } from 'App/mstore';
import { filterList } from 'App/utils';
import { sliceListPerPage } from 'App/utils';
import DashboardListItem from './DashboardListItem';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
function DashboardList() {
const { dashboardStore } = useStore();
const [shownDashboards, setDashboards] = React.useState([]);
const dashboards = dashboardStore.sortedDashboards;
const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.dashboardsSearch;
React.useEffect(() => {
setDashboards(filterList(dashboards, dashboardsSearch, ['name', 'owner', 'description']));
}, [dashboardsSearch]);
const list = dashboardsSearch !== '' ? shownDashboards : dashboards;
const lenth = list.length;
return (
@ -38,9 +30,6 @@ function DashboardList() {
)}
</div>
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} />
{/* <div className="my-2 bg-active-blue rounded flex items-center justify-center px-80 py-20">
<Icon name="grid-1x2" size={40} color="figmaColors-accent-secondary" />
</div> */}
</div>
}
>

View file

@ -32,7 +32,6 @@ function DashboardListItem(props: Props) {
<div className="link capitalize-first">{dashboard.name}</div>
</div>
</div>
{/* <div><Label className="capitalize">{metric.metricType}</Label></div> */}
<div className="col-span-2">
<div className="flex items-center">
<Icon name={dashboard.isPublic ? 'user-friends' : 'person-fill'} className="mr-2" />

View file

@ -4,33 +4,37 @@ import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
let debounceUpdate: any = () => {};
function DashboardSearch() {
const { dashboardStore } = useStore();
const [query, setQuery] = useState(dashboardStore.dashboardsSearch);
useEffect(() => {
debounceUpdate = debounce((key: string, value: any) => dashboardStore.updateKey(key, value), 500);
}, [])
// @ts-ignore
const write = ({ target: { value } }) => {
setQuery(value);
debounceUpdate('dashboardsSearch', value);
}
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="dashboardsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title or description"
onChange={write}
/>
</div>
const { dashboardStore } = useStore();
const [query, setQuery] = useState(dashboardStore.dashboardsSearch);
useEffect(() => {
debounceUpdate = debounce(
(key: string, value: any) =>
dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value }),
500
);
}, []);
// @ts-ignore
const write = ({ target: { value } }) => {
setQuery(value);
debounceUpdate('dashboardsSearch', value);
};
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="dashboardsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title or description"
onChange={write}
/>
</div>
);
}
export default observer(DashboardSearch);

View file

@ -19,30 +19,49 @@ function Header({ history, siteId }: { history: any; siteId: string }) {
};
return (
<div className="flex items-center mb-4 justify-between px-6">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" />
</div>
<div className="ml-auto flex items-center">
<Button variant="primary" onClick={onAddDashboardClick}>
New Dashboard
</Button>
<div className="mx-2">
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' },
]}
defaultValue={sort.by}
plain
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
/>
<>
<div className="flex items-center mb-4 justify-between px-6">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" />
</div>
<div className="w-1/4" style={{ minWidth: 300 }}>
<DashboardSearch />
<div className="ml-auto flex items-center">
<Button variant="primary" onClick={onAddDashboardClick}>
New Dashboard
</Button>
<div className="mx-2"></div>
<div className="w-1/4" style={{ minWidth: 300 }}>
<DashboardSearch />
</div>
</div>
</div>
</div>
<div className="border-y px-3 py-1 mt-2 flex items-center w-full justify-end gap-4">
<Select
options={[
{ label: 'Visibility - All', value: 'all' },
{ label: 'Visibility - Private', value: 'private' },
{ label: 'Visibility - Team', value: 'team' },
]}
defaultValue={'all'}
plain
onChange={({ value }) =>
dashboardStore.updateKey('filter', {
...dashboardStore.filter,
visibility: value.value,
})
}
/>
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' },
]}
defaultValue={sort.by}
plain
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
/>
</div>
</>
);
}

View file

@ -1,16 +1,15 @@
import React from 'react';
import { Icon, PageTitle, Button, Link, SegmentSelection } from 'UI';
import { Icon, PageTitle, Button, Link, Toggler } from 'UI';
import MetricsSearch from '../MetricsSearch';
import Select from 'Shared/Select';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import { observer, useObserver } from 'mobx-react-lite';
import { DROPDOWN_OPTIONS } from 'App/constants/card';
function MetricViewHeader() {
const { metricStore } = useStore();
const sort = useObserver(() => metricStore.sort);
const listView = useObserver(() => metricStore.listView);
const filter = metricStore.filter;
return (
<div>
<div className="flex items-center mb-4 justify-between px-6">
@ -21,28 +20,6 @@ function MetricViewHeader() {
<Link to={'/metrics/create'}>
<Button variant="primary">New Card</Button>
</Link>
<SegmentSelection
name="viewType"
className="mx-3"
primary
onSelect={ () => metricStore.updateKey('listView', !listView) }
value={{ value: listView ? 'list' : 'grid' }}
list={ [
{ value: 'list', name: '', icon: 'graph-up-arrow' },
{ value: 'grid', name: '', icon: 'hash' },
]}
/>
<div className="mx-2">
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' },
]}
defaultValue={sort.by}
plain
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })}
/>
</div>
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
<MetricsSearch />
</div>
@ -52,8 +29,95 @@ function MetricViewHeader() {
<Icon name="info-circle-fill" className="mr-2" size={16} />
Create custom Cards to capture key interactions and track KPIs.
</div>
<div className="border-y px-3 py-1 mt-2 flex items-center w-full justify-between">
<ListViewToggler />
<div className="items-center flex gap-4">
<Toggler
label="My Cards"
checked={filter.showMine}
name="test"
className="font-medium mr-2"
onChange={() =>
metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine })
}
/>
<Select
options={[{ label: 'All Types', value: 'all' }, ...DROPDOWN_OPTIONS]}
name="type"
defaultValue={filter.type}
onChange={({ value }) =>
metricStore.updateKey('filter', { ...filter, type: value.value })
}
plain={true}
isSearchable={true}
/>
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' },
]}
name="sort"
defaultValue={metricStore.sort.by}
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })}
plain={true}
/>
<DashboardDropdown
plain={true}
onChange={(value: any) =>
metricStore.updateKey('filter', { ...filter, dashboard: value })
}
/>
</div>
</div>
</div>
);
}
export default MetricViewHeader;
export default observer(MetricViewHeader);
function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) {
const { dashboardStore, metricStore } = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
value: i.dashboardId,
}));
return (
<Select
isSearchable={true}
placeholder="Select Dashboard"
plain={plain}
options={dashboardOptions}
value={metricStore.filter.dashboard}
onChange={({ value }: any) => onChange(value)}
isMulti={true}
/>
);
}
function ListViewToggler({}) {
const { metricStore } = useStore();
const listView = useObserver(() => metricStore.listView);
return (
<div className="flex items-center">
<Button
icon="list-alt"
variant={listView ? 'text-primary' : 'text'}
onClick={() => metricStore.updateKey('listView', true)}
>
List
</Button>
<Button
icon="grid"
variant={!listView ? 'text-primary' : 'text'}
onClick={() => metricStore.updateKey('listView', false)}
>
Grid
</Button>
</div>
);
}

View file

@ -70,7 +70,7 @@ function MetricSearch({ onChange }: any) {
function SelectedContent({ dashboardId, selected }: any) {
const { hideModal } = useModal();
const { metricStore, dashboardStore } = useStore();
const total = useObserver(() => metricStore.sortedWidgets.length);
const total = useObserver(() => metricStore.metrics.length);
const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]);
const addSelectedToDashboard = () => {

View file

@ -2,9 +2,7 @@ import { observer, useObserver } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';
import { NoContent, Pagination } from 'UI';
import { useStore } from 'App/mstore';
import { filterList } from 'App/utils';
import { sliceListPerPage } from 'App/utils';
import Widget from 'App/mstore/types/widget';
import GridView from './GridView';
import ListView from './ListView';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@ -17,11 +15,10 @@ function MetricsList({
onSelectionChange?: (selected: any[]) => void;
}) {
const { metricStore } = useStore();
const metrics = metricStore.sortedWidgets;
const cards = metricStore.filteredCards;
const metricsSearch = metricStore.metricsSearch;
const listView = useObserver(() => metricStore.listView);
const [selectedMetrics, setSelectedMetrics] = useState<any>([]);
const sortBy = useObserver(() => metricStore.sort.by);
useEffect(() => {
metricStore.fetchList();
@ -42,17 +39,7 @@ function MetricsList({
}
};
const filterByDashboard = (item: Widget, searchRE: RegExp) => {
const dashboardsStr = item.dashboards.map((d: any) => d.name).join(' ');
return searchRE.test(dashboardsStr);
};
const list =
metricsSearch !== ''
? filterList(metrics, metricsSearch, ['name', 'metricType', 'owner'], filterByDashboard)
: metrics;
const lenth = list.length;
const lenth = cards.length;
useEffect(() => {
metricStore.updateKey('sessionsPage', 1);
@ -74,18 +61,18 @@ function MetricsList({
<ListView
disableSelection={!onSelectionChange}
siteId={siteId}
list={sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize)}
list={sliceListPerPage(cards, metricStore.page - 1, metricStore.pageSize)}
selectedList={selectedMetrics}
toggleSelection={toggleMetricSelection}
allSelected={list.length === selectedMetrics.length}
allSelected={cards.length === selectedMetrics.length}
toggleAll={({ target: { checked, name } }) =>
setSelectedMetrics(checked ? list.map((i: any) => i.metricId) : [])
setSelectedMetrics(checked ? cards.map((i: any) => i.metricId) : [])
}
/>
) : (
<GridView
siteId={siteId}
list={sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize)}
list={sliceListPerPage(cards, metricStore.page - 1, metricStore.pageSize)}
selectedList={selectedMetrics}
toggleSelection={toggleMetricSelection}
/>
@ -94,8 +81,8 @@ function MetricsList({
<div className="w-full flex items-center justify-between py-4 px-6 border-t">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, metricStore.pageSize)}</span> out
of <span className="font-semibold">{list.length}</span> cards
<span className="font-semibold">{Math.min(cards.length, metricStore.pageSize)}</span> out
of <span className="font-semibold">{cards.length}</span> cards
</div>
<Pagination
page={metricStore.page}

View file

@ -4,31 +4,34 @@ import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {}
function MetricsSearch(props) {
const { metricStore } = useStore();
const [query, setQuery] = useState(metricStore.metricsSearch);
useEffect(() => {
debounceUpdate = debounce((key, value) => metricStore.updateKey(key, value), 500);
}, [])
let debounceUpdate: any = () => {};
function MetricsSearch() {
const { metricStore } = useStore();
const [query, setQuery] = useState(metricStore.filter.query);
useEffect(() => {
debounceUpdate = debounce(
(key: any, value: any) => metricStore.updateKey('filter', { ...metricStore.filter, query: value }),
500
);
}, []);
const write = ({ target: { value } }) => {
setQuery(value);
debounceUpdate('metricsSearch', value);
}
return useObserver(() => (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="metricsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title, type, dashboard and owner"
onChange={write}
/>
</div>
));
const write = ({ target: { value } }: any) => {
setQuery(value);
debounceUpdate('metricsSearch', value);
};
return useObserver(() => (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="metricsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title and owner"
onChange={write}
/>
</div>
));
}
export default MetricsSearch;

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;
@ -94,15 +95,7 @@ function WidgetChart(props: Props) {
const renderChart = () => {
const { metricType, viewType, metricOf } = metric;
const metricWithData = { ...metric, data };
if (metricType === 'sessions') {
return <SessionWidget metric={metric} data={data} />
}
// if (metricType === ERRORS) {
// return <ErrorsWidget metric={metric} data={data} />
// }
if (metricType === FUNNEL) {
return <FunnelWidget metric={metric} data={data} isWidget={isWidget || isTemplate} />
@ -193,6 +186,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));
}
@ -127,13 +112,18 @@ function WidgetForm(props: Props) {
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this metric?`,
confirmation: `Are you sure you want to permanently delete this card?`,
})
) {
metricStore.delete(metric).then(props.onDelete);
}
};
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

@ -1,7 +1,6 @@
import React, { useMemo } from 'react';
import { TYPES, LIBRARY } from 'App/constants/card';
import React from 'react';
import { DROPDOWN_OPTIONS, Option } from 'App/constants/card';
import Select from 'Shared/Select';
import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem';
import { components } from 'react-select';
import CustomDropdownOption from 'Shared/CustomDropdownOption';
import { observer } from 'mobx-react-lite';
@ -22,20 +21,11 @@ interface Props {
function MetricTypeDropdown(props: Props) {
const { metricStore } = useStore();
const metric: any = metricStore.instance;
const options: Options[] = useMemo(() => {
// TYPES.shift(); // remove "Add from library" item
return TYPES.filter((i: MetricType) => i.slug !== LIBRARY).map((i: MetricType) => ({
label: i.title,
icon: i.icon,
value: i.slug,
description: i.description,
}));
}, []);
React.useEffect(() => {
const queryCardType = props.query.get('type');
if (queryCardType && options.length > 0 && metric.metricType) {
const type = options.find((i) => i.value === queryCardType);
if (queryCardType && DROPDOWN_OPTIONS.length > 0 && metric.metricType) {
const type: Option = DROPDOWN_OPTIONS.find((i) => i.value === queryCardType) as Option;
setTimeout(() => onChange(type.value), 0);
}
}, []);
@ -43,17 +33,21 @@ function MetricTypeDropdown(props: Props) {
const onChange = (type: string) => {
metricStore.changeType(type);
};
return (
<Select
name="metricType"
placeholder="Select Card Type"
options={options}
value={options.find((i: any) => i.value === metric.metricType) || options[0]}
options={DROPDOWN_OPTIONS}
value={
DROPDOWN_OPTIONS.find((i: any) => i.value === metric.metricType) || DROPDOWN_OPTIONS[0]
}
onChange={props.onSelect}
// onSelect={onSelect}
components={{
SingleValue: ({ children, ...props }: any) => {
const { data: { icon, label } } = props;
const {
data: { icon, label },
} = props;
return (
<components.SingleValue {...props}>
<div className="flex items-center">

View file

@ -81,7 +81,6 @@ function WidgetSessions(props: Props) {
const customFilter = { ...filter, ...timeRange, filters: [ ...sessionStore.userFilter.filters, clickFilter]}
debounceClickMapSearch(customFilter)
} else {
console.log(widget)
debounceRequest(widget.metricId, {
...filter,
series: widget.series,

View file

@ -13,6 +13,13 @@ 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,
INSIGHTS,
} from 'App/constants/card';
interface Props {
history: any;
@ -110,10 +117,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

@ -13,7 +13,7 @@
& span {
padding-left: 10px;
color: $gray-medium;
/* color: $gray-dark; */
}
}
.switch input {

View file

@ -1,5 +1,6 @@
import { IconNames } from 'App/components/ui/SVG';
import { FilterKey } from 'Types/filter/filterType';
import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem';
export interface CardType {
title: string;
@ -21,6 +22,14 @@ export const WEB_VITALS = 'webVitals';
export const USER_PATH = 'userPath';
export const RETENTION = 'retention';
export const FEATURE_ADOPTION = 'featureAdoption';
export const INSIGHTS = 'insights';
export interface Option {
label: string;
icon: string;
value: string;
description: string;
}
export const TYPES: CardType[] = [
{
@ -34,9 +43,7 @@ export const TYPES: CardType[] = [
icon: 'puzzle-piece',
description: 'Track the features that are being used the most.',
slug: CLICKMAP,
subTypes: [
{ title: 'Visited URL', slug: FilterKey.CLICKMAP_URL, description: "" },
]
subTypes: [{ title: 'Visited URL', slug: FilterKey.CLICKMAP_URL, description: '' }],
},
{
title: 'Timeseries',
@ -222,4 +229,19 @@ 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,
},
];
export const DROPDOWN_OPTIONS = TYPES.filter((i: MetricType) => i.slug !== LIBRARY).map(
(i: MetricType) => ({
label: i.title,
icon: i.icon,
value: i.slug,
description: i.description,
})
);

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,18 @@ export const issueOptions = [
{ label: 'Error', value: IssueType.JS_EXCEPTION },
]
export const issueCategories = [
{ label: 'Resources', value: IssueCategory.RESOURCES },
{ label: 'Network', value: IssueCategory.NETWORK },
{ label: 'Rage', value: IssueCategory.RAGE },
{ label: 'Errors', value: IssueCategory.ERRORS },
]
export const issueCategoriesMap = issueCategories.reduce((acc, {value, label}) => {
acc[value] = label;
return acc;
}, {})
export default {
options,
baseOperators,
@ -130,6 +142,7 @@ export default {
metricTypes,
metricOf,
issueOptions,
issueCategories,
methodOptions,
pageUrlOperators,
}

View file

@ -1,513 +1,432 @@
import {
makeAutoObservable,
runInAction,
} from "mobx";
import Dashboard from "./types/dashboard";
import Widget from "./types/widget";
import { dashboardService, metricService } from "App/services";
import { toast } from "react-toastify";
import Period, {
LAST_24_HOURS,
LAST_7_DAYS,
} from "Types/app/period";
import { getChartFormatter } from "Types/dashboard/helper";
import Filter from "./types/filter";
import Funnel from "./types/funnel";
import Session from "./types/session";
import Error from "./types/error";
import { FilterKey } from "Types/filter/filterType";
import { makeAutoObservable, runInAction } from 'mobx';
import Dashboard from './types/dashboard';
import Widget from './types/widget';
import { dashboardService, metricService } from 'App/services';
import { toast } from 'react-toastify';
import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period';
import Filter from './types/filter';
import { getRE } from 'App/utils';
export default class DashboardStore {
siteId: any = null;
dashboards: Dashboard[] = [];
selectedDashboard: Dashboard | null = null;
dashboardInstance: Dashboard = new Dashboard();
selectedWidgets: Widget[] = [];
currentWidget: Widget = new Widget();
widgetCategories: any[] = [];
widgets: Widget[] = [];
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
drillDownFilter: Filter = new Filter();
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_7_DAYS });
startTimestamp: number = 0;
endTimestamp: number = 0;
pendingRequests: number = 0;
// Metrics
metricsPage: number = 1;
metricsPageSize: number = 10;
metricsSearch: string = "";
// Loading states
isLoading: boolean = true;
isSaving: boolean = false;
isDeleting: boolean = false;
loadingTemplates: boolean = false
fetchingDashboard: boolean = false;
sessionsLoading: boolean = false;
showAlertModal: boolean = false;
// Pagination
page: number = 1
pageSize: number = 10
dashboardsSearch: string = ''
sort: any = { by: 'desc'}
constructor() {
makeAutoObservable(this);
this.drillDownPeriod = Period({ rangeName: LAST_7_DAYS });
const timeStamps = this.drillDownPeriod.toTimestamps();
this.drillDownFilter.updateKey(
"startTimestamp",
timeStamps.startTimestamp
);
this.drillDownFilter.updateKey("endTimestamp", timeStamps.endTimestamp);
}
get sortedDashboards() {
const sortOrder = this.sort.by
return [...this.dashboards].sort((a, b) => sortOrder === 'desc' ? b.createdAt - a.createdAt : a.createdAt - b.createdAt)
}
toggleAllSelectedWidgets(isSelected: boolean) {
if (isSelected) {
const allWidgets = this.widgetCategories.reduce((acc, cat) => {
return acc.concat(cat.widgets);
}, []);
this.selectedWidgets = allWidgets;
} else {
this.selectedWidgets = [];
}
}
selectWidgetsByCategory(category: string) {
const selectedWidgetIds = this.selectedWidgets.map(
(widget: any) => widget.metricId
);
const widgets = this.widgetCategories
.find((cat) => cat.name === category)
?.widgets.filter(
(widget: any) => !selectedWidgetIds.includes(widget.metricId)
);
this.selectedWidgets = this.selectedWidgets.concat(widgets) || [];
}
removeSelectedWidgetByCategory = (category: any) => {
const categoryWidgetIds = category.widgets.map((w: Widget) => w.metricId);
this.selectedWidgets = this.selectedWidgets.filter(
(widget: any) => !categoryWidgetIds.includes(widget.metricId)
);
};
toggleWidgetSelection = (widget: any) => {
const selectedWidgetIds = this.selectedWidgets.map(
(widget: any) => widget.metricId
);
if (selectedWidgetIds.includes(widget.metricId)) {
this.selectedWidgets = this.selectedWidgets.filter(
(w: any) => w.metricId !== widget.metricId
);
} else {
this.selectedWidgets.push(widget);
}
};
findByIds(ids: string[]) {
return this.dashboards.filter((d) => ids.includes(d.dashboardId));
}
initDashboard(dashboard?: Dashboard) {
this.dashboardInstance = dashboard
? new Dashboard().fromJson(dashboard)
: new Dashboard();
this.selectedWidgets = [];
}
updateKey(key: string, value: any) {
// @ts-ignore
this[key] = value;
}
resetCurrentWidget() {
this.currentWidget = new Widget();
}
editWidget(widget: any) {
this.currentWidget.update(widget);
}
fetchList(): Promise<any> {
this.isLoading = true;
return dashboardService
.getDashboards()
.then((list: any) => {
runInAction(() => {
this.dashboards = list.map((d: Record<string, any>) =>
new Dashboard().fromJson(d)
);
});
})
.finally(() => {
runInAction(() => {
this.isLoading = false;
});
});
}
fetch(dashboardId: string): Promise<any> {
this.setFetchingDashboard(true);
return dashboardService
.getDashboard(dashboardId)
.then((response) => {
this.selectedDashboard?.update({
widgets: new Dashboard().fromJson(response).widgets,
});
})
.finally(() => {
this.setFetchingDashboard(false);
});
}
setFetchingDashboard(value: boolean) {
this.fetchingDashboard = value;
}
save(dashboard: Dashboard): Promise<any> {
this.isSaving = true;
const isCreating = !dashboard.dashboardId;
dashboard.metrics = this.selectedWidgets.map((w) => w.metricId);
return new Promise((resolve, reject) => {
dashboardService
.saveDashboard(dashboard)
.then((_dashboard) => {
runInAction(() => {
if (isCreating) {
toast.success("Dashboard created successfully");
this.addDashboard(
new Dashboard().fromJson(_dashboard)
);
} else {
toast.success("Dashboard successfully updated ");
this.updateDashboard(
new Dashboard().fromJson(_dashboard)
);
}
resolve(_dashboard);
});
})
.catch((error) => {
toast.error("Error saving dashboard");
reject();
})
.finally(() => {
runInAction(() => {
this.isSaving = false;
});
});
});
}
saveMetric(metric: Widget, dashboardId: string): Promise<any> {
const isCreating = !metric.widgetId;
return dashboardService
.saveMetric(metric, dashboardId)
.then((metric) => {
runInAction(() => {
if (isCreating) {
this.selectedDashboard?.widgets.push(metric);
} else {
this.selectedDashboard?.widgets.map((w) => {
if (w.widgetId === metric.widgetId) {
w.update(metric);
}
});
}
});
});
}
deleteDashboard(dashboard: Dashboard): Promise<any> {
this.isDeleting = true;
return dashboardService
.deleteDashboard(dashboard.dashboardId)
.then(() => {
toast.success("Dashboard deleted successfully");
runInAction(() => {
this.removeDashboard(dashboard);
});
})
.catch(() => {
toast.error("Dashboard could not be deleted");
})
.finally(() => {
runInAction(() => {
this.isDeleting = false;
});
});
}
toJson() {
return {
dashboards: this.dashboards.map((d) => d.toJson()),
};
}
fromJson(json: any) {
runInAction(() => {
this.dashboards = json.dashboards.map((d: Record<string, any>) =>
new Dashboard().fromJson(d)
);
});
return this;
}
addDashboard(dashboard: Dashboard) {
this.dashboards.push(new Dashboard().fromJson(dashboard));
}
removeDashboard(dashboard: Dashboard) {
this.dashboards = this.dashboards.filter(
(d) => d.dashboardId !== dashboard.dashboardId
);
}
getDashboard(dashboardId: string|number): Dashboard | null {
return (
this.dashboards.find((d) => d.dashboardId == dashboardId) || null
);
}
getDashboardByIndex(index: number) {
return this.dashboards[index];
}
getDashboardCount() {
return this.dashboards.length;
}
updateDashboard(dashboard: Dashboard) {
const index = this.dashboards.findIndex(
(d) => d.dashboardId === dashboard.dashboardId
);
if (index >= 0) {
this.dashboards[index] = dashboard;
if (this.selectedDashboard?.dashboardId === dashboard.dashboardId) {
this.selectDashboardById(dashboard.dashboardId);
}
}
}
selectDashboardById = (dashboardId: any) => {
this.selectedDashboard =
this.dashboards.find((d) => d.dashboardId == dashboardId) ||
new Dashboard();
};
getDashboardById = (dashboardId: string) => {
const dashboard = this.dashboards.find((d) => d.dashboardId == dashboardId)
if (dashboard) {
this.selectedDashboard = dashboard
return true;
} else {
this.selectedDashboard = null
return false;
}
}
setSiteId = (siteId: any) => {
this.siteId = siteId;
};
fetchTemplates(hardRefresh: boolean): Promise<any> {
this.loadingTemplates = true
return new Promise((resolve, reject) => {
if (this.widgetCategories.length > 0 && !hardRefresh) {
resolve(this.widgetCategories);
} else {
metricService
.getTemplates()
.then((response) => {
const categories: any[] = [];
response.forEach((category: any) => {
const widgets: any[] = [];
category.widgets
.forEach((widget: any) => {
const w = new Widget().fromJson(widget);
widgets.push(w);
});
const c: any = {};
c.widgets = widgets;
c.name = category.category;
c.description = category.description;
categories.push(c);
});
this.widgetCategories = categories;
resolve(this.widgetCategories);
})
.catch((error) => {
reject(error);
}).finally(() => {
this.loadingTemplates = false
});
}
});
}
deleteDashboardWidget(dashboardId: string, widgetId: string) {
this.isDeleting = true;
return dashboardService
.deleteWidget(dashboardId, widgetId)
.then(() => {
toast.success("Dashboard updated successfully");
runInAction(() => {
this.selectedDashboard?.removeWidget(widgetId);
});
})
.finally(() => {
this.isDeleting = false;
});
}
addWidgetToDashboard(dashboard: Dashboard, metricIds: any): Promise<any> {
this.isSaving = true;
return dashboardService
.addWidget(dashboard, metricIds)
.then((response) => {
toast.success("Card added to dashboard.");
})
.catch(() => {
toast.error("Card could not be added.");
})
.finally(() => {
this.isSaving = false;
});
}
setPeriod(period: any) {
this.period = Period({
start: period.start,
end: period.end,
rangeName: period.rangeName,
});
}
setDrillDownPeriod(period: any) {
this.drillDownPeriod = Period({
start: period.start,
end: period.end,
rangeName: period.rangeName,
});
}
toggleAlertModal(val: boolean) {
this.showAlertModal = val
}
fetchMetricChartData(
metric: Widget,
data: any,
isWidget: boolean = false,
period: Record<string, any>
): Promise<any> {
period = period.toTimestamps();
const params = { ...period, ...data, key: metric.predefinedKey };
if (metric.page && metric.limit) {
params["page"] = metric.page;
params["limit"] = metric.limit;
}
return new Promise((resolve, reject) => {
this.pendingRequests += 1
return metricService
.getMetricChartData(metric, params, isWidget)
.then((data: any) => {
if (
metric.metricType === "predefined" &&
metric.viewType === "overview"
) {
const _data = {
...data,
chart: getChartFormatter(period)(data.chart),
};
metric.setData(_data);
resolve(_data);
} else if (metric.metricType === "funnel") {
const _data = { ...data };
_data.funnel = new Funnel().fromJSON(data);
metric.setData(_data);
resolve(_data);
} else {
const _data = {
...data,
};
// TODO refactor to widget class
if (metric.metricOf === FilterKey.SESSIONS) {
_data["sessions"] = data.sessions.map((s: any) =>
new Session().fromJson(s)
);
} else if (metric.metricOf === FilterKey.ERRORS) {
_data["errors"] = data.errors.map((s: any) =>
new Error().fromJSON(s)
);
} else {
if (data.hasOwnProperty("chart")) {
_data["chart"] = getChartFormatter(period)(
data.chart
);
_data["namesMap"] = data.chart
.map((i: any) => Object.keys(i))
.flat()
.filter(
(i: any) => i !== "time" && i !== "timestamp"
)
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, []);
} else {
_data["chart"] = getChartFormatter(period)(
Array.isArray(data) ? data : []
);
_data["namesMap"] = Array.isArray(data)
? data
.map((i) => Object.keys(i))
.flat()
.filter(
(i) =>
i !== "time" &&
i !== "timestamp"
)
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, [])
: [];
}
}
metric.setData(_data);
resolve(_data);
}
})
.catch((err: any) => {
reject(err);
}).finally(() => {
setTimeout(() => {
this.pendingRequests = this.pendingRequests - 1
}, 100)
});
});
}
interface DashboardFilter {
query?: string;
visibility?: string;
}
export default class DashboardStore {
siteId: any = null;
dashboards: Dashboard[] = [];
selectedDashboard: Dashboard | null = null;
dashboardInstance: Dashboard = new Dashboard();
selectedWidgets: Widget[] = [];
currentWidget: Widget = new Widget();
widgetCategories: any[] = [];
widgets: Widget[] = [];
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS });
drillDownFilter: Filter = new Filter();
drillDownPeriod: Record<string, any> = Period({ rangeName: LAST_7_DAYS });
startTimestamp: number = 0;
endTimestamp: number = 0;
pendingRequests: number = 0;
filter: DashboardFilter = { visibility: 'all', query: '' };
// Metrics
metricsPage: number = 1;
metricsPageSize: number = 10;
metricsSearch: string = '';
// Loading states
isLoading: boolean = true;
isSaving: boolean = false;
isDeleting: boolean = false;
loadingTemplates: boolean = false;
fetchingDashboard: boolean = false;
sessionsLoading: boolean = false;
showAlertModal: boolean = false;
// Pagination
page: number = 1;
pageSize: number = 10;
dashboardsSearch: string = '';
sort: any = { by: 'desc' };
constructor() {
makeAutoObservable(this);
this.drillDownPeriod = Period({ rangeName: LAST_7_DAYS });
const timeStamps = this.drillDownPeriod.toTimestamps();
this.drillDownFilter.updateKey('startTimestamp', timeStamps.startTimestamp);
this.drillDownFilter.updateKey('endTimestamp', timeStamps.endTimestamp);
}
get sortedDashboards() {
const sortOrder = this.sort.by;
return [...this.dashboards].sort((a, b) =>
sortOrder === 'desc' ? b.createdAt - a.createdAt : a.createdAt - b.createdAt
);
}
get filteredList() {
const filterRE = this.filter.query ? getRE(this.filter.query, 'i') : null;
return this.dashboards
.filter(
(dashboard) =>
(this.filter.visibility === 'all' ||
(this.filter.visibility === 'team' ? dashboard.isPublic : !dashboard.isPublic)) &&
(!filterRE ||
// @ts-ignore
['name', 'owner', 'description'].some((key) => filterRE.test(dashboard[key])))
)
.sort((a, b) =>
this.sort.by === 'desc' ? b.createdAt - a.createdAt : a.createdAt - b.createdAt
);
}
toggleAllSelectedWidgets(isSelected: boolean) {
if (isSelected) {
const allWidgets = this.widgetCategories.reduce((acc, cat) => {
return acc.concat(cat.widgets);
}, []);
this.selectedWidgets = allWidgets;
} else {
this.selectedWidgets = [];
}
}
selectWidgetsByCategory(category: string) {
const selectedWidgetIds = this.selectedWidgets.map((widget: any) => widget.metricId);
const widgets = this.widgetCategories
.find((cat) => cat.name === category)
?.widgets.filter((widget: any) => !selectedWidgetIds.includes(widget.metricId));
this.selectedWidgets = this.selectedWidgets.concat(widgets) || [];
}
removeSelectedWidgetByCategory = (category: any) => {
const categoryWidgetIds = category.widgets.map((w: Widget) => w.metricId);
this.selectedWidgets = this.selectedWidgets.filter(
(widget: any) => !categoryWidgetIds.includes(widget.metricId)
);
};
toggleWidgetSelection = (widget: any) => {
const selectedWidgetIds = this.selectedWidgets.map((widget: any) => widget.metricId);
if (selectedWidgetIds.includes(widget.metricId)) {
this.selectedWidgets = this.selectedWidgets.filter(
(w: any) => w.metricId !== widget.metricId
);
} else {
this.selectedWidgets.push(widget);
}
};
findByIds(ids: string[]) {
return this.dashboards.filter((d) => ids.includes(d.dashboardId));
}
initDashboard(dashboard?: Dashboard) {
this.dashboardInstance = dashboard ? new Dashboard().fromJson(dashboard) : new Dashboard();
this.selectedWidgets = [];
}
updateKey(key: string, value: any) {
// @ts-ignore
this[key] = value;
}
resetCurrentWidget() {
this.currentWidget = new Widget();
}
editWidget(widget: any) {
this.currentWidget.update(widget);
}
fetchList(): Promise<any> {
this.isLoading = true;
return dashboardService
.getDashboards()
.then((list: any) => {
runInAction(() => {
this.dashboards = list.map((d: Record<string, any>) => new Dashboard().fromJson(d));
});
})
.finally(() => {
runInAction(() => {
this.isLoading = false;
});
});
}
fetch(dashboardId: string): Promise<any> {
this.setFetchingDashboard(true);
return dashboardService
.getDashboard(dashboardId)
.then((response) => {
this.selectedDashboard?.update({
widgets: new Dashboard().fromJson(response).widgets,
});
})
.finally(() => {
this.setFetchingDashboard(false);
});
}
setFetchingDashboard(value: boolean) {
this.fetchingDashboard = value;
}
save(dashboard: Dashboard): Promise<any> {
this.isSaving = true;
const isCreating = !dashboard.dashboardId;
dashboard.metrics = this.selectedWidgets.map((w) => w.metricId);
return new Promise((resolve, reject) => {
dashboardService
.saveDashboard(dashboard)
.then((_dashboard) => {
runInAction(() => {
if (isCreating) {
toast.success('Dashboard created successfully');
this.addDashboard(new Dashboard().fromJson(_dashboard));
} else {
toast.success('Dashboard successfully updated ');
this.updateDashboard(new Dashboard().fromJson(_dashboard));
}
resolve(_dashboard);
});
})
.catch((error) => {
toast.error('Error saving dashboard');
reject();
})
.finally(() => {
runInAction(() => {
this.isSaving = false;
});
});
});
}
saveMetric(metric: Widget, dashboardId: string): Promise<any> {
const isCreating = !metric.widgetId;
return dashboardService.saveMetric(metric, dashboardId).then((metric) => {
runInAction(() => {
if (isCreating) {
this.selectedDashboard?.widgets.push(metric);
} else {
this.selectedDashboard?.widgets.map((w) => {
if (w.widgetId === metric.widgetId) {
w.update(metric);
}
});
}
});
});
}
deleteDashboard(dashboard: Dashboard): Promise<any> {
this.isDeleting = true;
return dashboardService
.deleteDashboard(dashboard.dashboardId)
.then(() => {
toast.success('Dashboard deleted successfully');
runInAction(() => {
this.removeDashboard(dashboard);
});
})
.catch(() => {
toast.error('Dashboard could not be deleted');
})
.finally(() => {
runInAction(() => {
this.isDeleting = false;
});
});
}
toJson() {
return {
dashboards: this.dashboards.map((d) => d.toJson()),
};
}
fromJson(json: any) {
runInAction(() => {
this.dashboards = json.dashboards.map((d: Record<string, any>) =>
new Dashboard().fromJson(d)
);
});
return this;
}
addDashboard(dashboard: Dashboard) {
this.dashboards.push(new Dashboard().fromJson(dashboard));
}
removeDashboard(dashboard: Dashboard) {
this.dashboards = this.dashboards.filter((d) => d.dashboardId !== dashboard.dashboardId);
}
getDashboard(dashboardId: string | number): Dashboard | null {
return this.dashboards.find((d) => d.dashboardId == dashboardId) || null;
}
getDashboardByIndex(index: number) {
return this.dashboards[index];
}
getDashboardCount() {
return this.dashboards.length;
}
updateDashboard(dashboard: Dashboard) {
const index = this.dashboards.findIndex((d) => d.dashboardId === dashboard.dashboardId);
if (index >= 0) {
this.dashboards[index] = dashboard;
if (this.selectedDashboard?.dashboardId === dashboard.dashboardId) {
this.selectDashboardById(dashboard.dashboardId);
}
}
}
selectDashboardById = (dashboardId: any) => {
this.selectedDashboard =
this.dashboards.find((d) => d.dashboardId == dashboardId) || new Dashboard();
};
getDashboardById = (dashboardId: string) => {
const dashboard = this.dashboards.find((d) => d.dashboardId == dashboardId);
if (dashboard) {
this.selectedDashboard = dashboard;
return true;
} else {
this.selectedDashboard = null;
return false;
}
};
setSiteId = (siteId: any) => {
this.siteId = siteId;
};
fetchTemplates(hardRefresh: boolean): Promise<any> {
this.loadingTemplates = true;
return new Promise((resolve, reject) => {
if (this.widgetCategories.length > 0 && !hardRefresh) {
resolve(this.widgetCategories);
} else {
metricService
.getTemplates()
.then((response) => {
const categories: any[] = [];
response.forEach((category: any) => {
const widgets: any[] = [];
category.widgets.forEach((widget: any) => {
const w = new Widget().fromJson(widget);
widgets.push(w);
});
const c: any = {};
c.widgets = widgets;
c.name = category.category;
c.description = category.description;
categories.push(c);
});
this.widgetCategories = categories;
resolve(this.widgetCategories);
})
.catch((error) => {
reject(error);
})
.finally(() => {
this.loadingTemplates = false;
});
}
});
}
deleteDashboardWidget(dashboardId: string, widgetId: string) {
this.isDeleting = true;
return dashboardService
.deleteWidget(dashboardId, widgetId)
.then(() => {
toast.success('Dashboard updated successfully');
runInAction(() => {
this.selectedDashboard?.removeWidget(widgetId);
});
})
.finally(() => {
this.isDeleting = false;
});
}
addWidgetToDashboard(dashboard: Dashboard, metricIds: any): Promise<any> {
this.isSaving = true;
return dashboardService
.addWidget(dashboard, metricIds)
.then((response) => {
toast.success('Card added to dashboard.');
})
.catch(() => {
toast.error('Card could not be added.');
})
.finally(() => {
this.isSaving = false;
});
}
setPeriod(period: any) {
this.period = Period({
start: period.start,
end: period.end,
rangeName: period.rangeName,
});
}
setDrillDownPeriod(period: any) {
this.drillDownPeriod = Period({
start: period.start,
end: period.end,
rangeName: period.rangeName,
});
}
toggleAlertModal(val: boolean) {
this.showAlertModal = val
}
fetchMetricChartData(
metric: Widget,
data: any,
isWidget: boolean = false,
period: Record<string, any>
): Promise<any> {
period = period.toTimestamps();
const params = { ...period, ...data, key: metric.predefinedKey };
if (metric.page && metric.limit) {
params['page'] = metric.page;
params['limit'] = metric.limit;
}
return new Promise((resolve, reject) => {
this.pendingRequests += 1;
return metricService
.getMetricChartData(metric, params, isWidget)
.then((data: any) => {
metric.setData(data, period);
resolve(metric.data);
})
.catch((err: any) => {
reject(err);
})
.finally(() => {
setTimeout(() => {
this.pendingRequests = this.pendingRequests - 1;
}, 100);
});
});
}
}

View file

@ -3,8 +3,25 @@ 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';
import { getRE } from 'App/utils';
interface MetricFilter {
query?: string;
showMine?: boolean;
type?: string;
dashboard?: [];
}
export default class MetricStore {
isLoading: boolean = false;
isSaving: boolean = false;
@ -17,20 +34,44 @@ export default class MetricStore {
metricsSearch: string = '';
sort: any = { by: 'desc' };
filter: MetricFilter = { type: 'all', dashboard: [], query: '' };
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
);
}
get filteredCards() {
const filterRE = this.filter.query ? getRE(this.filter.query, 'i') : null;
const dbIds = this.filter.dashboard ? this.filter.dashboard.map((i: any) => i.value) : [];
return this.metrics
.filter(
(card) =>
(this.filter.showMine
? card.owner === JSON.parse(localStorage.getItem('user')!).account.email
: true) &&
(this.filter.type === 'all' || card.metricType === this.filter.type) &&
(!dbIds.length ||
card.dashboards.map((i) => i.dashboardId).some((id) => dbIds.includes(id))) &&
// @ts-ignore
(!filterRE || ['name', 'owner'].some((key) => filterRE.test(card[key])))
)
.sort((a, b) =>
this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified
);
}
// State Actions
@ -44,35 +85,78 @@ 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

@ -1,184 +1,306 @@
import { makeAutoObservable, runInAction } from "mobx"
import FilterSeries from "./filterSeries";
import { makeAutoObservable, runInAction } from 'mobx';
import FilterSeries from './filterSeries';
import { DateTime } from 'luxon';
import Session from "App/mstore/types/session";
import Session from 'App/mstore/types/session';
import Funnelissue from 'App/mstore/types/funnelIssue';
import { issueOptions } from 'App/constants/filterOptions';
import { issueOptions, issueCategories, issueCategoriesMap } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import Period, { LAST_24_HOURS } from 'Types/app/period';
import { metricService } from "App/services";
import { WEB_VITALS } from "App/constants/card";
import { metricService } from 'App/services';
import { INSIGHTS, TABLE, WEB_VITALS } from 'App/constants/card';
import Error from '../types/error';
import { getChartFormatter } from 'Types/dashboard/helper';
export class InishtIssue {
icon: string;
iconColor: string;
change: number;
isNew = false;
category: string;
label: string;
value: number;
oldValue: number;
isIncreased?: boolean;
constructor(
category: string,
public name: string,
public ratio: number,
oldValue = 0,
value = 0,
change = 0,
isNew = false
) {
this.category = category;
this.value = Math.round(value);
this.oldValue = Math.round(oldValue);
// @ts-ignore
this.label = issueCategoriesMap[category];
this.icon = `ic-${category}`;
this.iconColor = 'red';
this.change = parseInt(change.toFixed(2));
this.isIncreased = this.change > 0;
this.isNew = isNew;
}
}
export default class Widget {
public static get ID_KEY():string { return "metricId" }
metricId: any = undefined
widgetId: any = undefined
category?: string = undefined
name: string = "Untitled Card"
metricType: string = "timeseries"
metricOf: string = "sessionCount"
metricValue: string = ""
viewType: string = "lineChart"
metricFormat: string = "sessionCount"
series: FilterSeries[] = []
sessions: [] = []
isPublic: boolean = true
owner: string = ""
lastModified: number = new Date().getTime()
dashboards: any[] = []
dashboardIds: any[] = []
config: any = {}
page: number = 1
limit: number = 5
thumbnail?: string
params: any = { density: 70 }
public static get ID_KEY(): string {
return 'metricId';
}
metricId: any = undefined;
widgetId: any = undefined;
category?: string = undefined;
name: string = 'Untitled Card';
metricType: string = 'timeseries';
metricOf: string = 'sessionCount';
metricValue: string = '';
viewType: string = 'lineChart';
metricFormat: string = 'sessionCount';
series: FilterSeries[] = [];
sessions: [] = [];
isPublic: boolean = true;
owner: string = '';
lastModified: number = new Date().getTime();
dashboards: any[] = [];
dashboardIds: any[] = [];
config: any = {};
page: number = 1;
limit: number = 5;
thumbnail?: string;
params: any = { density: 70 };
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS }) // temp value in detail view
hasChanged: boolean = false
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS }); // temp value in detail view
hasChanged: boolean = false;
position: number = 0
data: any = {
sessions: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
percentiles: [],
}
isLoading: boolean = false
isValid: boolean = false
dashboardId: any = undefined
predefinedKey: string = ''
position: number = 0;
data: any = {
sessions: [],
issues: [],
total: 0,
chart: [],
namesMap: {},
avg: 0,
percentiles: [],
};
isLoading: boolean = false;
isValid: boolean = false;
dashboardId: any = undefined;
predefinedKey: string = '';
constructor() {
makeAutoObservable(this)
constructor() {
makeAutoObservable(this);
const filterSeries = new FilterSeries()
this.series.push(filterSeries)
}
const filterSeries = new FilterSeries();
this.series.push(filterSeries);
}
updateKey(key: string, value: any) {
this[key] = value
}
updateKey(key: string, value: any) {
this[key] = value;
}
removeSeries(index: number) {
this.series.splice(index, 1)
}
removeSeries(index: number) {
this.series.splice(index, 1);
}
addSeries() {
const series = new FilterSeries()
series.name = "Series " + (this.series.length + 1)
this.series.push(series)
}
addSeries() {
const series = new FilterSeries();
series.name = 'Series ' + (this.series.length + 1);
this.series.push(series);
}
fromJson(json: any, period?: any) {
json.config = json.config || {}
runInAction(() => {
this.metricId = json.metricId
this.widgetId = json.widgetId
this.metricValue = this.metricValueFromArray(json.metricValue)
this.metricOf = json.metricOf
this.metricType = json.metricType
this.metricFormat = json.metricFormat
this.viewType = json.viewType
this.name = json.name
this.series = json.series ? json.series.map((series: any) => new FilterSeries().fromJson(series)) : []
this.dashboards = json.dashboards || []
this.owner = json.ownerEmail
this.lastModified = json.editedAt || json.createdAt ? DateTime.fromMillis(json.editedAt || json.createdAt) : null
this.config = json.config
this.position = json.config.position
this.predefinedKey = json.predefinedKey
this.category = json.category
this.thumbnail = json.thumbnail
fromJson(json: any, period?: any) {
json.config = json.config || {};
runInAction(() => {
this.metricId = json.metricId;
this.widgetId = json.widgetId;
this.metricValue = this.metricValueFromArray(json.metricValue, json.metricType);
this.metricOf = json.metricOf;
this.metricType = json.metricType;
this.metricFormat = json.metricFormat;
this.viewType = json.viewType;
this.name = json.name;
this.series =
json.series && json.series.length > 0
? json.series.map((series: any) => new FilterSeries().fromJson(series))
: [new FilterSeries()];
this.dashboards = json.dashboards || [];
this.owner = json.ownerEmail;
this.lastModified =
json.editedAt || json.createdAt
? DateTime.fromMillis(json.editedAt || json.createdAt)
: null;
this.config = json.config;
this.position = json.config.position;
this.predefinedKey = json.predefinedKey;
this.category = json.category;
this.thumbnail = json.thumbnail;
this.isPublic = json.isPublic;
if (period) {
this.period = period
if (period) {
this.period = period;
}
});
return this;
}
toWidget(): any {
return {
config: {
position: this.position,
col: this.config.col,
row: this.config.row,
},
};
}
toJson() {
return {
metricId: this.metricId,
widgetId: this.widgetId,
metricOf: this.metricOf,
metricValue: this.metricValueToArray(this.metricValue),
metricType: this.metricType,
metricFormat: this.metricFormat,
viewType: this.viewType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
thumbnail: this.thumbnail,
config: {
...this.config,
col:
this.metricType === 'funnel' ||
this.metricOf === FilterKey.ERRORS ||
this.metricOf === FilterKey.SESSIONS ||
this.metricOf === FilterKey.SLOWEST_RESOURCES ||
this.metricOf === FilterKey.MISSING_RESOURCES ||
this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION
? 4
: this.metricType === WEB_VITALS
? 1
: 2,
},
};
}
validate() {
this.isValid = this.name.length > 0;
}
update(data: any) {
runInAction(() => {
Object.assign(this, data);
});
}
exists() {
return this.metricId !== undefined;
}
setData(data: any, period: any) {
const _data: any = {};
if (this.metricOf === FilterKey.ERRORS) {
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s));
} else if (this.metricType === INSIGHTS) {
_data['issues'] = data
.filter((i: any) => i.change > 0 || i.change < 0)
.map(
(i: any) =>
new InishtIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew)
);
} else {
if (data.hasOwnProperty('chart')) {
_data['chart'] = getChartFormatter(period)(data.chart);
_data['namesMap'] = data.chart
.map((i: any) => Object.keys(i))
.flat()
.filter((i: any) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, []);
} else {
_data['chart'] = getChartFormatter(period)(Array.isArray(data) ? data : []);
_data['namesMap'] = Array.isArray(data)
? data
.map((i) => Object.keys(i))
.flat()
.filter((i) => i !== 'time' && i !== 'timestamp')
.reduce((unique: any, item: any) => {
if (!unique.includes(item)) {
unique.push(item);
}
return unique;
}, [])
: [];
}
}
this.data = _data;
}
fetchSessions(metricId: any, filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchSessions(metricId, filter).then((response: any[]) => {
resolve(
response.map((cat: { sessions: any[] }) => {
return {
...cat,
sessions: cat.sessions.map((s: any) => new Session().fromJson(s)),
};
})
);
});
});
}
fetchIssues(filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchIssues(filter).then((response: any) => {
const significantIssues = response.issues.significant
? response.issues.significant.map((issue: any) => new Funnelissue().fromJSON(issue))
: [];
const insignificantIssues = response.issues.insignificant
? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue))
: [];
resolve({
issues: significantIssues.length > 0 ? significantIssues : insignificantIssues,
});
});
});
}
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService
.fetchIssue(funnelId, issueId, params)
.then((response: any) => {
resolve({
issue: new Funnelissue().fromJSON(response.issue),
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)),
});
})
return this
}
.catch((error: any) => {
reject(error);
});
});
}
toWidget(): any {
return {
config: {
position: this.position,
col: this.config.col,
row: this.config.row,
}
}
private metricValueFromArray(metricValue: any, metricType: string) {
if (!Array.isArray(metricValue)) return metricValue;
if (metricType === TABLE) {
return issueOptions.filter((i: any) => metricValue.includes(i.value));
} else if (metricType === INSIGHTS) {
return issueCategories.filter((i: any) => metricValue.includes(i.value));
}
}
toJson() {
return {
metricId: this.metricId,
widgetId: this.widgetId,
metricOf: this.metricOf,
metricValue: this.metricValueToArray(this.metricValue),
metricType: this.metricType,
metricFormat: this.metricFormat,
viewType: this.viewType,
name: this.name,
series: this.series.map((series: any) => series.toJson()),
thumbnail: this.thumbnail,
config: {
...this.config,
col: (this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS || this.metricOf === FilterKey.SESSIONS || this.metricOf === FilterKey.SLOWEST_RESOURCES || this.metricOf === FilterKey.MISSING_RESOURCES || this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION) ? 4 : (this.metricType === WEB_VITALS ? 1 : 2)
},
}
}
validate() {
this.isValid = this.name.length > 0
}
update(data: any) {
runInAction(() => {
Object.assign(this, data)
})
}
exists() {
return this.metricId !== undefined
}
setData(data: any) {
this.data = data;
}
fetchSessions(metricId: any, filter: any): Promise<any> {
return new Promise((resolve) => {
metricService.fetchSessions(metricId, filter).then((response: any[]) => {
resolve(response.map((cat: { sessions: any[]; }) => {
return {
...cat,
sessions: cat.sessions.map((s: any) => new Session().fromJson(s))
}
}))
})
})
}
fetchIssue(funnelId: any, issueId: any, params: any): Promise<any> {
return new Promise((resolve, reject) => {
metricService.fetchIssue(funnelId, issueId, params).then((response: any) => {
resolve({
issue: new Funnelissue().fromJSON(response.issue),
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)),
})
}).catch((error: any) => {
reject(error)
})
})
}
private metricValueFromArray(metricValue: any) {
if (!Array.isArray(metricValue)) return metricValue;
return issueOptions.filter((i: any) => metricValue.includes(i.value))
}
private metricValueToArray(metricValue: any) {
if (!Array.isArray(metricValue)) return metricValue;
return metricValue.map((i: any) => i.value)
}
private metricValueToArray(metricValue: any) {
if (!Array.isArray(metricValue)) return metricValue;
return metricValue.map((i: any) => i.value);
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,14 +1,62 @@
<svg viewBox="0 0 250 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="250" height="100" rx="13.1579" fill="#3EAAAF" fill-opacity="0.08"/>
<rect opacity="0.6" x="86.8947" y="29.579" width="138.158" height="14.4737" rx="6.57895" fill="#3EAAAF" fill-opacity="0.5"/>
<rect opacity="0.3" x="86.8947" y="55.8948" width="46.0526" height="14.4737" rx="6.57895" fill="#3EAAAF" fill-opacity="0.5"/>
<g clip-path="url(#clip0_2_20)">
<path d="M54 67C58.5087 67 62.8327 65.2089 66.0208 62.0208C69.2089 58.8327 71 54.5087 71 50C71 45.4913 69.2089 41.1673 66.0208 37.9792C62.8327 34.7911 58.5087 33 54 33C49.4913 33 45.1673 34.7911 41.9792 37.9792C38.7911 41.1673 37 45.4913 37 50C37 54.5087 38.7911 58.8327 41.9792 62.0208C45.1673 65.2089 49.4913 67 54 67V67ZM51.875 46.8125C51.875 48.572 50.923 50 49.75 50C48.577 50 47.625 48.572 47.625 46.8125C47.625 45.053 48.577 43.625 49.75 43.625C50.923 43.625 51.875 45.053 51.875 46.8125ZM46.1056 59.4201C45.8616 59.2792 45.6835 59.0472 45.6106 58.775C45.5377 58.5028 45.5759 58.2128 45.7168 57.9688C46.5559 56.5145 47.7632 55.3069 49.2174 54.4676C50.6715 53.6282 52.321 53.1867 54 53.1875C55.6789 53.1872 57.3283 53.6289 58.7823 54.4682C60.2364 55.3075 61.4438 56.5148 62.2832 57.9688C62.4219 58.2127 62.4585 58.5015 62.385 58.7723C62.3115 59.0431 62.1338 59.2738 61.8909 59.414C61.6479 59.5543 61.3593 59.5928 61.088 59.5211C60.8168 59.4494 60.5849 59.2733 60.443 59.0312C59.7904 57.9 58.8513 56.9607 57.7202 56.3079C56.5891 55.655 55.306 55.3117 54 55.3125C52.694 55.3117 51.4109 55.655 50.2798 56.3079C49.1487 56.9607 48.2096 57.9 47.557 59.0312C47.4161 59.2753 47.184 59.4533 46.9118 59.5263C46.6397 59.5992 46.3497 59.561 46.1056 59.4201ZM58.25 50C57.077 50 56.125 48.572 56.125 46.8125C56.125 45.053 57.077 43.625 58.25 43.625C59.423 43.625 60.375 45.053 60.375 46.8125C60.375 48.572 59.423 50 58.25 50Z" fill="#3EAAAF" fill-opacity="0.5"/>
<svg viewBox="0 0 210 197" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_39_211)">
<rect width="40.2491" height="80.702" rx="4" transform="matrix(0.938191 0.346117 -0.00820296 0.999966 165.932 38.3288)" fill="#F4F5FF"/>
<rect x="13" y="8" width="88.2438" height="80.702" rx="4" fill="#FDF9F3"/>
<path d="M163.204 103.088C152.823 101.957 142.32 101.896 131.902 101.793C121.447 101.691 110.986 101.751 100.531 101.793C79.5514 101.884 58.5769 102.011 37.597 102.217C25.8209 102.332 14.0501 102.447 2.27933 102.767C2.21516 102.767 2.21516 102.882 2.27933 102.882C23.2539 102.997 44.2337 102.695 65.2083 102.555C86.1561 102.41 107.115 102.199 128.062 102.289C133.924 102.314 139.78 102.392 145.641 102.549C151.497 102.707 157.348 103.033 163.204 103.196C163.262 103.196 163.262 103.094 163.204 103.088Z" fill="#010101"/>
<path d="M162.351 84.5336C162.587 84.5336 162.593 84.1647 162.351 84.1647C162.115 84.1647 162.115 84.5336 162.351 84.5336Z" fill="black"/>
<path d="M150.594 12.3377C150.395 16.184 150.431 20.0484 150.425 23.9007C150.419 27.753 150.419 31.6053 150.431 35.4636C150.455 43.1985 150.516 50.9273 150.619 58.6621C150.679 63.0043 150.8 67.3464 150.963 71.6826C150.969 71.8096 151.163 71.8156 151.163 71.6826C151.235 63.9779 151.181 56.2673 151.121 48.5627C151.06 40.8278 151.024 33.099 151 25.3642C150.988 21.0281 151.048 16.6738 150.848 12.3377C150.842 12.1744 150.607 12.1744 150.594 12.3377Z" fill="black"/>
<path d="M106.429 146.316C92.4935 147.93 78.5994 150.011 64.7156 152.146C50.9408 154.262 37.1815 156.506 23.443 158.901C15.648 160.261 7.85814 161.658 0.0890588 163.188C-0.00948097 163.207 0.0320104 163.382 0.13055 163.364C14.0091 161.072 27.8773 158.653 41.761 156.397C55.6137 154.147 69.4818 151.994 83.3656 149.962C91.0672 148.838 98.7637 147.688 106.45 146.485C106.528 146.473 106.512 146.31 106.429 146.316Z" fill="black"/>
<path d="M171.351 146.635C173.673 147.839 176.084 148.874 178.464 149.96C180.858 151.054 183.254 152.145 185.647 153.236C188.03 154.322 190.416 155.408 192.799 156.491C195.184 157.575 197.57 158.658 199.956 159.736C202.344 160.817 204.74 161.878 207.13 162.953C207.431 163.089 207.732 163.225 208.036 163.361C208.084 163.384 208.13 163.31 208.079 163.286C205.691 162.185 203.31 161.066 200.919 159.97C198.539 158.878 196.158 157.787 193.775 156.699C191.382 155.605 188.989 154.514 186.593 153.42C184.2 152.329 181.804 151.238 179.408 150.147C178.217 149.604 177.022 149.064 175.831 148.521C174.655 147.985 173.481 147.435 172.289 146.94C171.986 146.815 171.682 146.692 171.376 146.571C171.34 146.553 171.315 146.615 171.351 146.635Z" fill="black"/>
<path d="M20.5054 73.1279C20.5054 73.1279 15.9516 71.3076 15.4254 75.3353L15.2803 88.6158C15.2803 88.6158 16.1451 90.2002 20.076 89.5289C20.1667 85.6404 20.5054 73.1279 20.5054 73.1279Z" fill="white"/>
<path d="M18.2922 90.0853C15.6615 90.0853 14.96 88.8637 14.9297 88.8033L14.8813 88.7126L15.0265 75.3293C15.1837 74.0956 15.6796 73.2489 16.5081 72.7651C18.1773 71.7854 20.554 72.7167 20.6568 72.759L20.9108 72.8618L20.9047 73.14C20.9047 73.14 20.5721 85.6525 20.4754 89.535L20.4693 89.8616L20.1488 89.916C19.4352 90.037 18.8244 90.0853 18.2922 90.0853ZM15.6736 88.4888C15.8913 88.7367 16.8589 89.5955 19.6831 89.1843C19.7799 85.3622 20.052 75.2507 20.1004 73.4061C19.4956 73.2126 17.9596 72.8316 16.9073 73.4485C16.3086 73.7992 15.9397 74.4524 15.8187 75.3837L15.6736 88.4888Z" fill="#020202"/>
<path d="M42.8447 36.7881C43.897 36.8364 121.137 38.56 121.137 38.56L119.649 119.059L44.3748 120.16L42.8447 36.7881Z" fill="white"/>
<path d="M43.9879 120.565L42.4397 36.3768L42.863 36.395C43.9032 36.4434 120.375 38.1488 121.149 38.1669L121.542 38.173L121.536 38.566L120.042 119.446L43.9879 120.565ZM43.2501 37.1933L44.768 119.755L119.262 118.666L120.732 38.947C114.376 38.808 49.1888 37.3505 43.2501 37.1933Z" fill="#020202"/>
<path d="M54.0088 113.907L53.3315 46.9057L53.271 45.5328L55.0732 45.1095H55.1216L113.632 45.545L111.884 111.863L54.0088 113.907ZM54.0935 46.1618L54.1237 46.8875L54.7889 113.09L111.11 111.107L112.815 46.3432L55.1578 45.9138L54.0935 46.1618Z" fill="#020202"/>
<path d="M53.9847 113.235L53.2046 103.378L102.553 102.501L111.074 111.125L53.9847 113.235ZM54.0633 104.152L54.7165 112.413L109.241 110.399L102.226 103.299L54.0633 104.152Z" fill="#020202"/>
<path d="M53.6343 103.765L102.396 102.9L102.777 45.9925L53.6827 45.7989L53.6343 103.765Z" fill="#E0E1E0"/>
<path d="M53.2349 104.17L53.2832 45.4058L103.176 45.5994L102.789 103.293L53.2349 104.17ZM54.0755 46.1981L54.0271 103.36L101.996 102.507L102.371 46.3795L54.0755 46.1981Z" fill="#020202"/>
<path d="M29.8065 41.5838L55.3635 48.2966L56.2767 107.315L30.1451 120.021L20.7472 120.069L20.0759 40.5799L29.8065 41.5838Z" fill="white"/>
<path d="M20.348 120.462L19.6707 40.1324L29.9031 41.1967L55.7505 47.9882L56.6697 107.557L30.2297 120.414L20.348 120.462ZM20.4689 41.0153L21.1342 119.67L30.0483 119.622L55.8714 107.067L54.9703 48.599L29.7338 41.9708L20.4689 41.0153Z" fill="#020202"/>
<path d="M20.348 120.462L19.6707 40.1445L28.8025 40.8278L30.5442 120.408L20.348 120.462ZM20.4689 41.0032L21.1342 119.67L29.7338 119.628L28.0224 41.5717L20.4689 41.0032Z" fill="#020202"/>
<path d="M111.999 161.616C111.926 165.734 132.27 165.196 133.462 162.807C134.653 160.425 138.415 151.462 126.453 152.654C126.453 152.66 114.345 151.97 111.999 161.616Z" fill="white"/>
<path d="M123.12 164.942C120.193 164.942 117.272 164.7 115.24 164.204C112.876 163.624 111.684 162.753 111.702 161.61L111.708 161.543C114.091 151.765 126.344 152.351 126.471 152.357C130.492 151.952 133.201 152.678 134.465 154.516C136.286 157.159 134.556 161.289 133.728 162.946C133.074 164.253 128.091 164.942 123.12 164.942ZM112.295 161.652C112.307 162.438 113.402 163.14 115.379 163.624C121.288 165.069 132.421 164.223 133.195 162.674C133.976 161.114 135.614 157.232 133.976 154.855C132.845 153.21 130.323 152.575 126.483 152.956C126.313 152.95 114.581 152.4 112.295 161.652Z" fill="#020202"/>
<path d="M157.707 163.86C157.525 167.978 140.471 166.242 139.606 163.823C138.736 161.368 136.099 152.224 146.108 154.105C146.071 154.069 156.298 154.069 157.707 163.86Z" fill="white"/>
<path d="M150.655 166.629C145.781 166.629 139.89 165.505 139.328 163.926C138.681 162.094 137.399 157.57 139.491 155.127C140.749 153.657 142.999 153.216 146.168 153.809V153.815C146.44 153.784 146.966 153.809 147.994 153.978C150.643 154.425 156.926 156.276 158.009 163.817L158.015 163.848V163.878C157.978 164.767 157.253 165.928 154.011 166.418C153.013 166.557 151.864 166.629 150.655 166.629ZM143.658 154.147C141.965 154.147 140.719 154.601 139.939 155.514C138.052 157.715 139.273 161.985 139.89 163.721C140.398 165.13 148.726 166.599 153.915 165.819C155.191 165.626 157.325 165.118 157.41 163.872C156.14 155.181 147.643 154.456 146.319 154.401L146.301 154.444L146.053 154.395C145.176 154.232 144.378 154.147 143.658 154.147Z" fill="#020202"/>
<path d="M121.935 149.225C121.681 150.198 122.334 154.069 123.199 154.97C124.07 155.871 126.162 158.078 132.162 157.026C135.808 156.161 134.581 149.327 134.581 149.327L123.846 148.711C123.852 148.717 122.189 148.287 121.935 149.225Z" fill="white"/>
<path d="M129.198 157.606C125.316 157.606 123.743 155.968 122.993 155.187C122.056 154.208 121.367 150.234 121.645 149.152C121.905 148.184 123.199 148.245 123.925 148.432L134.835 149.049L134.877 149.279C134.931 149.569 136.111 156.397 132.228 157.316C131.085 157.522 130.081 157.606 129.198 157.606ZM122.225 149.303C121.996 150.204 122.637 153.954 123.417 154.764C124.282 155.665 126.289 157.752 132.113 156.736C135.07 156.034 134.496 150.791 134.327 149.618L123.834 149.019C123.761 149.001 122.395 148.68 122.225 149.303Z" fill="#020202"/>
<path d="M138.488 148.862L138.923 155.151C138.923 155.151 143.259 158.151 151.931 155.042L150.268 147.132L138.488 148.862Z" fill="white"/>
<path d="M144.541 156.76C140.785 156.76 138.868 155.478 138.747 155.399L138.626 155.314L138.167 148.608L150.498 146.793L152.269 155.236L152.021 155.326C149.016 156.403 146.518 156.76 144.541 156.76ZM139.207 154.976C139.938 155.399 144.099 157.449 151.58 154.849L150.032 147.465L138.802 149.116L139.207 154.976Z" fill="#020202"/>
<path d="M100.938 127.653C100.938 127.653 97.9026 132.781 101.374 138.387C101.374 138.387 106.72 143.516 111.31 138.962L110.875 127.979L100.938 127.653Z" fill="white"/>
<path d="M107.064 141.139C103.865 141.139 101.204 138.641 101.168 138.605L101.12 138.545C97.5939 132.854 100.654 127.556 100.684 127.502L100.775 127.35L111.165 127.689L111.612 139.083L111.521 139.173C110.052 140.631 108.504 141.139 107.064 141.139ZM101.61 138.194C102.057 138.611 106.823 142.862 111.007 138.841L110.59 128.27L101.114 127.961C100.666 128.838 98.6341 133.362 101.61 138.194Z" fill="#020202"/>
<path d="M109.683 100.227C109.465 99.326 100.182 110.671 98.5552 128.373C98.5552 128.373 102.347 132.672 112.537 134.553C112.537 134.553 114.309 119.198 109.683 100.227Z" fill="white"/>
<path d="M112.797 134.904L112.482 134.843C102.31 132.969 98.4882 128.747 98.331 128.566L98.2463 128.469L98.2584 128.342C99.8127 111.409 108.461 99.955 109.598 99.8764C109.773 99.8643 109.936 99.9792 109.979 100.155C114.563 118.956 112.857 134.432 112.839 134.589L112.797 134.904ZM98.8632 128.27C99.474 128.887 103.363 132.491 112.271 134.202C112.482 131.916 113.571 117.68 109.465 100.608C107.868 101.957 100.357 112.467 98.8632 128.27Z" fill="#020202"/>
<path d="M108.092 132.243C106.647 118.545 106.611 94.6634 109.683 81.909C112.755 69.191 128.727 61.4561 142.527 64.6372C142.527 64.6372 168.145 67.0925 167.062 103.299C168.689 122.343 166.554 133.906 166.554 133.906C166.554 133.906 165.798 144.713 158.245 148.287C152.028 151.65 142.418 156.312 118.065 149.95C112.573 146.479 109.507 145.898 108.092 132.243Z" fill="white"/>
<path d="M139.316 153.409C133.807 153.409 126.876 152.557 117.992 150.234L117.908 150.198C117.406 149.884 116.928 149.587 116.462 149.309C111.866 146.515 109.09 144.828 107.796 132.273C106.248 117.626 106.423 94.1796 109.393 81.8365C112.404 69.3724 128.225 61.0328 142.594 64.3408C142.618 64.3408 149.125 65.0182 155.39 70.1525C161.16 74.8878 167.921 84.4792 167.359 103.299C168.967 122.131 166.869 133.833 166.851 133.948C166.844 134.027 165.998 144.937 158.372 148.547C154.695 150.549 149.397 153.409 139.316 153.409ZM118.192 149.672C143.065 156.161 152.482 151.069 158.112 148.021C165.435 144.55 166.258 133.991 166.27 133.882C166.294 133.731 168.381 122.101 166.778 103.323C167.334 84.7091 160.694 75.2688 155.028 70.6182C148.901 65.5927 142.576 64.9335 142.515 64.9274C128.406 61.6799 112.924 69.8078 109.985 81.9695C107.028 94.2521 106.859 117.608 108.401 132.201C109.665 144.465 112.193 146.001 116.789 148.789C117.23 149.073 117.702 149.364 118.192 149.672Z" fill="#020202"/>
<path d="M138.137 85.9488C121.113 85.9488 109.985 81.6006 109.822 81.5341L110.046 80.9777C110.275 81.0684 133.117 89.9886 163.518 82.0844L163.669 82.665C154.308 85.1021 145.648 85.9488 138.137 85.9488Z" fill="#020202"/>
<path d="M109.937 81.401C109.937 81.401 114.854 83.7838 128.727 85.2654L127.463 104.346C127.463 104.346 108.963 102.682 107.736 98.2012C107.808 94.9174 108.746 84.8723 109.937 81.401Z" fill="#C8E2E2"/>
<path d="M127.741 104.672L127.438 104.642C126.676 104.575 108.715 102.906 107.445 98.2798L107.433 98.2375V98.1951C107.505 94.8871 108.443 84.8179 109.652 81.3043L109.761 80.9837L110.064 81.1289C110.112 81.1531 115.113 83.5116 128.757 84.9691L129.041 84.9993L127.741 104.672ZM108.032 98.1649C109.066 101.582 121.905 103.517 127.184 104.019L128.412 85.5315C116.589 84.2494 111.388 82.3384 110.118 81.8062C108.981 85.5013 108.11 94.9295 108.032 98.1649Z" fill="#020202"/>
<path d="M137.665 105.646C118.96 105.646 107.651 100.106 107.487 100.022L107.753 99.4893C107.983 99.6042 131.309 111.022 166.542 100.808L166.711 101.382C155.789 104.551 145.992 105.646 137.665 105.646Z" fill="#020202"/>
<path d="M133.934 67.0199C134.188 63.5486 133.643 47.2201 148.569 43.6762C148.569 43.6762 159.267 41.9769 156.159 50.7217C154.641 53.0318 154.066 55.0215 146.694 57.2228C146.694 57.2228 138.778 57.7671 137.332 68.3503C135.881 71.2834 133.68 70.4912 133.934 67.0199Z" fill="white"/>
<path d="M135.367 70.4549C135.246 70.4549 135.131 70.4368 135.01 70.4065C134.03 70.1404 133.498 68.8342 133.637 67.0017C133.655 66.7356 133.673 66.391 133.685 65.9797C133.891 61.2142 134.514 46.7061 148.496 43.386C148.738 43.3436 153.951 42.5514 156.116 45.1519C157.198 46.4581 157.307 48.3631 156.436 50.8184L156.406 50.885C156.255 51.1148 156.116 51.3385 155.977 51.5623C154.713 53.5822 153.522 55.4932 146.772 57.5131L146.706 57.5252C146.391 57.5494 139.013 58.1904 137.622 68.3987L137.592 68.4895C136.975 69.7474 136.159 70.4549 135.367 70.4549ZM134.23 67.0441C134.103 68.7616 134.611 69.6808 135.167 69.832C135.754 69.9893 136.479 69.3784 137.042 68.2717C138.469 57.9848 146.035 56.9991 146.633 56.9386C153.141 54.9913 154.217 53.2556 155.469 51.2539C155.602 51.0362 155.741 50.8184 155.886 50.5947C156.672 48.3692 156.594 46.6698 155.656 45.545C153.709 43.2045 148.659 43.9726 148.611 43.9786C135.089 47.1899 134.478 61.3533 134.278 66.0099C134.266 66.4212 134.248 66.7719 134.23 67.0441Z" fill="#020202"/>
<path d="M173.968 134.263C173.968 134.263 175.994 146.298 165.87 145.79C155.753 145.282 159.514 133.827 159.514 133.827L173.968 134.263Z" fill="white"/>
<path d="M166.482 146.104C166.276 146.104 166.07 146.098 165.859 146.086C163.077 145.947 161.051 144.979 159.835 143.213C157.35 139.597 159.152 133.973 159.231 133.737L159.303 133.525L174.222 133.973L174.265 134.214C174.307 134.468 175.263 140.401 172.227 143.818C170.872 145.336 168.943 146.104 166.482 146.104ZM159.732 134.136C159.436 135.188 158.269 139.881 160.325 142.881C161.426 144.483 163.301 145.366 165.883 145.493C168.453 145.62 170.437 144.925 171.773 143.425C174.289 140.601 173.847 135.696 173.702 134.559L159.732 134.136Z" fill="#020202"/>
<path d="M163.561 102.362C162.805 102.688 157.997 103.735 157.489 125.851C157.489 125.851 157.416 134.704 158.136 136.513C158.862 138.321 175.154 139.258 175.553 133.513C175.958 127.798 174.761 97.4089 163.561 102.362Z" fill="white"/>
<path d="M165.157 138.188C161.565 138.188 158.245 137.565 157.864 136.615C157.126 134.777 157.186 126.207 157.192 125.839C157.416 116.017 158.668 103.934 163.355 102.114L163.439 102.078C165.393 101.213 167.171 101.334 168.737 102.434C175.371 107.091 176.218 128.252 175.849 133.519C175.698 135.726 173.454 137.232 169.36 137.873C168.03 138.091 166.572 138.188 165.157 138.188ZM165.913 102.108C165.211 102.108 164.468 102.283 163.681 102.628L163.573 102.67C160.156 103.995 158.099 112.225 157.791 125.845C157.791 125.929 157.725 134.656 158.42 136.386C158.728 137.154 164.171 138.085 169.275 137.281C171.434 136.942 175.087 135.992 175.262 133.477C175.613 128.481 174.736 107.369 168.398 102.924C167.618 102.386 166.796 102.108 165.913 102.108Z" fill="#020202"/>
<path d="M135.596 72.8255C131.49 72.8255 121.856 72.5474 119.486 69.9953C119.087 69.5659 118.911 69.0942 118.96 68.5862L119.552 68.6406C119.522 68.9793 119.643 69.2877 119.921 69.5901C121.778 71.5858 129.845 72.3478 137.03 72.2208C146.222 72.0515 152.778 70.9085 152.959 69.445L153.552 69.5175C153.207 72.2268 141.886 72.7227 137.036 72.8135C136.721 72.8135 136.225 72.8255 135.596 72.8255Z" fill="#020202"/>
<path d="M175.414 68.1387L191.748 57.4042L194.965 64.5948L178.202 71.3862L175.414 68.1387Z" fill="#F0F3C2"/>
<path d="M178.111 71.7491L174.966 68.0782L191.875 56.9688L195.365 64.7641L178.111 71.7491ZM175.861 68.2052L178.286 71.0355L194.566 64.4376L191.621 57.8518L175.861 68.2052Z" fill="#020202"/>
<path d="M173.787 69.1486L175.559 71.8217L171.839 73.013C171.839 73.013 170.068 71.3862 170.866 71.1685C171.652 70.9206 173.787 69.1486 173.787 69.1486Z" fill="#F0F3C2"/>
<path d="M171.761 73.3577L171.634 73.2428C171.204 72.8497 170.231 71.8761 170.37 71.2834C170.394 71.1685 170.485 70.969 170.781 70.8903C171.374 70.7029 173.013 69.4087 173.593 68.9249L173.847 68.7132L176.018 71.991L171.761 73.3577ZM170.957 71.4588C170.999 71.6705 171.422 72.2087 171.912 72.6804L175.087 71.6644L173.714 69.5962C173.109 70.086 171.646 71.2351 170.957 71.4588Z" fill="#020202"/>
<path d="M55.7377 165.051C55.6955 165.093 55.6533 165.135 55.6172 165.178C55.5991 165.202 55.575 165.22 55.5569 165.238C55.5448 165.25 55.5328 165.262 55.5267 165.268C55.5147 165.28 55.5026 165.292 55.4966 165.31C55.4785 165.334 55.5147 165.371 55.5388 165.352C55.5508 165.34 55.5629 165.334 55.581 165.322C55.593 165.31 55.6051 165.298 55.6111 165.292C55.6352 165.268 55.6533 165.25 55.6774 165.232C55.7196 165.19 55.7618 165.147 55.8101 165.111C55.8281 165.093 55.8281 165.063 55.8101 165.045C55.786 165.033 55.7558 165.027 55.7377 165.051Z" fill="#010101"/>
<path d="M85.8127 61.8673C82.4442 63.7058 79.0575 65.5261 75.689 67.3646C72.3205 69.203 68.9339 71.0113 65.6501 73.007C65.5714 73.0553 65.638 73.1702 65.7226 73.1279C69.1637 71.4285 72.5201 69.5477 75.8825 67.6972C79.245 65.8406 82.5954 63.9719 85.9578 62.1213C86.1211 62.0246 85.9759 61.7766 85.8127 61.8673Z" fill="#010101"/>
<path d="M89.8652 64.9879C82.4025 68.8705 75.0305 72.9042 67.7855 77.1859C65.7232 78.4014 63.661 79.623 61.6835 80.9777C61.6169 81.0261 61.6774 81.1228 61.75 81.0865C65.4451 79.0909 69.0131 76.8472 72.6538 74.7547C76.2944 72.6562 79.9592 70.594 83.6362 68.556C85.7347 67.4009 87.8392 66.264 89.9559 65.1452C90.0526 65.0907 89.9619 64.9395 89.8652 64.9879Z" fill="#010101"/>
<path d="M90.0403 72.5595C84.0411 75.5772 78.0963 78.7219 72.206 81.9513C70.5248 82.8706 68.8375 83.7838 67.1805 84.7635C67.1139 84.8058 67.1744 84.9026 67.2409 84.8663C70.2708 83.4209 73.2462 81.8364 76.2095 80.258C79.1789 78.6796 82.1482 77.1133 85.0934 75.5046C86.7746 74.5854 88.4437 73.6299 90.1129 72.6925C90.2036 72.6441 90.131 72.5171 90.0403 72.5595Z" fill="#010101"/>
<path d="M111.999 90.1035C112.9 89.6681 113.765 89.148 114.636 88.6521C115.513 88.1622 116.36 87.63 117.206 87.0918C117.321 87.0192 117.218 86.8559 117.103 86.9104C116.202 87.3579 115.319 87.8175 114.442 88.3194C113.572 88.8153 112.689 89.2931 111.854 89.8374C111.685 89.9462 111.824 90.1821 111.999 90.1035Z" fill="#010101"/>
<path d="M110.983 93.8046C112.422 93.0426 113.795 92.1415 115.216 91.3311C116.631 90.5207 118.077 89.7467 119.467 88.9C119.54 88.8577 119.48 88.7609 119.401 88.7911C117.913 89.4503 116.474 90.2667 115.065 91.0832C113.656 91.8996 112.204 92.7039 110.898 93.6715C110.825 93.7259 110.892 93.8529 110.983 93.8046Z" fill="#010101"/>
</g>
<path d="M28.375 28.0625C28.375 26.7198 28.9084 25.4322 29.8578 24.4828C30.8072 23.5334 32.0948 23 33.4375 23H73.9375C75.2802 23 76.5678 23.5334 77.5172 24.4828C78.4666 25.4322 79 26.7198 79 28.0625V71.9375C79 73.2802 78.4666 74.5678 77.5172 75.5172C76.5678 76.4666 75.2802 77 73.9375 77H33.4375C32.0948 77 30.8072 76.4666 29.8578 75.5172C28.9084 74.5678 28.375 73.2802 28.375 71.9375V66.875H26.6875C26.2399 66.875 25.8107 66.6972 25.4943 66.3807C25.1778 66.0643 25 65.6351 25 65.1875C25 64.7399 25.1778 64.3107 25.4943 63.9943C25.8107 63.6778 26.2399 63.5 26.6875 63.5H28.375V51.6875H26.6875C26.2399 51.6875 25.8107 51.5097 25.4943 51.1932C25.1778 50.8768 25 50.4476 25 50C25 49.5524 25.1778 49.1232 25.4943 48.8068C25.8107 48.4903 26.2399 48.3125 26.6875 48.3125H28.375V36.5H26.6875C26.2399 36.5 25.8107 36.3222 25.4943 36.0057C25.1778 35.6893 25 35.2601 25 34.8125C25 34.3649 25.1778 33.9357 25.4943 33.6193C25.8107 33.3028 26.2399 33.125 26.6875 33.125H28.375V28.0625ZM33.4375 26.375C32.9899 26.375 32.5607 26.5528 32.2443 26.8693C31.9278 27.1857 31.75 27.6149 31.75 28.0625V71.9375C31.75 72.3851 31.9278 72.8143 32.2443 73.1307C32.5607 73.4472 32.9899 73.625 33.4375 73.625H73.9375C74.3851 73.625 74.8143 73.4472 75.1307 73.1307C75.4472 72.8143 75.625 72.3851 75.625 71.9375V28.0625C75.625 27.6149 75.4472 27.1857 75.1307 26.8693C74.8143 26.5528 74.3851 26.375 73.9375 26.375H33.4375Z" fill="#3EAAAF" fill-opacity="0.5"/>
<defs>
<clipPath id="clip0_2_20">
<rect width="34" height="34" fill="white" transform="translate(37 33)"/>
<clipPath id="clip0_39_211">
<rect width="210" height="197" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

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

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-exclamation-circle" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-cloud-arrow-down" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M7.646 10.854a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 9.293V5.5a.5.5 0 0 0-1 0v3.793L6.354 8.146a.5.5 0 1 0-.708.708l2 2z"/>
<path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383zm.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 749 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-emoji-angry" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683zm6.991-8.38a.5.5 0 1 1 .448.894l-1.009.504c.176.27.285.64.285 1.049 0 .828-.448 1.5-1 1.5s-1-.672-1-1.5c0-.247.04-.48.11-.686a.502.502 0 0 1 .166-.761l2-1zm-6.552 0a.5.5 0 0 0-.448.894l1.009.504A1.94 1.94 0 0 0 5 6.5C5 7.328 5.448 8 6 8s1-.672 1-1.5c0-.247-.04-.48-.11-.686a.502.502 0 0 0-.166-.761l-2-1z"/>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-text" viewBox="0 0 16 16">
<path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
<path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View file

@ -149,6 +149,13 @@ export enum IssueType {
JS_EXCEPTION = 'js_exception',
}
export enum IssueCategory {
RESOURCES = 'resources',
NETWORK = 'network',
RAGE = 'rage',
ERRORS = 'errors'
}
export enum FilterType {
STRING = 'STRING',
ISSUE = 'ISSUE',
@ -249,8 +256,8 @@ export enum FilterKey {
ERRORS_PER_DOMAINS = 'errorsPerDomains',
ERRORS_PER_TYPE = 'errorsPerType',
CALLS_ERRORS = 'callsErrors',
DOMAINS_ERRORS_4XX = 'domainsErrors4Xx',
DOMAINS_ERRORS_5XX = 'domainsErrors5Xx',
DOMAINS_ERRORS_4XX = 'domainsErrors4xx',
DOMAINS_ERRORS_5XX = 'domainsErrors5xx',
IMPACTED_SESSIONS_BY_JS_ERRORS = 'impactedSessionsByJsErrors',
// Performance