ui fix outdated libraries, remove storybook (unused), fix player re-renders on mount
This commit is contained in:
parent
59d6ef6bd0
commit
8024083c64
69 changed files with 7573 additions and 14579 deletions
File diff suppressed because one or more lines are too long
786
frontend/.yarn/releases/yarn-3.2.1.cjs
vendored
786
frontend/.yarn/releases/yarn-3.2.1.cjs
vendored
File diff suppressed because one or more lines are too long
925
frontend/.yarn/releases/yarn-4.5.0.cjs
vendored
Executable file
925
frontend/.yarn/releases/yarn-4.5.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,7 @@
|
|||
nodeLinker: node-modules
|
||||
compressionLevel: 0
|
||||
compressionLevel: 1
|
||||
|
||||
enableGlobalCache: true
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||
spec: "@yarnpkg/plugin-typescript"
|
||||
nodeLinker: pnpm
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.2.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import ChatWindow from './ChatWindow';
|
||||
|
||||
storiesOf('Assist', module)
|
||||
.add('ChatWindow', () => (
|
||||
<ChatWindow userId="test@test.com" />
|
||||
))
|
||||
|
||||
|
|
@ -1,91 +1,96 @@
|
|||
import React, {useEffect, useMemo} from 'react';
|
||||
import {useStore} from "App/mstore";
|
||||
import WidgetWrapper from "Components/Dashboard/components/WidgetWrapper/WidgetWrapper";
|
||||
import {observer} from "mobx-react-lite";
|
||||
import {Loader} from "UI";
|
||||
import WidgetChart from "Components/Dashboard/components/WidgetChart/WidgetChart";
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import {Card} from "antd";
|
||||
import {CARD_CATEGORIES} from "Components/Dashboard/components/DashboardList/NewDashModal/ExampleCards";
|
||||
import React, { useEffect, useMemo, lazy } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Loader } from 'UI';
|
||||
import { Card } from 'antd';
|
||||
import { CARD_CATEGORIES } from 'Components/Dashboard/components/DashboardList/NewDashModal/ExampleCards';
|
||||
|
||||
const CARD_TYPES_MAP = CARD_CATEGORIES.reduce((acc: any, category: any) => {
|
||||
acc[category.key] = category.types;
|
||||
return acc;
|
||||
acc[category.key] = category.types;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart/WidgetChart'));
|
||||
|
||||
interface Props {
|
||||
category?: string;
|
||||
selectedList: any;
|
||||
onCard: (metricId: number) => void;
|
||||
query?: string;
|
||||
category?: string;
|
||||
selectedList: any;
|
||||
onCard: (metricId: number) => void;
|
||||
query?: string;
|
||||
}
|
||||
|
||||
function CardsLibrary(props: Props) {
|
||||
const {selectedList, query = ''} = props;
|
||||
const {metricStore, dashboardStore} = useStore();
|
||||
const { selectedList, query = '' } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
|
||||
// const cards = useMemo(() => {
|
||||
// return metricStore.filteredCards.filter((card: any) => {
|
||||
// return CARD_TYPES_MAP[props.category || 'default'].includes(card.metricType);
|
||||
// });
|
||||
// }, [metricStore.filteredCards, props.category]);
|
||||
// const cards = useMemo(() => {
|
||||
// return metricStore.filteredCards.filter((card: any) => {
|
||||
// return CARD_TYPES_MAP[props.category || 'default'].includes(card.metricType);
|
||||
// });
|
||||
// }, [metricStore.filteredCards, props.category]);
|
||||
|
||||
const cards = useMemo(() => {
|
||||
return metricStore.filteredCards.filter((card: any) => {
|
||||
return card.name.toLowerCase().includes(query.toLowerCase());
|
||||
});
|
||||
}, [query, metricStore.filteredCards]);
|
||||
const cards = useMemo(() => {
|
||||
return metricStore.filteredCards.filter((card: any) => {
|
||||
return card.name.toLowerCase().includes(query.toLowerCase());
|
||||
});
|
||||
}, [query, metricStore.filteredCards]);
|
||||
|
||||
useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
|
||||
const onItemClick = (e: any, metricId: number) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
props.onCard(metricId);
|
||||
}
|
||||
const onItemClick = (e: any, metricId: number) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
props.onCard(metricId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Loader loading={metricStore.isLoading}>
|
||||
<div className="grid grid-cols-4 gap-4 items-start">
|
||||
{cards.map((metric: any) => (
|
||||
<React.Fragment key={metric.metricId}>
|
||||
<div className={'relative col-span-' + metric.config.col}>
|
||||
<div className="absolute inset-0 z-10 cursor-pointer" onClick={(e) => onItemClick(e, metric.metricId)} />
|
||||
<LazyLoad>
|
||||
<Card className='border border-transparent hover:border-indigo-50 hover:shadow-sm rounded-lg'
|
||||
style={{
|
||||
border: selectedList.includes(metric.metricId) ? '1px solid #1890ff' : '1px solid #f0f0f0',
|
||||
}}
|
||||
styles={{
|
||||
header: {
|
||||
padding: '4px 14px',
|
||||
minHeight: '36px',
|
||||
fontSize: '14px',
|
||||
borderBottom: 'none'
|
||||
},
|
||||
body: {padding: '14px'},
|
||||
cover: {
|
||||
border: '2px solid #1890ff',
|
||||
// border: selectedList.includes(metric.metricId) ? '2px solid #1890ff' : 'none',
|
||||
}
|
||||
}} title={metric.name}>
|
||||
<WidgetChart
|
||||
// isPreview={true}
|
||||
metric={metric}
|
||||
isTemplate={true}
|
||||
isWidget={true}
|
||||
isSaved={true}
|
||||
/>
|
||||
</Card>
|
||||
</LazyLoad>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
return (
|
||||
<Loader loading={metricStore.isLoading}>
|
||||
<div className="grid grid-cols-4 gap-4 items-start">
|
||||
{cards.map((metric: any) => (
|
||||
<React.Fragment key={metric.metricId}>
|
||||
<div className={'relative col-span-' + metric.config.col}>
|
||||
<div
|
||||
className="absolute inset-0 z-10 cursor-pointer"
|
||||
onClick={(e) => onItemClick(e, metric.metricId)}
|
||||
/>
|
||||
<Card
|
||||
className="border border-transparent hover:border-indigo-50 hover:shadow-sm rounded-lg"
|
||||
style={{
|
||||
border: selectedList.includes(metric.metricId)
|
||||
? '1px solid #1890ff'
|
||||
: '1px solid #f0f0f0',
|
||||
}}
|
||||
styles={{
|
||||
header: {
|
||||
padding: '4px 14px',
|
||||
minHeight: '36px',
|
||||
fontSize: '14px',
|
||||
borderBottom: 'none',
|
||||
},
|
||||
body: { padding: '14px' },
|
||||
cover: {
|
||||
border: '2px solid #1890ff',
|
||||
// border: selectedList.includes(metric.metricId) ? '2px solid #1890ff' : 'none',
|
||||
},
|
||||
}}
|
||||
title={metric.name}
|
||||
>
|
||||
<WidgetChart
|
||||
// isPreview={true}
|
||||
metric={metric}
|
||||
isTemplate={true}
|
||||
isWidget={true}
|
||||
isSaved={true}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(CardsLibrary);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/
|
|||
import SankeyChart from 'Shared/Insights/SankeyChart';
|
||||
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
|
||||
import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
|
||||
import { Empty } from 'antd';
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -43,6 +43,10 @@ interface Props {
|
|||
}
|
||||
|
||||
function WidgetChart(props: Props) {
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: "200px 0px",
|
||||
});
|
||||
const {isSaved = false, metric, isTemplate} = props;
|
||||
const {dashboardStore, metricStore, sessionStore} = useStore();
|
||||
const _metric: any = metricStore.instance;
|
||||
|
|
@ -105,6 +109,7 @@ function WidgetChart(props: Props) {
|
|||
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
|
||||
const loadPage = () => {
|
||||
if (!inView) return;
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return;
|
||||
|
|
@ -117,7 +122,18 @@ function WidgetChart(props: Props) {
|
|||
useEffect(() => {
|
||||
_metric.updateKey('page', 1);
|
||||
loadPage();
|
||||
}, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue, metric.startType, metric.metricFormat]);
|
||||
}, [
|
||||
drillDownPeriod,
|
||||
period,
|
||||
depsString,
|
||||
metric.metricType,
|
||||
metric.metricOf,
|
||||
metric.viewType,
|
||||
metric.metricValue,
|
||||
metric.startType,
|
||||
metric.metricFormat,
|
||||
inView,
|
||||
]);
|
||||
useEffect(loadPage, [_metric.page]);
|
||||
|
||||
|
||||
|
|
@ -254,9 +270,11 @@ function WidgetChart(props: Props) {
|
|||
return <div>Unknown metric type</div>;
|
||||
};
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Loader loading={loading} style={{height: `240px`}}>
|
||||
<div style={{minHeight: 240}}>{renderChart()}</div>
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
import MetricTypeDropdown from './';
|
||||
|
||||
export default {
|
||||
title: 'Dashboad/Cards/Form/MetricTypeDropdown',
|
||||
component: MetricTypeDropdown,
|
||||
};
|
||||
|
||||
const Template = (args: any) => <MetricTypeDropdown {...args} />;
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useRef } from 'react';
|
||||
import React, { useRef, lazy } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { ItemMenu, TextEllipsis } from 'UI';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import WidgetChart from '../WidgetChart';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
|
@ -11,9 +10,10 @@ import TemplateOverlay from './TemplateOverlay';
|
|||
import AlertButton from './AlertButton';
|
||||
import stl from './widgetWrapper.module.css';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import { TIMESERIES } from "App/constants/card";
|
||||
|
||||
const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart'));
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
widget?: any;
|
||||
|
|
@ -165,7 +165,6 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<LazyLoad offset={!isTemplate ? 100 : 600}>
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart
|
||||
isPreview={isPreview}
|
||||
|
|
@ -174,7 +173,6 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
isSaved={isSaved}
|
||||
/>
|
||||
</div>
|
||||
</LazyLoad>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import React, { useRef } from 'react';
|
||||
import React, { useRef, lazy } from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Card, Tooltip, Button } from 'antd';
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import WidgetChart from '../WidgetChart';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
|
@ -10,11 +9,12 @@ import { withSiteId, dashboardMetricDetails } from 'App/routes';
|
|||
import TemplateOverlay from './TemplateOverlay';
|
||||
import stl from './widgetWrapper.module.css';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import LazyLoad from 'react-lazyload';
|
||||
import { TIMESERIES } from 'App/constants/card';
|
||||
import CardMenu from 'Components/Dashboard/components/WidgetWrapper/CardMenu';
|
||||
import AlertButton from 'Components/Dashboard/components/WidgetWrapper/AlertButton';
|
||||
|
||||
const WidgetChart = lazy(() => import('Components/Dashboard/components/WidgetChart'));
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
widget?: any;
|
||||
|
|
@ -146,7 +146,6 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
|
|||
|
||||
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate} />}
|
||||
|
||||
<LazyLoad offset={!isTemplate ? 100 : 600}>
|
||||
<div className="px-4" onClick={onChartClick}>
|
||||
<WidgetChart
|
||||
isPreview={isPreview}
|
||||
|
|
@ -156,7 +155,6 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
|
|||
isSaved={isSaved}
|
||||
/>
|
||||
</div>
|
||||
</LazyLoad>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Funnel from 'Types/funnel'
|
||||
import FunnelIssue from 'Types/funnelIssue'
|
||||
import FunnelList from './FunnelList';
|
||||
import FunnelItem from './FunnelItem';
|
||||
import IssueItem from './IssueItem';
|
||||
import FunnelGraph from './FunnelGraph';
|
||||
import FunnelHeader from './FunnelHeader';
|
||||
import FunnelOverview from './FunnelOverview';
|
||||
import IssueFilter from './IssueFilter';
|
||||
|
||||
const funnel = Funnel({
|
||||
title: 'Sessions from france',
|
||||
users: 308,
|
||||
steps: 10,
|
||||
sessionsCount: 200,
|
||||
criticalIssues: 5,
|
||||
missedConversions: 30
|
||||
})
|
||||
|
||||
const list = [funnel, funnel, funnel];
|
||||
|
||||
const funnelIssue = FunnelIssue({
|
||||
type: 'Error',
|
||||
error: 'Unchecked runtime lasterror the message port closed before a response was received.',
|
||||
affectedUsers: 132,
|
||||
conversionImpact: 30,
|
||||
lostConversions: 200,
|
||||
})
|
||||
|
||||
storiesOf('Funnels', module)
|
||||
.add('Funnel List', () => (
|
||||
<FunnelList list={list} />
|
||||
))
|
||||
.add('Funnel Item', () => (
|
||||
<FunnelItem funnel={funnel} />
|
||||
))
|
||||
.add('Funnel Header', () => (
|
||||
<FunnelHeader />
|
||||
))
|
||||
.add('Funnel Header', () => (
|
||||
<FunnelHeader funnel={funnel} />
|
||||
))
|
||||
.add('Issue Item', () => (
|
||||
<IssueItem issue={funnelIssue} />
|
||||
))
|
||||
.add('Funnel graph', () => (
|
||||
<FunnelGraph />
|
||||
))
|
||||
.add('Funnel Overview', () => (
|
||||
<FunnelOverview funnel={funnel} />
|
||||
))
|
||||
.add('Funnel IssueFilter', () => (
|
||||
<IssueFilter />
|
||||
))
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Onboarding from '.';
|
||||
|
||||
storiesOf('Onboarding', module)
|
||||
.add('Pure', () => (
|
||||
<Onboarding />
|
||||
))
|
||||
|
||||
|
|
@ -68,14 +68,16 @@ function Player(props: IProps) {
|
|||
const isReady = playerContext.store.get().ready;
|
||||
const screenWrapper = React.useRef<HTMLDivElement>(null);
|
||||
const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE;
|
||||
const [isAttached, setAttached] = React.useState(false);
|
||||
const isAttached = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
updateLastPlayedSession(sessionId);
|
||||
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
|
||||
if (parentElement && !isAttached) {
|
||||
playerContext.player.attach(parentElement);
|
||||
setAttached(true);
|
||||
if (isReady && !isAttached.current) {
|
||||
const parentElement = findDOMNode(screenWrapper.current) as HTMLDivElement | null; //TODO: good architecture
|
||||
if (parentElement) {
|
||||
playerContext.player.attach(parentElement);
|
||||
isAttached.current = true;
|
||||
}
|
||||
}
|
||||
}, [isReady]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { List } from 'immutable';
|
||||
import PageInsightsPanel from './';
|
||||
|
||||
const list = [
|
||||
{
|
||||
"alertId": 2,
|
||||
"projectId": 1,
|
||||
"name": "new alert",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 240,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1591893324078,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1592929583000,
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 14,
|
||||
"projectId": 1,
|
||||
"name": "alert 19.06",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 3000.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592579750935,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"renotifyInterval": 120
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 15,
|
||||
"projectId": 1,
|
||||
"name": "notify every 60min",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592848779604,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
},
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"alertId": 21,
|
||||
"projectId": 1,
|
||||
"name": "always notify",
|
||||
"description": null,
|
||||
"active": true,
|
||||
"threshold": 30,
|
||||
"detectionMethod": "threshold",
|
||||
"query": {
|
||||
"left": "avgPageLoad",
|
||||
"right": 1.0,
|
||||
"operator": ">="
|
||||
},
|
||||
"createdAt": 1592849011350,
|
||||
"options": {
|
||||
"message": [
|
||||
{
|
||||
"type": "slack",
|
||||
"value": "51"
|
||||
}
|
||||
],
|
||||
"LastNotification": 1599135058000,
|
||||
"renotifyInterval": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const notifications = List([
|
||||
{ title: 'test', type: 'change', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
{ title: 'test', type: 'threshold', createdAt: 1591893324078, description: 'Lorem ipusm'},
|
||||
])
|
||||
storiesOf('PageInsights', module)
|
||||
.add('Panel', () => (
|
||||
<PageInsightsPanel />
|
||||
))
|
||||
|
|
@ -4,7 +4,6 @@ import { useStore } from 'App/mstore';
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import { fileNameFormat } from 'App/utils';
|
||||
import { toast } from 'react-toastify';
|
||||
import { forceVisible } from 'react-lazyload';
|
||||
|
||||
const TEXT_GENERATING = 'Generating report...';
|
||||
const TEXT_SUCCESS = 'Report successfully generated';
|
||||
|
|
@ -38,7 +37,6 @@ export default function withReport<P extends Props>(WrappedComponent: React.Comp
|
|||
};
|
||||
|
||||
const renderPromise = async (): Promise<any> => {
|
||||
forceVisible();
|
||||
setRendering(true);
|
||||
toast.info(TEXT_GENERATING, {
|
||||
autoClose: false,
|
||||
|
|
|
|||
183
frontend/app/components/shared/DatePicker/config.ts
Normal file
183
frontend/app/components/shared/DatePicker/config.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/**
|
||||
* https://github.com/react-component/picker/blob/master/src/generate/luxon.ts
|
||||
* we don't need entire lib so I'm only using this part for antd datepicker
|
||||
* */
|
||||
|
||||
import { DateTime, Info } from 'luxon';
|
||||
|
||||
const weekDayFormatMap = {
|
||||
zh_CN: 'narrow',
|
||||
zh_TW: 'narrow',
|
||||
};
|
||||
|
||||
const weekDayLengthMap = {
|
||||
en_US: 2,
|
||||
en_GB: 2,
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes part of a moment format string that should
|
||||
* not be escaped to a luxon compatible format string.
|
||||
*
|
||||
* @param part string
|
||||
* @returns string
|
||||
*/
|
||||
const normalizeFormatPart = (part: string): string =>
|
||||
part
|
||||
.replace(/Y/g, 'y')
|
||||
.replace(/D/g, 'd')
|
||||
.replace(/gg/g, 'kk')
|
||||
.replace(/Q/g, 'q')
|
||||
.replace(/([Ww])o/g, 'WW')
|
||||
.replace(/A/g, 'a');
|
||||
|
||||
/**
|
||||
* Normalizes a moment compatible format string to a luxon compatible format string
|
||||
*
|
||||
* @param format string
|
||||
* @returns string
|
||||
*/
|
||||
const normalizeFormat = (format: string): string =>
|
||||
format
|
||||
// moment escapes strings contained in brackets
|
||||
.split(/[[\]]/)
|
||||
.map((part, index) => {
|
||||
const shouldEscape = index % 2 > 0;
|
||||
|
||||
return shouldEscape ? part : normalizeFormatPart(part);
|
||||
})
|
||||
// luxon escapes strings contained in single quotes
|
||||
.join("'");
|
||||
|
||||
/**
|
||||
* Normalizes language tags used to luxon compatible
|
||||
* language tags by replacing underscores with hyphen-minus.
|
||||
*
|
||||
* @param locale string
|
||||
* @returns string
|
||||
*/
|
||||
const normalizeLocale = (locale: string): string => locale.replace(/_/g, '-');
|
||||
|
||||
const generateConfig: GenerateConfig<DateTime> = {
|
||||
// get
|
||||
getNow: () => DateTime.local(),
|
||||
getFixedDate: (string) => DateTime.fromFormat(string, 'yyyy-MM-dd'),
|
||||
getEndDate: (date) => date.endOf('month'),
|
||||
getWeekDay: (date) => date.weekday,
|
||||
getYear: (date) => date.year,
|
||||
getMonth: (date) => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12
|
||||
getDate: (date) => date.day,
|
||||
getHour: (date) => date.hour,
|
||||
getMinute: (date) => date.minute,
|
||||
getSecond: (date) => date.second,
|
||||
getMillisecond: (date) => date.millisecond,
|
||||
|
||||
// set
|
||||
addYear: (date, diff) => date.plus({ year: diff }),
|
||||
addMonth: (date, diff) => date.plus({ month: diff }),
|
||||
addDate: (date, diff) => date.plus({ day: diff }),
|
||||
setYear: (date, year) => date.set({ year }),
|
||||
setMonth: (date, month) => date.set({ month: month + 1 }), // setMonth month argument is 0-11, luxon months are 1-12
|
||||
setDate: (date, day) => date.set({ day }),
|
||||
setHour: (date, hour) => date.set({ hour }),
|
||||
setMinute: (date, minute) => date.set({ minute }),
|
||||
setSecond: (date, second) => date.set({ second }),
|
||||
setMillisecond: (date, milliseconds) => date.set({ millisecond: milliseconds }),
|
||||
|
||||
// Compare
|
||||
isAfter: (date1, date2) => date1 > date2,
|
||||
isValidate: (date) => date.isValid,
|
||||
|
||||
locale: {
|
||||
getWeekFirstDate: (locale, date) => date.setLocale(normalizeLocale(locale)).startOf('week'),
|
||||
getWeekFirstDay: (locale) =>
|
||||
DateTime.local().setLocale(normalizeLocale(locale)).startOf('week').weekday,
|
||||
getWeek: (locale, date) => date.setLocale(normalizeLocale(locale)).weekNumber,
|
||||
getShortWeekDays: (locale) => {
|
||||
const weekdays = Info.weekdays(weekDayFormatMap[locale] || 'short', {
|
||||
locale: normalizeLocale(locale),
|
||||
});
|
||||
|
||||
const shifted = weekdays.map((weekday) => weekday.slice(0, weekDayLengthMap[locale]));
|
||||
|
||||
// getShortWeekDays should return weekday labels starting from Sunday.
|
||||
// luxon returns them starting from Monday, so we have to shift the results.
|
||||
shifted.unshift(shifted.pop() as string);
|
||||
|
||||
return shifted;
|
||||
},
|
||||
getShortMonths: (locale) => Info.months('short', { locale: normalizeLocale(locale) }),
|
||||
format: (locale, date, format) => {
|
||||
if (!date || !date.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date.setLocale(normalizeLocale(locale)).toFormat(normalizeFormat(format));
|
||||
},
|
||||
parse: (locale, text, formats) => {
|
||||
for (let i = 0; i < formats.length; i += 1) {
|
||||
const normalizedFormat = normalizeFormat(formats[i]);
|
||||
|
||||
const date = DateTime.fromFormat(text, normalizedFormat, {
|
||||
locale: normalizeLocale(locale),
|
||||
});
|
||||
|
||||
if (date.isValid) {
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default generateConfig;
|
||||
|
||||
export type GenerateConfig<DateType> = {
|
||||
// Get
|
||||
getWeekDay: (value: DateType) => number;
|
||||
getMillisecond: (value: DateType) => number;
|
||||
getSecond: (value: DateType) => number;
|
||||
getMinute: (value: DateType) => number;
|
||||
getHour: (value: DateType) => number;
|
||||
getDate: (value: DateType) => number;
|
||||
getMonth: (value: DateType) => number;
|
||||
getYear: (value: DateType) => number;
|
||||
getNow: () => DateType;
|
||||
getFixedDate: (fixed: string) => DateType;
|
||||
getEndDate: (value: DateType) => DateType;
|
||||
|
||||
// Set
|
||||
addYear: (value: DateType, diff: number) => DateType;
|
||||
addMonth: (value: DateType, diff: number) => DateType;
|
||||
addDate: (value: DateType, diff: number) => DateType;
|
||||
setYear: (value: DateType, year: number) => DateType;
|
||||
setMonth: (value: DateType, month: number) => DateType;
|
||||
setDate: (value: DateType, date: number) => DateType;
|
||||
setHour: (value: DateType, hour: number) => DateType;
|
||||
setMinute: (value: DateType, minute: number) => DateType;
|
||||
setSecond: (value: DateType, second: number) => DateType;
|
||||
setMillisecond: (value: DateType, millisecond: number) => DateType;
|
||||
|
||||
// Compare
|
||||
isAfter: (date1: DateType, date2: DateType) => boolean;
|
||||
isValidate: (date: DateType) => boolean;
|
||||
|
||||
locale: {
|
||||
getWeekFirstDay: (locale: string) => number;
|
||||
getWeekFirstDate: (locale: string, value: DateType) => DateType;
|
||||
getWeek: (locale: string, value: DateType) => number;
|
||||
|
||||
format: (locale: string, date: DateType, format: string) => string;
|
||||
|
||||
/** Should only return validate date instance */
|
||||
parse: (locale: string, text: string, formats: string[]) => DateType | null;
|
||||
|
||||
/** A proxy for getting locale with moment or other locale library */
|
||||
getShortWeekDays?: (locale: string) => string[];
|
||||
/** A proxy for getting locale with moment or other locale library */
|
||||
getShortMonths?: (locale: string) => string[];
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
import { DatePicker } from 'antd';
|
||||
import { PickerTimeProps } from 'antd/es/time-picker';
|
||||
import React from 'react';
|
||||
import luxonGenerateConfig from 'rc-picker/lib/generate/luxon';
|
||||
import luxonGenerateConfig from './config';
|
||||
|
||||
const CustomPicker = DatePicker.generatePicker<DateTime>(luxonGenerateConfig);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Button } from 'antd';
|
|||
import React from 'react';
|
||||
import DateRangePicker from '@wojtekmaj/react-daterange-picker';
|
||||
import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css';
|
||||
import 'react-calendar/dist/Calendar.css';
|
||||
import './ReactCalendar.css';
|
||||
|
||||
import { TimePicker } from 'App/components/shared/DatePicker';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
.react-calendar {
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
border: 1px solid #a0a096;
|
||||
font-family: 'Arial', 'Helvetica', sans-serif;
|
||||
line-height: 1.125em;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer {
|
||||
display: flex;
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer > * {
|
||||
width: 50%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar,
|
||||
.react-calendar *,
|
||||
.react-calendar *:before,
|
||||
.react-calendar *:after {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.react-calendar button {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-calendar button:enabled:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-calendar__navigation {
|
||||
display: flex;
|
||||
height: 44px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.react-calendar__navigation button {
|
||||
min-width: 44px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.react-calendar__navigation button:disabled {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.react-calendar__navigation button:enabled:hover,
|
||||
.react-calendar__navigation button:enabled:focus {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font: inherit;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers .react-calendar__tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font: inherit;
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
color: #d10000;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth,
|
||||
.react-calendar__decade-view__years__year--neighboringDecade,
|
||||
.react-calendar__century-view__decades__decade--neighboringCentury {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.react-calendar__year-view .react-calendar__tile,
|
||||
.react-calendar__decade-view .react-calendar__tile,
|
||||
.react-calendar__century-view .react-calendar__tile {
|
||||
padding: 2em 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
max-width: 100%;
|
||||
padding: 10px 6.6667px;
|
||||
background: none;
|
||||
text-align: center;
|
||||
font: inherit;
|
||||
font-size: 0.833em;
|
||||
}
|
||||
|
||||
.react-calendar__tile:disabled {
|
||||
background-color: #f0f0f0;
|
||||
color: #ababab;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth:disabled,
|
||||
.react-calendar__decade-view__years__year--neighboringDecade:disabled,
|
||||
.react-calendar__century-view__decades__decade--neighboringCentury:disabled {
|
||||
color: #cdcdcd;
|
||||
}
|
||||
|
||||
.react-calendar__tile:enabled:hover,
|
||||
.react-calendar__tile:enabled:focus {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
background: #ffff76;
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:enabled:hover,
|
||||
.react-calendar__tile--now:enabled:focus {
|
||||
background: #ffffa9;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive {
|
||||
background: #76baff;
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:enabled:hover,
|
||||
.react-calendar__tile--hasActive:enabled:focus {
|
||||
background: #a9d4ff;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
background: #006edc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:enabled:hover,
|
||||
.react-calendar__tile--active:enabled:focus {
|
||||
background: #1087ff;
|
||||
}
|
||||
|
||||
.react-calendar--selectRange .react-calendar__tile--hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
|
||||
import GettingStartedModal, { Props } from './GettingStartedModal';
|
||||
import { Step } from './StepList';
|
||||
|
||||
const list: Step[] = [
|
||||
{
|
||||
title: '🕵️ Install OpenReplay',
|
||||
status: 'pending',
|
||||
description: 'Install OpenReplay on your website or mobile app.',
|
||||
icon: 'tools',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Identify Users',
|
||||
status: 'pending',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Integrations',
|
||||
status: 'completed',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
{
|
||||
title: '🕵️ Invite Team Members',
|
||||
status: 'ignored',
|
||||
description: 'Identify users across devices and sessions.',
|
||||
icon: 'users',
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'GettingStarted',
|
||||
component: GettingStartedModal,
|
||||
} as Meta;
|
||||
|
||||
const Template: Story<Props> = (args) => <GettingStartedModal {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
list,
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import SankeyChart, { SankeyChartData } from './SankeyChart';
|
||||
import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
const data: SankeyChartData = {
|
||||
nodes: [
|
||||
{ name: 'Home Page' },
|
||||
{ name: 'Dashboard' },
|
||||
{ name: 'Preferences' },
|
||||
{ name: 'Billing' },
|
||||
],
|
||||
links: [
|
||||
{ source: 0, target: 1, value: 100 },
|
||||
{ source: 1, target: 2, value: 50 },
|
||||
{ source: 1, target: 3, value: 50 },
|
||||
{ source: 2, target: 3, value: 10 },
|
||||
],
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Dashboad/Cards/SankeyChart',
|
||||
component: SankeyChart,
|
||||
} as ComponentMeta<typeof SankeyChart>;
|
||||
|
||||
const Template: ComponentStory<typeof SankeyChart> = (args: any) => <SankeyChart {...args} />;
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
Simple.args = { data };
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import ScatterChart from './ScatterChart';
|
||||
|
||||
const data01 = [
|
||||
{ x: 100, y: 200, z: 200 },
|
||||
{ x: 120, y: 100, z: 260 },
|
||||
{ x: 170, y: 300, z: 400 },
|
||||
{ x: 140, y: 250, z: 280 },
|
||||
{ x: 150, y: 400, z: 500 },
|
||||
{ x: 110, y: 280, z: 200 },
|
||||
];
|
||||
const data02 = [
|
||||
{ x: 200, y: 260, z: 240 },
|
||||
{ x: 240, y: 290, z: 220 },
|
||||
{ x: 190, y: 290, z: 250 },
|
||||
{ x: 198, y: 250, z: 210 },
|
||||
{ x: 180, y: 280, z: 260 },
|
||||
{ x: 210, y: 220, z: 230 },
|
||||
];
|
||||
|
||||
storiesOf('ScatterChart', module).add('Pure', () => (
|
||||
<ScatterChart dataFirst={data01} dataSecond={data02} />
|
||||
));
|
||||
|
|
@ -1,21 +1,10 @@
|
|||
import { issues_types, types } from 'Types/session/issue';
|
||||
import { Segmented } from 'antd';
|
||||
import { Angry, CircleAlert, HandIcon, Skull, WifiOff } from 'lucide-react';
|
||||
import { Angry, CircleAlert, Skull, WifiOff } from 'lucide-react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
interface Tag {
|
||||
name: string;
|
||||
type: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
}
|
||||
|
||||
type Props = StateProps;
|
||||
|
||||
const tagIcons = {
|
||||
[types.ALL]: undefined,
|
||||
[types.JS_EXCEPTION]: <CircleAlert size={14} />,
|
||||
|
|
@ -25,7 +14,7 @@ const tagIcons = {
|
|||
[types.TAP_RAGE]: <Angry size={14} />,
|
||||
} as Record<string, any>;
|
||||
|
||||
const SessionTags: React.FC<Props> = () => {
|
||||
const SessionTags = () => {
|
||||
const { projectsStore, sessionStore, searchStore } = useStore();
|
||||
const total = sessionStore.total;
|
||||
const platform = projectsStore.active?.platform || '';
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default, TagItem } from './SessionTags';
|
||||
export { default } from './SessionTags';
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import ResultTimings from './ResultTimings';
|
||||
|
||||
const timing = [
|
||||
{ "name": "connectStart", "value": 1602181968963 },
|
||||
{ "name": "navigationStart", "value": 1602181965923 },
|
||||
{ "name": "loadEventEnd", "value": 1602181993795 },
|
||||
{ "name": "domLoading", "value": 1602181971233 },
|
||||
{ "name": "secureConnectionStart", "value": 1602181969010 },
|
||||
{ "name": "fetchStart", "value": 1602181965976 },
|
||||
{ "name": "domContentLoadedEventStart", "value": 1602181980930 },
|
||||
{ "name": "responseStart", "value": 1602181970432 },
|
||||
{ "name": "responseEnd", "value": 1602181970763 },
|
||||
{ "name": "domInteractive", "value": 1602181980930 },
|
||||
{ "name": "domainLookupEnd", "value": 1602181968963 },
|
||||
{ "name": "redirectStart", "value": 0 },
|
||||
{ "name": "requestStart", "value": 1602181969932 },
|
||||
{ "name": "unloadEventEnd", "value": 0 },
|
||||
{ "name": "unloadEventStart", "value": 0 },
|
||||
{ "name": "domComplete", "value": 1602181993794 },
|
||||
{ "name": "domainLookupStart", "value": 1602181968871 },
|
||||
{ "name": "loadEventStart", "value": 1602181993794 },
|
||||
{ "name": "domContentLoadedEventEnd", "value": 1602181982868 },
|
||||
{ "name": "redirectEnd", "value": 0 },
|
||||
{ "name": "connectEnd", "value": 1602181969783 }
|
||||
];
|
||||
|
||||
storiesOf('Shared', module)
|
||||
.add('ResultTimings', () => (
|
||||
<ResultTimings timing={timing} />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import BackLink from '.';
|
||||
|
||||
storiesOf('BackLink', module)
|
||||
.add('Pure', () => (
|
||||
<BackLink />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Button from '.';
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('Pure', () => (
|
||||
<Button />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Checkbox from '.';
|
||||
|
||||
storiesOf('Checkbox', module)
|
||||
.add('Pure', () => (
|
||||
<Checkbox />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import CircularLoader from '.';
|
||||
|
||||
storiesOf('CircularLoader', module)
|
||||
.add('Pure', () => (
|
||||
<CircularLoader />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import CloseButton from '.';
|
||||
|
||||
storiesOf('CloseButton', module)
|
||||
.add('Pure', () => (
|
||||
<CloseButton />
|
||||
))
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Icon from '.';
|
||||
|
||||
storiesOf('Icon', module)
|
||||
.add('Pure', () => (
|
||||
<Icon />
|
||||
))
|
||||
.add('Icon', () => (
|
||||
<Icon name="close" />
|
||||
))
|
||||
.add('Icon Size 16', () => (
|
||||
<Icon name="close" size="16" />
|
||||
))
|
||||
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import IconButton from '.';
|
||||
|
||||
storiesOf('IconButton', module)
|
||||
.add('Pure', () => (
|
||||
<IconButton />
|
||||
))
|
||||
.add('Icon', () => (
|
||||
<IconButton icon="cog" />
|
||||
))
|
||||
.add('Icon & Label', () => (
|
||||
<IconButton icon="cog" label="Button" />
|
||||
))
|
||||
.add('Plain', () => (
|
||||
<IconButton icon="cog" label="Button" plain />
|
||||
))
|
||||
.add('Primary', () => (
|
||||
<IconButton icon="cog" label="Button" primary />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import ItemMenu from '.';
|
||||
|
||||
storiesOf('ItemMenu', module)
|
||||
.add('Pure', () => (
|
||||
<ItemMenu />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Label from '.';
|
||||
|
||||
storiesOf('Label', module)
|
||||
.add('Pure', () => (
|
||||
<Label />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Link from '.';
|
||||
|
||||
storiesOf('Link', module)
|
||||
.add('Pure', () => (
|
||||
<Link />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import LinkStyledInput from '.';
|
||||
|
||||
storiesOf('LinkStyledInput', module)
|
||||
.add('Pure', () => (
|
||||
<LinkStyledInput />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Loader from '.';
|
||||
|
||||
storiesOf('Loader', module)
|
||||
.add('Pure', () => (
|
||||
<Loader />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Message from '.';
|
||||
|
||||
storiesOf('Message', module)
|
||||
.add('Pure', () => (
|
||||
<Message />
|
||||
))
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import NoContent from '.';
|
||||
|
||||
storiesOf('NoContent', module)
|
||||
.add('Pure', () => (
|
||||
<NoContent />
|
||||
))
|
||||
.add('Text and icon', () => (
|
||||
<NoContent icon subtext="this is subtext to be displayed."/>
|
||||
))
|
||||
.add('Empty Content', () => (
|
||||
<NoContent empty icon subtext="this is subtext to be displayed."/>
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Notification from '.';
|
||||
|
||||
storiesOf('Notification', module)
|
||||
.add('Pure', () => (
|
||||
<Notification />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import PopMenu from '.';
|
||||
|
||||
storiesOf('PopMenu', module)
|
||||
.add('Pure', () => (
|
||||
<PopMenu />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import SegmentSelection from '.';
|
||||
|
||||
storiesOf('SegmentSelection', module)
|
||||
.add('Pure', () => (
|
||||
<SegmentSelection />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import SlideModal from '.';
|
||||
|
||||
storiesOf('SlideModal', module)
|
||||
.add('Pure', () => (
|
||||
<SlideModal />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import SplitButton from './SplitButton';
|
||||
|
||||
storiesOf('SplitButton', module)
|
||||
.add('Pure', () => (
|
||||
<SplitButton label="Issues" primary icon="plus" />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Tabs from '.';
|
||||
|
||||
storiesOf('Tabs', module)
|
||||
.add('Pure', () => (
|
||||
<Tabs />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import TagBadge from '.';
|
||||
|
||||
storiesOf('TagBadge', module)
|
||||
.add('Pure', () => (
|
||||
<TagBadge />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import TagInput from '.';
|
||||
|
||||
storiesOf('TagInput', module)
|
||||
.add('Pure', () => (
|
||||
<TagInput />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import TagList from '.';
|
||||
|
||||
storiesOf('TagList', module)
|
||||
.add('Pure', () => (
|
||||
<TagList />
|
||||
))
|
||||
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import TextEllipsis from '.';
|
||||
|
||||
storiesOf('TextEllipsis', module)
|
||||
.add('Pure', () => (
|
||||
<TextEllipsis />
|
||||
))
|
||||
.add('Normal Text', () => (
|
||||
<TextEllipsis popupProps={{ wide: 'very'}}>
|
||||
{'this is test'}
|
||||
</TextEllipsis>
|
||||
))
|
||||
.add('Inverted', () => (
|
||||
<TextEllipsis popupProps={{ wide: 'very', inverted: true }}>
|
||||
{'this is test'}
|
||||
</TextEllipsis>
|
||||
))
|
||||
.add('Bigger Text', () => (
|
||||
<TextEllipsis popupProps={{ wide: 'very'}}>
|
||||
<div style={{width: '200px', }}>{'this is the biggest text in the application to test the popup content. this is the biggest text in the application to test the popup content.'}</div>
|
||||
</TextEllipsis>
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import TextLabel from '.';
|
||||
|
||||
storiesOf('TextLabel', module)
|
||||
.add('Pure', () => (
|
||||
<TextLabel />
|
||||
))
|
||||
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import Toggler from '.';
|
||||
|
||||
storiesOf('Toggler', module)
|
||||
.add('Pure', () => (
|
||||
<Toggler />
|
||||
))
|
||||
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import SideMenuItem from './SideMenuItem';
|
||||
import { Avatar, ErrorItem, ErrorFrame, ErrorDetails, TimelinePointer } from 'UI';
|
||||
import Error from 'Types/session/error';
|
||||
import ErrorStackModel from 'Types/session/errorStack';
|
||||
|
||||
const errorStack = ErrorStackModel(
|
||||
{
|
||||
"url": "https://staging.openreplay.com/app-1cac32a.js",
|
||||
"args": [],
|
||||
"func": "FilterModal._this.onFilterClick",
|
||||
"line": 75,
|
||||
"column": 100,
|
||||
"context": [
|
||||
[ 70, " });" ],
|
||||
[ 71, " } else {" ],
|
||||
[ 72, " props.fetchSession(props.sessionId).then(() => {" ],
|
||||
[ 73, " const { session } = this.props;" ],
|
||||
[ 74, " if (!session.sessionId) return; // shouldn't be. On first load component constructed twise somewhy" ],
|
||||
[ 75, " initPlayer(session, props.jwt);" ],
|
||||
[ 76, " });" ],
|
||||
[ 77, " }" ],
|
||||
[ 78, " }" ],
|
||||
[ 79, "" ],
|
||||
[ 80, " componentDidUpdate(prevProps) {" ]
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
const errors = [
|
||||
Error({
|
||||
"sessionId": 2315691667741445,
|
||||
"messageId": 220546,
|
||||
"timestamp": 1585335179312,
|
||||
"errorId": "1_5c3b207b20a36c08c408c4990b9f5cbc",
|
||||
"projectId": 1,
|
||||
"source": "js_exception",
|
||||
"name": "TypeError",
|
||||
"message": "Cannot read property '0' of undefined",
|
||||
"payload": {
|
||||
"mode": "stack",
|
||||
"stack": [
|
||||
{
|
||||
"url": "https://staging.openreplay.com/app-1cac32a.js",
|
||||
"args": [],
|
||||
"func": "FilterModal._this.onFilterClick",
|
||||
"line": 3233,
|
||||
"column": 62,
|
||||
"context": []
|
||||
},
|
||||
{
|
||||
"url": "https://staging.openreplay.com/app-1cac32a.js",
|
||||
"args": [],
|
||||
"func": "onClick",
|
||||
"line": 3342,
|
||||
"column": 25,
|
||||
"context": []
|
||||
},
|
||||
]
|
||||
},
|
||||
"status": "unresolved",
|
||||
"parentErrorId": null
|
||||
})
|
||||
]
|
||||
|
||||
storiesOf('UI Components', module)
|
||||
.add('SideMenuItem', () => (
|
||||
<SideMenuItem title="Menu Label" />
|
||||
))
|
||||
.add('SideMenuItem active', () => (
|
||||
<SideMenuItem title="Menu Label" active />
|
||||
))
|
||||
.add('Avatar', () => (
|
||||
<Avatar />
|
||||
))
|
||||
.add('ErrorItem', () => (
|
||||
<ErrorItem error={errors[0]} />
|
||||
))
|
||||
.add('ErrorFrame', () => (
|
||||
<ErrorFrame stack={errorStack} />
|
||||
))
|
||||
.add('ErrorDetails', () => (
|
||||
<div className="p-4 bg-white">
|
||||
<ErrorDetails error={errors[0]} />
|
||||
</div>
|
||||
))
|
||||
.add('Timeline POinter', () => (
|
||||
<TimelinePointer />
|
||||
))
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ class SearchStore {
|
|||
}
|
||||
|
||||
edit(instance: Partial<Search>) {
|
||||
this.instance = new Search(Object.assign(this.instance.toData(), instance));
|
||||
this.instance = new Search(Object.assign({ ...this.instance }, instance));
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class SearchStoreLive {
|
|||
}
|
||||
|
||||
edit(instance: Partial<Search>) {
|
||||
this.instance = new Search(Object.assign(this.instance.toData(), instance));
|
||||
this.instance = new Search(Object.assign({ ...this.instance }, instance));
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ export default class Screen {
|
|||
|
||||
attach(parentElement: HTMLElement) {
|
||||
if (this.parentElement) {
|
||||
this.parentElement = null;
|
||||
console.warn('BaseScreen: reattaching the screen.');
|
||||
console.error('!!! web/Screen.ts#108: Tried to reattach the parent element.');
|
||||
return;
|
||||
}
|
||||
|
||||
parentElement.appendChild(this.screen);
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ export default class StylesManager {
|
|||
this.linkLoadingCount++;
|
||||
this.setLoading(true);
|
||||
const addSkipAndResolve = (e: any) => {
|
||||
this.skipCSSLinks.push(value); // watch out
|
||||
this.skipCSSLinks.push(value);
|
||||
logger.error('skip node', e)
|
||||
resolve()
|
||||
}
|
||||
timeoutId = setTimeout(addSkipAndResolve, 4000);
|
||||
timeoutId = setTimeout(() => addSkipAndResolve('by timeout'), 5000);
|
||||
|
||||
node.onload = () => {
|
||||
const doc = this.screen.document;
|
||||
|
|
@ -55,6 +55,7 @@ export default class StylesManager {
|
|||
this.linkLoadingCount--;
|
||||
if (this.linkLoadingCount === 0) {
|
||||
this.setLoading(false)
|
||||
this.linkLoadPromises = [];
|
||||
}
|
||||
});
|
||||
this.linkLoadPromises.push(promise);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,8 @@
|
|||
"gen:constants": "node ./scripts/constants.js",
|
||||
"gen:icons": "node ./scripts/icons.js",
|
||||
"gen:colors": "node ./scripts/colors.js",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"flow": "flow",
|
||||
"gen:static": "yarn gen:icons && yarn gen:colors",
|
||||
"build-storybook": "build-storybook",
|
||||
"test:ci": "jest --maxWorkers=1 --no-cache --coverage",
|
||||
"test": "jest --watch",
|
||||
"cy:open": "cypress open",
|
||||
|
|
@ -52,9 +50,9 @@
|
|||
"lucide-react": "^0.396.0",
|
||||
"luxon": "^3.5.0",
|
||||
"microdiff": "^1.4.0",
|
||||
"mobx": "^6.3.8",
|
||||
"mobx": "^6.13.3",
|
||||
"mobx-persist-store": "^1.1.5",
|
||||
"mobx-react-lite": "^3.1.6",
|
||||
"mobx-react-lite": "^4.0.7",
|
||||
"peerjs": "1.3.2",
|
||||
"prismjs": "^1.29.0",
|
||||
"rc-time-picker": "^3.7.3",
|
||||
|
|
@ -66,7 +64,7 @@
|
|||
"react-dom": "^18.2.0",
|
||||
"react-draggable": "^4.4.5",
|
||||
"react-google-recaptcha": "^2.1.0",
|
||||
"react-lazyload": "^3.2.0",
|
||||
"react-intersection-observer": "^9.13.1",
|
||||
"react-merge-refs": "^2.0.1",
|
||||
"react-router": "^5.3.3",
|
||||
"react-router-dom": "^5.3.3",
|
||||
|
|
@ -77,7 +75,7 @@
|
|||
"recharts": "^2.12.7",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"syncod": "^0.0.1",
|
||||
"virtua": "^0.33.4"
|
||||
"virtua": "^0.35.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.23.0",
|
||||
|
|
@ -96,15 +94,6 @@
|
|||
"@babel/runtime": "^7.23.2",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@openreplay/sourcemap-uploader": "^3.0.8",
|
||||
"@storybook/addon-actions": "^6.5.12",
|
||||
"@storybook/addon-docs": "^6.5.12",
|
||||
"@storybook/addon-essentials": "^6.5.12",
|
||||
"@storybook/addon-interactions": "^6.5.12",
|
||||
"@storybook/addon-links": "^6.5.12",
|
||||
"@storybook/builder-webpack5": "^6.5.12",
|
||||
"@storybook/manager-webpack5": "^6.5.12",
|
||||
"@storybook/react": "^6.5.12",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/prismjs": "^1",
|
||||
|
|
@ -161,5 +150,5 @@
|
|||
"engines": {
|
||||
"node": ">=10.14.1"
|
||||
},
|
||||
"packageManager": "yarn@3.2.1"
|
||||
"packageManager": "yarn@4.5.0"
|
||||
}
|
||||
|
|
|
|||
19053
frontend/yarn.lock
19053
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -81,7 +81,6 @@ up to date with every new library you use.
|
|||
| react-dom | MIT | JavaScript |
|
||||
| react-google-recaptcha | MIT | JavaScript |
|
||||
| react-json-view | MIT | JavaScript |
|
||||
| react-lazyload | MIT | JavaScript |
|
||||
| react-redux | MIT | JavaScript |
|
||||
| react-router | MIT | JavaScript |
|
||||
| react-router-dom | MIT | JavaScript |
|
||||
|
|
@ -122,4 +121,5 @@ up to date with every new library you use.
|
|||
| @wojtekmaj/react-daterange-picker | MIT | JavaScript |
|
||||
| prismjs | MIT | JavaScript |
|
||||
| virtua | MIT | JavaScript |
|
||||
| babel-plugin-prismjs | MIT | JavaScript |
|
||||
| babel-plugin-prismjs | MIT | JavaScript |
|
||||
| react-intersection-observer | MIT | JavaScript |
|
||||
Binary file not shown.
|
|
@ -1 +1 @@
|
|||
nodeLinker: node-modules
|
||||
nodeLinker: pnpm
|
||||
|
|
|
|||
|
|
@ -335,17 +335,25 @@ export default class App {
|
|||
this.revID = this.options.revID
|
||||
this.localStorage = this.options.localStorage ?? window.localStorage
|
||||
this.sessionStorage = this.options.sessionStorage ?? window.sessionStorage
|
||||
this.sanitizer = new Sanitizer(this, options)
|
||||
this.nodes = new Nodes(this.options.node_id, Boolean(options.angularMode))
|
||||
this.observer = new Observer(this, options)
|
||||
this.sanitizer = new Sanitizer({ app: this, options })
|
||||
this.nodes = new Nodes({
|
||||
node_id: this.options.node_id,
|
||||
angularMode: Boolean(options.angularMode),
|
||||
})
|
||||
this.observer = new Observer({ app: this, options })
|
||||
this.ticker = new Ticker(this)
|
||||
this.ticker.attach(() => this.commit())
|
||||
this.debug = new Logger(this.options.__debug__)
|
||||
this.session = new Session(this, this.options)
|
||||
this.attributeSender = new AttributeSender(this, Boolean(this.options.disableStringDict))
|
||||
this.session = new Session({ app: this, options: this.options })
|
||||
this.attributeSender = new AttributeSender({
|
||||
app: this,
|
||||
isDictDisabled: Boolean(this.options.disableStringDict || this.options.crossdomain?.enabled),
|
||||
})
|
||||
this.featureFlags = new FeatureFlags(this)
|
||||
this.tagWatcher = new TagWatcher(this.sessionStorage, this.debug.error, (tag) => {
|
||||
this.send(TagTrigger(tag) as Message)
|
||||
this.tagWatcher = new TagWatcher({
|
||||
sessionStorage: this.sessionStorage,
|
||||
errLog: this.debug.error,
|
||||
onTag: (tag) => this.send(TagTrigger(tag) as Message),
|
||||
})
|
||||
this.session.attachUpdateCallback(({ userID, metadata }) => {
|
||||
if (userID != null) {
|
||||
|
|
@ -895,12 +903,12 @@ export default class App {
|
|||
|
||||
const createListener = () =>
|
||||
target
|
||||
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
? createEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
const deleteListener = () =>
|
||||
target
|
||||
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
? deleteEventListener(target, type, listener, useCapture, this.options.angularMode)
|
||||
: null
|
||||
|
||||
this.attachStartCallback(createListener, useSafe)
|
||||
this.attachStopCallback(deleteListener, useSafe)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,13 @@ export default class Nodes {
|
|||
private readonly nodeCallbacks: Array<NodeCallback> = []
|
||||
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
|
||||
private nextNodeId = 0
|
||||
private readonly node_id: string
|
||||
private readonly angularMode: boolean
|
||||
|
||||
constructor(
|
||||
private readonly node_id: string,
|
||||
private readonly angularMode: boolean,
|
||||
) {}
|
||||
constructor(params: { node_id: string; angularMode: boolean }) {
|
||||
this.node_id = params.node_id
|
||||
this.angularMode = params.angularMode
|
||||
}
|
||||
|
||||
syntheticMode(frameOrder: number) {
|
||||
const maxSafeNumber = Number.MAX_SAFE_INTEGER
|
||||
|
|
@ -36,7 +38,12 @@ export default class Nodes {
|
|||
this.nodes.forEach((node) => cb(node))
|
||||
}
|
||||
|
||||
attachNodeListener = (node: Node, type: string, listener: EventListener, useCapture = true): void => {
|
||||
attachNodeListener = (
|
||||
node: Node,
|
||||
type: string,
|
||||
listener: EventListener,
|
||||
useCapture = true,
|
||||
): void => {
|
||||
const id = this.getID(node)
|
||||
if (id === undefined) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -22,14 +22,16 @@ const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () =>
|
|||
export default class TopObserver extends Observer {
|
||||
private readonly options: Options
|
||||
private readonly iframeOffsets: IFrameOffsets = new IFrameOffsets()
|
||||
readonly app: App
|
||||
|
||||
constructor(app: App, options: Partial<Options>) {
|
||||
super(app, true)
|
||||
constructor(params: { app: App; options: Partial<Options> }) {
|
||||
super(params.app, true)
|
||||
this.app = params.app
|
||||
this.options = Object.assign(
|
||||
{
|
||||
captureIFrames: true,
|
||||
},
|
||||
options,
|
||||
params.options,
|
||||
)
|
||||
|
||||
// IFrames
|
||||
|
|
|
|||
|
|
@ -23,17 +23,16 @@ export default class Sanitizer {
|
|||
private readonly obscured: Set<number> = new Set()
|
||||
private readonly hidden: Set<number> = new Set()
|
||||
private readonly options: Options
|
||||
private readonly app: App
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
options: Partial<Options>,
|
||||
) {
|
||||
constructor(params: { app: App; options?: Partial<Options> }) {
|
||||
this.app = params.app
|
||||
this.options = Object.assign(
|
||||
{
|
||||
obscureTextEmails: true,
|
||||
obscureTextNumbers: false,
|
||||
},
|
||||
options,
|
||||
params.options,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,11 +35,12 @@ export default class Session {
|
|||
private tabId: string
|
||||
public userInfo: UserInfo
|
||||
private token: string | undefined
|
||||
private readonly app: App
|
||||
private readonly options: Options
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly options: Options,
|
||||
) {
|
||||
constructor(params: { app: App; options: Options }) {
|
||||
this.app = params.app
|
||||
this.options = params.options
|
||||
this.createTabId()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ export class StringDictionary {
|
|||
|
||||
export default class AttributeSender {
|
||||
private dict = new StringDictionary()
|
||||
|
||||
constructor(
|
||||
private readonly app: App,
|
||||
private readonly isDictDisabled: boolean,
|
||||
) {}
|
||||
private readonly app: App
|
||||
private readonly isDictDisabled: boolean
|
||||
constructor(options: { app: App; isDictDisabled: boolean }) {
|
||||
this.app = options.app
|
||||
this.isDictDisabled = options.isDictDisabled
|
||||
}
|
||||
|
||||
public sendSetAttribute(id: number, name: string, value: string) {
|
||||
if (this.isDictDisabled) {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,19 @@ class TagWatcher {
|
|||
intervals: Record<string, ReturnType<typeof setInterval>> = {}
|
||||
tags: { id: number; selector: string }[] = []
|
||||
observer: IntersectionObserver
|
||||
private readonly sessionStorage: Storage
|
||||
private readonly errLog: (args: any[]) => void
|
||||
private readonly onTag: (tag: number) => void
|
||||
|
||||
constructor(params: {
|
||||
sessionStorage: Storage
|
||||
errLog: (args: any[]) => void
|
||||
onTag: (tag: number) => void
|
||||
}) {
|
||||
this.sessionStorage = params.sessionStorage
|
||||
this.errLog = params.errLog
|
||||
this.onTag = params.onTag
|
||||
|
||||
constructor(
|
||||
private readonly sessionStorage: Storage,
|
||||
private readonly errLog: (args: any[]) => void,
|
||||
private readonly onTag: (tag: number) => void,
|
||||
) {
|
||||
const tags: { id: number; selector: string }[] = JSON.parse(
|
||||
sessionStorage.getItem(WATCHED_TAGS_KEY) ?? '[]',
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue