feat(ui) - insights - wip
This commit is contained in:
parent
2b8e008a18
commit
9b558d444a
13 changed files with 244 additions and 74 deletions
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
|
||||
interface Props {
|
||||
item: any;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
function InsightItem(props: Props) {
|
||||
const { item, onClick = () => {} } = props;
|
||||
return (
|
||||
<div
|
||||
className="flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Icon name={item.icon} size={20} className="mr-2" color={item.iconColor} />
|
||||
<div className="mx-1 font-medium">{item.ratio}</div>
|
||||
<div className="mx-1">on</div>
|
||||
<div className="mx-1 bg-gray-100 px-2 rounded">Update</div>
|
||||
<div className="mx-1">increased by</div>
|
||||
<div className="font-medium text-red">{item.increase}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InsightItem;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import InsightItem from './InsightItem';
|
||||
|
||||
const data = [
|
||||
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' },
|
||||
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'yello' },
|
||||
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'green' },
|
||||
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'gray' },
|
||||
{ icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' },
|
||||
];
|
||||
interface Props {}
|
||||
function InsightsCard(props: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const metric = metricStore.instance;
|
||||
|
||||
const clickHanddler = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
console.log(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data.map((item) => (
|
||||
<InsightItem item={item} onClick={clickHanddler} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(InsightsCard);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './InsightsCard'
|
||||
|
|
@ -13,12 +13,13 @@ import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
|||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted'
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS } from 'App/constants/card';
|
||||
import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS, INSIGHTS } from 'App/constants/card';
|
||||
import FunnelWidget from 'App/components/Funnels/FunnelWidget';
|
||||
import SessionWidget from '../Sessions/SessionWidget';
|
||||
import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions';
|
||||
import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors';
|
||||
import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard'
|
||||
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -193,6 +194,10 @@ function WidgetChart(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
if (metricType === INSIGHTS) {
|
||||
return <InsightsCard />
|
||||
}
|
||||
|
||||
return <div>Unknown metric type</div>;
|
||||
}
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { metricOf, issueOptions } from 'App/constants/filterOptions';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
|
@ -18,9 +18,11 @@ import {
|
|||
RESOURCE_MONITORING,
|
||||
PERFORMANCE,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
} from 'App/constants/card';
|
||||
import { clickmapFilter, eventKeys } from 'App/types/filter/newFilter';
|
||||
import { eventKeys } from 'App/types/filter/newFilter';
|
||||
import { renderClickmapThumbnail } from './renderMap';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
|
|
@ -37,68 +39,45 @@ function WidgetForm(props: Props) {
|
|||
const { metricStore, dashboardStore } = useStore();
|
||||
const isSaving = metricStore.isSaving;
|
||||
const metric: any = metricStore.instance;
|
||||
const [initialInstance, setInitialInstance] = useState();
|
||||
|
||||
const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
|
||||
const tableOptions = metricOf.filter((i) => i.type === 'table');
|
||||
const isTable = metric.metricType === 'table';
|
||||
const isTable = metric.metricType === TABLE;
|
||||
const isClickmap = metric.metricType === CLICKMAP;
|
||||
const isFunnel = metric.metricType === 'funnel';
|
||||
const isFunnel = metric.metricType === FUNNEL;
|
||||
const isInsights = metric.metricType === INSIGHTS;
|
||||
const canAddSeries = metric.series.length < 3;
|
||||
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length;
|
||||
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
|
||||
|
||||
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(
|
||||
metric.metricType
|
||||
);
|
||||
|
||||
const excludeFilterKeys = isClickmap ? eventKeys : []
|
||||
const excludeFilterKeys = isClickmap ? eventKeys : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!!metric && !initialInstance) {
|
||||
setInitialInstance(metric.toJson());
|
||||
}
|
||||
}, [metric]);
|
||||
|
||||
const writeOption = ({ value, name }: { value: any; name: any }) => {
|
||||
value = Array.isArray(value) ? value : value.value;
|
||||
const obj: any = { [name]: value };
|
||||
|
||||
if (name === 'metricValue') {
|
||||
obj.metricValue = value;
|
||||
|
||||
if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) {
|
||||
obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all');
|
||||
}
|
||||
}
|
||||
|
||||
if (name === 'metricType') {
|
||||
switch (value) {
|
||||
case TIMESERIES:
|
||||
obj.metricOf = timeseriesOptions[0].value;
|
||||
obj.viewType = 'lineChart';
|
||||
break;
|
||||
case TABLE:
|
||||
obj.metricOf = tableOptions[0].value;
|
||||
obj.viewType = 'table';
|
||||
break;
|
||||
case FUNNEL:
|
||||
obj.metricOf = 'sessionCount';
|
||||
break;
|
||||
case ERRORS:
|
||||
case RESOURCE_MONITORING:
|
||||
case PERFORMANCE:
|
||||
case WEB_VITALS:
|
||||
obj.viewType = 'chart';
|
||||
break;
|
||||
case CLICKMAP:
|
||||
obj.viewType = 'chart';
|
||||
|
||||
if (value !== CLICKMAP) {
|
||||
metric.series[0].filter.removeFilter(0);
|
||||
}
|
||||
|
||||
if (metric.series[0].filter.filters.length < 1) {
|
||||
metric.series[0].filter.addFilter({
|
||||
...clickmapFilter,
|
||||
value: [''],
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
metricStore.merge(obj);
|
||||
};
|
||||
|
||||
|
|
@ -112,10 +91,16 @@ function WidgetForm(props: Props) {
|
|||
}
|
||||
}
|
||||
const savedMetric = await metricStore.save(metric);
|
||||
setInitialInstance(metric.toJson())
|
||||
if (wasCreating) {
|
||||
if (parseInt(dashboardId, 10) > 0) {
|
||||
history.replace(withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId));
|
||||
dashboardStore.addWidgetToDashboard(dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [savedMetric.metricId]);
|
||||
history.replace(
|
||||
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
|
||||
);
|
||||
dashboardStore.addWidgetToDashboard(
|
||||
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
|
||||
[savedMetric.metricId]
|
||||
);
|
||||
} else {
|
||||
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
|
||||
}
|
||||
|
|
@ -134,6 +119,11 @@ function WidgetForm(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
const undoChnages = () => {
|
||||
const w = new Widget();
|
||||
metricStore.merge(w.fromJson(initialInstance), false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="form-group">
|
||||
|
|
@ -142,7 +132,7 @@ function WidgetForm(props: Props) {
|
|||
<MetricTypeDropdown onSelect={writeOption} />
|
||||
<MetricSubtypeDropdown onSelect={writeOption} />
|
||||
|
||||
{metric.metricOf === FilterKey.ISSUE && (
|
||||
{metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && (
|
||||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<Select
|
||||
|
|
@ -156,6 +146,20 @@ function WidgetForm(props: Props) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === INSIGHTS && (
|
||||
<>
|
||||
<span className="mx-3">issue category</span>
|
||||
<Select
|
||||
name="metricValue"
|
||||
options={issueCategories}
|
||||
value={metric.metricValue}
|
||||
onChange={writeOption}
|
||||
isMulti={true}
|
||||
placeholder="All Categories"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === 'table' &&
|
||||
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
|
||||
<>
|
||||
|
|
@ -183,8 +187,8 @@ function WidgetForm(props: Props) {
|
|||
{!isPredefined && (
|
||||
<div className="form-group">
|
||||
<div className="flex items-center font-medium py-2">
|
||||
{`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`}
|
||||
{!isTable && !isFunnel && !isClickmap && (
|
||||
{`${isTable || isFunnel || isClickmap || isInsights ? 'Filter by' : 'Chart Series'}`}
|
||||
{!isTable && !isFunnel && !isClickmap && !isInsights && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="text-primary"
|
||||
|
|
@ -198,14 +202,14 @@ function WidgetForm(props: Props) {
|
|||
|
||||
{metric.series.length > 0 &&
|
||||
metric.series
|
||||
.slice(0, isTable || isFunnel || isClickmap ? 1 : metric.series.length)
|
||||
.slice(0, isTable || isFunnel || isClickmap || isInsights ? 1 : metric.series.length)
|
||||
.map((series: any, index: number) => (
|
||||
<div className="mb-2" key={series.name}>
|
||||
<FilterSeries
|
||||
supportsEmpty={!isClickmap}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
observeChanges={() => metric.updateKey('hasChanged', true)}
|
||||
hideHeader={isTable || isClickmap}
|
||||
hideHeader={isTable || isClickmap || isInsights}
|
||||
seriesIndex={index}
|
||||
series={series}
|
||||
onRemoveSeries={() => metric.removeSeries(index)}
|
||||
|
|
@ -226,13 +230,20 @@ function WidgetForm(props: Props) {
|
|||
title="Cannot save funnel metric without at least 2 events"
|
||||
disabled={!cannotSaveFunnel}
|
||||
>
|
||||
<Button variant="primary" onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
|
||||
{metric.exists()
|
||||
? 'Update'
|
||||
: parseInt(dashboardId) > 0
|
||||
? 'Create & Add to Dashboard'
|
||||
: 'Create'}
|
||||
</Button>
|
||||
<div className="flex items-center">
|
||||
<Button variant="primary" onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
|
||||
{metric.exists()
|
||||
? 'Update'
|
||||
: parseInt(dashboardId) > 0
|
||||
? 'Create & Add to Dashboard'
|
||||
: 'Create'}
|
||||
</Button>
|
||||
{metric.exists() && metric.hasChanged && (
|
||||
<Button onClick={undoChnages} variant="text" icon="arrow-counterclockwise" className="ml-2">
|
||||
Undo
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="flex items-center">
|
||||
{metric.exists() && (
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ function MetricTypeDropdown(props: Props) {
|
|||
const onChange = (type: string) => {
|
||||
metricStore.changeType(type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
name="metricType"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,17 @@ import Breadcrumb from 'Shared/Breadcrumb';
|
|||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { Prompt } from 'react-router';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
CLICKMAP,
|
||||
FUNNEL,
|
||||
ERRORS,
|
||||
RESOURCE_MONITORING,
|
||||
PERFORMANCE,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
} from 'App/constants/card';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
|
|
@ -110,10 +121,11 @@ function WidgetView(props: Props) {
|
|||
</div>
|
||||
|
||||
<WidgetPreview className="mt-8" name={widget.name} />
|
||||
|
||||
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
|
||||
<>
|
||||
{(widget.metricType === 'table' || widget.metricType === 'timeseries' || widget.metricType === 'clickMap') && <WidgetSessions className="mt-8" />}
|
||||
{widget.metricType === 'funnel' && <FunnelIssues />}
|
||||
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) && <WidgetSessions className="mt-8" />}
|
||||
{widget.metricType === FUNNEL && <FunnelIssues />}
|
||||
</>
|
||||
)}
|
||||
</NoContent>
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -21,6 +21,7 @@ export const WEB_VITALS = 'webVitals';
|
|||
export const USER_PATH = 'userPath';
|
||||
export const RETENTION = 'retention';
|
||||
export const FEATURE_ADOPTION = 'featureAdoption';
|
||||
export const INSIGHTS = 'insights';
|
||||
|
||||
export const TYPES: CardType[] = [
|
||||
{
|
||||
|
|
@ -222,4 +223,10 @@ export const TYPES: CardType[] = [
|
|||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: FEATURE_ADOPTION,
|
||||
},
|
||||
{
|
||||
title: 'Insights',
|
||||
icon: 'lightbulb',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: INSIGHTS,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { FilterKey, IssueType } from 'Types/filter/filterType';
|
||||
import { FilterKey, IssueType, IssueCategory } from 'Types/filter/filterType';
|
||||
// TODO remove text property from options
|
||||
export const options = [
|
||||
{ key: 'on', label: 'on', value: 'on' },
|
||||
|
|
@ -118,6 +118,13 @@ export const issueOptions = [
|
|||
{ label: 'Error', value: IssueType.JS_EXCEPTION },
|
||||
]
|
||||
|
||||
export const issueCategories = [
|
||||
{ label: 'Resources', value: IssueCategory.RESOURCES },
|
||||
{ label: 'Network', value: IssueCategory.NETWORK },
|
||||
{ label: 'Click Rage', value: IssueCategory.CLICK_RAGE },
|
||||
{ label: 'Errors', value: IssueCategory.ERRORS },
|
||||
]
|
||||
|
||||
export default {
|
||||
options,
|
||||
baseOperators,
|
||||
|
|
@ -130,6 +137,7 @@ export default {
|
|||
metricTypes,
|
||||
metricOf,
|
||||
issueOptions,
|
||||
issueCategories,
|
||||
methodOptions,
|
||||
pageUrlOperators,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,18 @@ import Widget from './types/widget';
|
|||
import { metricService, errorService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import Error from './types/error';
|
||||
import { TIMESERIES, TABLE, FUNNEL, ERRORS, RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS } from 'App/constants/card';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
|
||||
FUNNEL,
|
||||
ERRORS,
|
||||
RESOURCE_MONITORING,
|
||||
PERFORMANCE,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
} from 'App/constants/card';
|
||||
import { clickmapFilter } from 'App/types/filter/newFilter';
|
||||
|
||||
export default class MetricStore {
|
||||
isLoading: boolean = false;
|
||||
|
|
@ -19,18 +30,20 @@ export default class MetricStore {
|
|||
|
||||
sessionsPage: number = 1;
|
||||
sessionsPageSize: number = 10;
|
||||
listView?: boolean = true
|
||||
clickMapFilter: boolean = false
|
||||
listView?: boolean = true;
|
||||
clickMapFilter: boolean = false;
|
||||
|
||||
clickMapSearch = ''
|
||||
clickMapLabel = ''
|
||||
clickMapSearch = '';
|
||||
clickMapLabel = '';
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
get sortedWidgets() {
|
||||
return [...this.metrics].sort((a, b) => this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified)
|
||||
return [...this.metrics].sort((a, b) =>
|
||||
this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified
|
||||
);
|
||||
}
|
||||
|
||||
// State Actions
|
||||
|
|
@ -44,35 +57,79 @@ export default class MetricStore {
|
|||
}
|
||||
|
||||
setClickMaps(val: boolean) {
|
||||
this.clickMapFilter = val
|
||||
this.clickMapFilter = val;
|
||||
}
|
||||
|
||||
changeClickMapSearch(val: string, label: string) {
|
||||
this.clickMapSearch = val
|
||||
this.clickMapLabel = label
|
||||
this.clickMapSearch = val;
|
||||
this.clickMapLabel = label;
|
||||
}
|
||||
|
||||
merge(object: any) {
|
||||
Object.assign(this.instance, object);
|
||||
this.instance.updateKey('hasChanged', true);
|
||||
merge(obj: any, updateChangeFlag: boolean = true) {
|
||||
const type = obj.metricType;
|
||||
|
||||
// handle metricType change
|
||||
if (obj.hasOwnProperty('metricType') && type !== this.instance.metricType) {
|
||||
this.changeType(type);
|
||||
}
|
||||
|
||||
// handle metricValue change
|
||||
if (obj.hasOwnProperty('metricValue') && obj.metricValue !== this.instance.metricValue) {
|
||||
if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) {
|
||||
obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Object.assign(this.instance, obj);
|
||||
this.instance.updateKey('hasChanged', updateChangeFlag);
|
||||
}
|
||||
|
||||
changeType(value: string) {
|
||||
const obj: any = { metricType: value};
|
||||
const obj: any = { metricType: value };
|
||||
obj.series = this.instance.series
|
||||
|
||||
obj['metricValue'] = [];
|
||||
|
||||
if (value === TABLE || value === TIMESERIES) {
|
||||
obj['viewType'] = 'table';
|
||||
}
|
||||
if (value === TIMESERIES) {
|
||||
obj['viewType'] = 'lineChart';
|
||||
}
|
||||
if (value === ERRORS || value === RESOURCE_MONITORING || value === PERFORMANCE || value === WEB_VITALS) {
|
||||
if (
|
||||
value === ERRORS ||
|
||||
value === RESOURCE_MONITORING ||
|
||||
value === PERFORMANCE ||
|
||||
value === WEB_VITALS ||
|
||||
value === CLICKMAP
|
||||
) {
|
||||
obj['viewType'] = 'chart';
|
||||
}
|
||||
}
|
||||
|
||||
if (value === FUNNEL) {
|
||||
obj['metricOf'] = 'sessionCount';
|
||||
}
|
||||
this.instance.update(obj)
|
||||
|
||||
if (value === INSIGHTS) {
|
||||
obj['metricOf'] = 'issueCategories';
|
||||
obj['viewType'] = 'list';
|
||||
}
|
||||
|
||||
if (value === CLICKMAP) {
|
||||
obj.series = obj.series.slice(0, 1)
|
||||
if (this.instance.metricType !== CLICKMAP) {
|
||||
obj.series[0].filter.removeFilter(0);
|
||||
}
|
||||
|
||||
if (obj.series[0] && obj.series[0].filter.filters.length < 1) {
|
||||
obj.series[0].filter.addFilter({
|
||||
...clickmapFilter,
|
||||
value: [''],
|
||||
});
|
||||
}
|
||||
}
|
||||
this.instance.update(obj);
|
||||
}
|
||||
|
||||
reset(id: string) {
|
||||
|
|
|
|||
4
frontend/app/svg/icons/arrow-counterclockwise.svg
Normal file
4
frontend/app/svg/icons/arrow-counterclockwise.svg
Normal 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 |
|
|
@ -149,6 +149,13 @@ export enum IssueType {
|
|||
JS_EXCEPTION = 'js_exception',
|
||||
}
|
||||
|
||||
export enum IssueCategory {
|
||||
RESOURCES = 'resources',
|
||||
NETWORK = 'network',
|
||||
CLICK_RAGE = 'click_rage',
|
||||
ERRORS = 'errors'
|
||||
}
|
||||
|
||||
export enum FilterType {
|
||||
STRING = 'STRING',
|
||||
ISSUE = 'ISSUE',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue