feat(ui) - path analysis (#1514)
* fix(ui) - insight item name * feat(ui) - path analysis - wip * feat(ui) - path analysis - wip * feat(ui) - path analysis * feat(ui) - user retention * feat(ui): path analysis - filters and graph * change(ui): plugins text and icon * feat(ui): path analysis - filters and graph * feat(ui): path analysis - filters and graph * feat(ui): path analysis * feat(ui): path analysis - start point filters * feat(ui): path analysis
This commit is contained in:
parent
429e55b746
commit
89704b033f
50 changed files with 2493 additions and 864 deletions
|
|
@ -18,8 +18,8 @@ export interface Module {
|
|||
|
||||
export const modules = [
|
||||
{
|
||||
label: 'Assist',
|
||||
description: 'Record and replay user sessions to see a video of what users did on your website.',
|
||||
label: 'Cobrowse',
|
||||
description: 'Enable live session playing, interaction, screen sharing, and annotations over video call.',
|
||||
key: MODULES.ASSIST,
|
||||
icon: 'broadcast'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
.cohortTableContainer {
|
||||
display: flex;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fixedTableWrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.scrollableTableWrapper {
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cohortTable {
|
||||
border-collapse: separate;
|
||||
width: max-content;
|
||||
border-spacing: 6px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
border: transparent;
|
||||
border-radius: 3px;
|
||||
padding: 4px 8px;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.header .bg {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import React from 'react';
|
||||
import styles from './CohortCard.module.css';
|
||||
|
||||
|
||||
interface Props {
|
||||
data: any
|
||||
}
|
||||
function CohortCard(props: Props) {
|
||||
// const { data } = props;
|
||||
const data = [
|
||||
{
|
||||
cohort: '2022-01-01',
|
||||
users: 100,
|
||||
data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
{
|
||||
cohort: '2022-01-08',
|
||||
users: 100,
|
||||
data: [90, 70, 50, 30],
|
||||
},
|
||||
// ... more rows
|
||||
];
|
||||
|
||||
const getCellColor = (value: number) => {
|
||||
const maxValue = 100; // Adjust this based on the maximum value in your data
|
||||
const maxOpacity = 0.5;
|
||||
const opacity = (value / maxValue) * maxOpacity;
|
||||
return `rgba(62, 170, 175, ${opacity})`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.cohortTableContainer}>
|
||||
<div className={styles.fixedTableWrapper}>
|
||||
<table className={styles.cohortTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={`${styles.cell} text-left`}>Date</th>
|
||||
<th className={`${styles.cell} text-left`}>Users</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className={`${styles.cell} ${styles.header}`}></th>
|
||||
<th className={`${styles.cell} ${styles.header}`}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={`row-fixed-${rowIndex}`}>
|
||||
<td className={styles.cell}>{row.cohort}</td>
|
||||
<td className={styles.cell}>{row.users}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={styles.scrollableTableWrapper}>
|
||||
<table className={styles.cohortTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={`${styles.cell}`} style={{ textAlign: 'left'}} colSpan={10}>Weeks later users retained</th>
|
||||
</tr>
|
||||
<tr>
|
||||
{data[0].data.map((_, index) => (
|
||||
<th key={`header-${index}`} className={`${styles.cell} ${styles.header}`}>{`${index + 1}`}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((row, rowIndex) => (
|
||||
<tr key={`row-scrollable-${rowIndex}`}>
|
||||
{row.data.map((cell, cellIndex) => (
|
||||
<td key={`cell-${rowIndex}-${cellIndex} text-center`} className={styles.cell} style={{ backgroundColor: getCellColor(cell) }}>{cell}%</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CohortCard;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CohortCard';
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import Issue from 'App/mstore/types/issue';
|
||||
|
||||
interface Props {
|
||||
issue: Issue;
|
||||
}
|
||||
|
||||
function CardIssueItem(props: Props) {
|
||||
const { issue } = props;
|
||||
return (
|
||||
<div className='flex items-center py-2 hover:bg-active-blue cursor-pointer'>
|
||||
<div className='mr-auto flex items-center'>
|
||||
<div className='flex items-center justify-center flex-shrink-0 mr-3 relative'>
|
||||
<Icon name={issue.icon} size='24' className='z-10 inset-0' />
|
||||
</div>
|
||||
<div className='flex-1 overflow-hidden'>
|
||||
{issue.name}
|
||||
<span className='color-gray-medium mx-2'>{issue.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>{issue.sessionCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardIssueItem;
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { Loader, Pagination, Button, NoContent } from 'UI';
|
||||
|
||||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import CardIssueItem from './CardIssueItem';
|
||||
import SessionsModal from '../SessionsModal';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import Issue from 'App/mstore/types/issue';
|
||||
|
||||
function CardIssues() {
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const [data, setData] = useState<{
|
||||
issues: Issue[];
|
||||
total: number;
|
||||
}>({ issues: [], total: 0 });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const widget: any = useObserver(() => metricStore.instance);
|
||||
const isMounted = useIsMounted();
|
||||
const { showModal } = useModal();
|
||||
|
||||
const fetchIssues = (filter: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
|
||||
const newFilter = {
|
||||
...filter,
|
||||
series: filter.series.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
filters: item.filter.filters.filter((filter: any, index: any) => {
|
||||
const stage = widget.data.funnel.stages[index];
|
||||
return stage && stage.isActive;
|
||||
}).map((f: any) => f.toJson())
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
widget.fetchIssues(newFilter).then((res: any) => {
|
||||
setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleClick = (issue: any) => {
|
||||
showModal(<SessionsModal issue={issue} list={[]} />, { right: true, width: 900 });
|
||||
};
|
||||
|
||||
const filter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []);
|
||||
const depsString = JSON.stringify(widget.series);
|
||||
|
||||
useEffect(() => {
|
||||
const newPayload = {
|
||||
...widget,
|
||||
page: metricStore.sessionsPage,
|
||||
limit: metricStore.sessionsPageSize,
|
||||
filters: filter.filters
|
||||
};
|
||||
console.log('drillDownPeriod', newPayload);
|
||||
debounceRequest(newPayload);
|
||||
}, [drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage]);
|
||||
|
||||
return useObserver(() => (
|
||||
<div className='my-8 bg-white rounded p-4 border'>
|
||||
<div className='flex justify-between'>
|
||||
<h1 className='font-medium text-2xl'>Issues</h1>
|
||||
{/*<div>*/}
|
||||
{/* <Button variant='text-primary'>All Sessions</Button>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
<NoContent show={data.issues.length == 0} title="No data!">
|
||||
{data.issues.map((item: any, index: any) => (
|
||||
<div onClick={() => handleClick(item)} key={index}>
|
||||
<CardIssueItem issue={item} />
|
||||
</div>
|
||||
))}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
|
||||
<div className='w-full flex items-center justify-between pt-4'>
|
||||
<div className='text-disabled-text'>
|
||||
Showing <span
|
||||
className='font-semibold'>{Math.min(data.issues.length, metricStore.sessionsPageSize)}</span> out of{' '}
|
||||
<span className='font-semibold'>{data.total}</span> Issues
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
totalPages={Math.ceil(data.issues.length / metricStore.sessionsPageSize)}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default observer(CardIssues);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CardIssues'
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
interface Props {
|
||||
user: any
|
||||
}
|
||||
function CardUserItem(props: Props) {
|
||||
const { user } = props;
|
||||
return (
|
||||
<div className="flex items-center py-2 hover:bg-active-blue cursor-pointer">
|
||||
<div className="mr-auto flex items-center">
|
||||
<div className="flex items-center justify-center flex-shrink-0 mr-2 relative">
|
||||
<div className="w-8 h-8 rounded-full flex items-center bg-tealx-light justify-center">
|
||||
<Icon name="person-fill" size="15" className="z-10 inset-0" color="tealx" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{user.name}
|
||||
{/* <span className="color-gray-medium mx-2">some-button</span> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 link">{user.sessions}</div>
|
||||
<div><Icon name="chevron-right" size="16" /></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CardUserItem;
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { RouteComponentProps, withRouter } from 'react-router';
|
||||
import { Loader, Pagination, Button } from 'UI';
|
||||
import SessionsModal from './SessionsModal';
|
||||
import CardUserItem from './CardUserItem';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
location: any;
|
||||
}
|
||||
function CardUserList(props: RouteComponentProps<Props>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { showModal } = useModal();
|
||||
const userId = new URLSearchParams(props.location.search).get("userId");
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
|
||||
const [data, setData] = useState<any>([
|
||||
{ name: 'user@domain.com', sessions: 29 },
|
||||
{ name: 'user@domain.com', sessions: 29 },
|
||||
{ name: 'user@domain.com', sessions: 29 },
|
||||
{ name: 'user@domain.com', sessions: 29 },
|
||||
]);
|
||||
const pageSize = data.length;
|
||||
|
||||
const handleClick = (issue: any) => {
|
||||
props.history.replace({search: (new URLSearchParams({userId : '123'})).toString()});
|
||||
// showModal(<SessionsModal list={[]} />, { right: true, width: 450 })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
showModal(<SessionsModal userId={userId} name="test" hash="test" />, { right: true, width: 600, onClose: () => {
|
||||
if (props.history.location.pathname.includes("/metric")) {
|
||||
props.history.replace({search: ""});
|
||||
}
|
||||
}});
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<div className="my-8 bg-white rounded p-4 border">
|
||||
<div className="flex justify-between">
|
||||
<h1 className="font-medium text-2xl">Returning users between</h1>
|
||||
<div>
|
||||
<Button variant="text-primary">All Sessions</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Loader loading={loading}>
|
||||
{data.map((item: any, index: any) => (
|
||||
<div key={index} onClick={() => handleClick(item)}>
|
||||
<CardUserItem user={item} />
|
||||
</div>
|
||||
))}
|
||||
</Loader>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4">
|
||||
<div className="text-disabled-text">
|
||||
Showing <span className="font-semibold">{Math.min(data.length, pageSize)}</span> out of{' '}
|
||||
<span className="font-semibold">{data.length}</span> Issues
|
||||
</div>
|
||||
<Pagination
|
||||
page={metricStore.sessionsPage}
|
||||
totalPages={Math.ceil(data.length / metricStore.sessionsPageSize)}
|
||||
onPageChange={(page: any) => metricStore.updateKey('sessionsPage', page)}
|
||||
limit={metricStore.sessionsPageSize}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(CardUserList));
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { NoContent, Pagination, Loader, Avatar } from 'UI';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { useObserver, observer } from 'mobx-react-lite';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
interface Props {
|
||||
userId: string;
|
||||
hash: string;
|
||||
name: string;
|
||||
}
|
||||
function SessionsModal(props: Props) {
|
||||
const { userId, hash, name } = props;
|
||||
const { sessionStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [data, setData] = React.useState<any>({ sessions: [], total: 0 });
|
||||
const filter = useObserver(() => sessionStore.userFilter);
|
||||
|
||||
const onDateChange = (period: any) => {
|
||||
filter.update('period', period);
|
||||
};
|
||||
|
||||
const fetchData = () => {
|
||||
setLoading(true);
|
||||
sessionStore
|
||||
.getSessions(filter)
|
||||
.then(setData)
|
||||
.catch(() => {
|
||||
console.log('error');
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const userFilter = { key: FilterKey.USERID, value: [userId], operator: 'is', isEvent: false };
|
||||
filter.update('filters', [userFilter]);
|
||||
}, []);
|
||||
useEffect(fetchData, [filter.page, filter.startDate, filter.endDate]);
|
||||
|
||||
return (
|
||||
<div className="h-screen overflow-y-auto bg-white">
|
||||
<div className="flex items-center justify-between w-full px-5 py-3">
|
||||
<div className="text-lg flex items-center">
|
||||
<Avatar isActive={false} seed={hash} isAssist={false} />
|
||||
<div className="ml-3">
|
||||
{name}'s <span className="color-gray-dark">Sessions</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<SelectDateRange period={filter.period} onChange={onDateChange} right={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NoContent show={data.sessions.length === 0} title={
|
||||
<div>
|
||||
<AnimatedSVG name={ICONS.NO_SESSIONS} size={170} />
|
||||
<div className="mt-2" />
|
||||
<div className="text-center text-gray-600">No recordings found.</div>
|
||||
</div>
|
||||
}>
|
||||
<div className="border rounded m-5">
|
||||
<Loader loading={loading}>
|
||||
{data.sessions.map((session: any) => (
|
||||
<div className="border-b last:border-none" key={session.sessionId}>
|
||||
<SessionItem key={session.sessionId} session={session} compact={true} onClick={hideModal} />
|
||||
</div>
|
||||
))}
|
||||
</Loader>
|
||||
|
||||
<div className="flex items-center justify-between p-5">
|
||||
<div>
|
||||
{/* showing x to x of total sessions */}
|
||||
Showing <span className="font-medium">{(filter.page - 1) * PER_PAGE + 1}</span> to{' '}
|
||||
<span className="font-medium">{(filter.page - 1) * PER_PAGE + data.sessions.length}</span> of{' '}
|
||||
<span className="font-medium">{data.total}</span> sessions.
|
||||
</div>
|
||||
<Pagination
|
||||
page={filter.page}
|
||||
totalPages={Math.ceil(data.total / PER_PAGE)}
|
||||
onPageChange={(page) => filter.update('page', page)}
|
||||
limit={PER_PAGE}
|
||||
debounceRequest={1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</NoContent>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SessionsModal);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsModal';
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import filters from 'App/duck/filters';
|
||||
import Filter from 'App/mstore/types/filter';
|
||||
import { FilterKey } from 'App/types/filter/filterType';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import FilterItem from 'Shared/Filters/FilterItem';
|
||||
import cn from 'classnames';
|
||||
|
||||
import { Button } from 'UI';
|
||||
|
||||
interface Props {
|
||||
filter: Filter;
|
||||
}
|
||||
function ExcludeFilters(props: Props) {
|
||||
const { filter } = props;
|
||||
const hasExcludes = filter.excludes.length > 0;
|
||||
|
||||
const addPageFilter = () => {
|
||||
const filterItem = filter.createFilterBykey(FilterKey.LOCATION);
|
||||
filter.addExcludeFilter(filterItem);
|
||||
};
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filterItem: any) => {
|
||||
filter.updateExcludeFilter(filterIndex, filterItem);
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
filter.removeExcludeFilter(filterIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center border-b", { 'p-5' : hasExcludes, 'px-2': !hasExcludes })}>
|
||||
{filter.excludes.length > 0 ? (
|
||||
<div className="flex items-center mb-2 flex-col">
|
||||
<div className="text-sm color-gray-medium mr-auto mb-2">EXCLUDES</div>
|
||||
{filter.excludes.map((f: any, index: number) => (
|
||||
<FilterItem
|
||||
hideIndex={true}
|
||||
filterIndex={index}
|
||||
filter={f}
|
||||
onUpdate={(f) => onUpdateFilter(index, f)}
|
||||
onRemoveFilter={() => onRemoveFilter(index)}
|
||||
// saveRequestPayloads={saveRequestPayloads}
|
||||
disableDelete={false}
|
||||
// excludeFilterKeys={excludeFilterKeys}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Button variant="text-primary" onClick={addPageFilter}>
|
||||
Add Exclustion
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(ExcludeFilters);
|
||||
|
|
@ -5,6 +5,7 @@ import FilterSelection from 'Shared/Filters/FilterSelection';
|
|||
import SeriesName from './SeriesName';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import ExcludeFilters from './ExcludeFilters';
|
||||
|
||||
interface Props {
|
||||
seriesIndex: number;
|
||||
|
|
@ -15,52 +16,57 @@ interface Props {
|
|||
hideHeader?: boolean;
|
||||
emptyMessage?: any;
|
||||
observeChanges?: () => void;
|
||||
excludeFilterKeys?: Array<string>
|
||||
excludeFilterKeys?: Array<string>;
|
||||
canExclude?: boolean;
|
||||
}
|
||||
|
||||
function FilterSeries(props: Props) {
|
||||
const {
|
||||
observeChanges = () => {
|
||||
},
|
||||
observeChanges = () => {},
|
||||
canDelete,
|
||||
hideHeader = false,
|
||||
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
|
||||
supportsEmpty = true,
|
||||
excludeFilterKeys = []
|
||||
excludeFilterKeys = [],
|
||||
canExclude = false,
|
||||
} = props;
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
series.filter.addFilter(filter)
|
||||
observeChanges()
|
||||
}
|
||||
series.filter.addFilter(filter);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter)
|
||||
observeChanges()
|
||||
}
|
||||
series.filter.updateFilter(filterIndex, filter);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onChangeEventsOrder = (_: any, { name, value }: any) => {
|
||||
series.filter.updateKey(name, value)
|
||||
observeChanges()
|
||||
}
|
||||
series.filter.updateKey(name, value);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
const onRemoveFilter = (filterIndex: any) => {
|
||||
series.filter.removeFilter(filterIndex)
|
||||
observeChanges()
|
||||
}
|
||||
series.filter.removeFilter(filterIndex);
|
||||
observeChanges();
|
||||
};
|
||||
|
||||
console.log(series.filter)
|
||||
return (
|
||||
<div className="border rounded bg-white">
|
||||
<div className={cn("border-b px-5 h-12 flex items-center relative", { 'hidden': hideHeader })}>
|
||||
{canExclude && <ExcludeFilters filter={series.filter} />}
|
||||
<div className={cn('border-b px-5 h-12 flex items-center relative', { hidden: hideHeader })}>
|
||||
<div className="mr-auto">
|
||||
<SeriesName seriesIndex={seriesIndex} name={series.name} onUpdate={(name) => series.update('name', name)} />
|
||||
<SeriesName
|
||||
seriesIndex={seriesIndex}
|
||||
name={series.name}
|
||||
onUpdate={(name) => series.update('name', name)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<div onClick={props.onRemoveSeries} className={cn("ml-3", { 'disabled': !canDelete })}>
|
||||
<div onClick={props.onRemoveSeries} className={cn('ml-3', { disabled: !canDelete })}>
|
||||
<Icon name="trash" size="16" />
|
||||
</div>
|
||||
|
||||
|
|
@ -92,7 +98,9 @@ function FilterSeries(props: Props) {
|
|||
onFilterClick={onAddFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
>
|
||||
<Button variant="text-primary" icon="plus">ADD STEP</Button>
|
||||
<Button variant="text-primary" icon="plus">
|
||||
ADD STEP
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -102,4 +110,4 @@ function FilterSeries(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default observer(FilterSeries);
|
||||
export default observer(FilterSeries);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
import { dashboardService, metricService } from 'App/services';
|
||||
import { Loader, Modal, NoContent, Pagination } from 'UI';
|
||||
import SessionItem from 'Shared/SessionItem';
|
||||
import Session from 'App/mstore/types/session';
|
||||
import { useModal } from 'Components/Modal';
|
||||
|
||||
interface Props {
|
||||
list: any,
|
||||
issue: any
|
||||
}
|
||||
|
||||
function SessionsModal(props: Props) {
|
||||
const { issue } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [total, setTotal] = React.useState(0);
|
||||
const [list, setList] = React.useState<any>([]);
|
||||
const { hideModal } = useModal();
|
||||
|
||||
const length = list.length;
|
||||
|
||||
const fetchSessions = async (filter: any) => {
|
||||
setLoading(true);
|
||||
filter.filters = [
|
||||
{
|
||||
type: 'issue',
|
||||
operator: 'is',
|
||||
value: [issue.type]
|
||||
}
|
||||
];
|
||||
const res = await metricService.fetchSessions(null, filter);
|
||||
console.log('res', res);
|
||||
setList(res[0].sessions.map((item: any) => new Session().fromJson(item)));
|
||||
setTotal(res[0].total);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions({ ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page: page });
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions({ ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page: 1 });
|
||||
}, [props.issue]);
|
||||
|
||||
|
||||
return (
|
||||
<div className='bg-white h-screen'>
|
||||
<Modal.Header title='Sessions'>
|
||||
Sessions with selected issue
|
||||
</Modal.Header>
|
||||
<Loader loading={loading}>
|
||||
<NoContent show={length == 0} title='No data!'>
|
||||
{list.map((item: any) => (
|
||||
<SessionItem session={item} onClick={hideModal} />
|
||||
))}
|
||||
</NoContent>
|
||||
</Loader>
|
||||
|
||||
<div className='w-full flex items-center justify-between p-4 absolute bottom-0 bg-white'>
|
||||
<div className='text-disabled-text'>
|
||||
Showing <span
|
||||
className='font-semibold'>{Math.min(length, 10)}</span> out of{' '}
|
||||
<span className='font-semibold'>{total}</span> Issues
|
||||
</div>
|
||||
<Pagination
|
||||
page={page}
|
||||
totalPages={Math.ceil(total / 10)}
|
||||
onPageChange={(page: any) => setPage(page)}
|
||||
limit={10}
|
||||
debounceRequest={500}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionsModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionsModal';
|
||||
|
|
@ -11,203 +11,245 @@ import WidgetPredefinedChart from '../WidgetPredefinedChart';
|
|||
import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart';
|
||||
import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper';
|
||||
import { debounce } from 'App/utils';
|
||||
import useIsMounted from 'App/hooks/useIsMounted'
|
||||
import useIsMounted from 'App/hooks/useIsMounted';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS, INSIGHTS } from 'App/constants/card';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
CLICKMAP,
|
||||
FUNNEL,
|
||||
ERRORS,
|
||||
PERFORMANCE,
|
||||
RESOURCE_MONITORING,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION
|
||||
} 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 ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard';
|
||||
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
|
||||
import SankeyChart from 'Shared/Insights/SankeyChart';
|
||||
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
isWidget?: boolean;
|
||||
isTemplate?: boolean;
|
||||
isPreview?: boolean;
|
||||
metric: any;
|
||||
isWidget?: boolean;
|
||||
isTemplate?: boolean;
|
||||
isPreview?: boolean;
|
||||
}
|
||||
|
||||
function WidgetChart(props: Props) {
|
||||
const { isWidget = false, metric, isTemplate } = props;
|
||||
const { dashboardStore, metricStore, sessionStore } = useStore();
|
||||
const _metric: any = metricStore.instance;
|
||||
const period = dashboardStore.period;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(true)
|
||||
const isOverviewWidget = metric.metricType === WEB_VITALS;
|
||||
const params = { density: isOverviewWidget ? 7 : 70 }
|
||||
const metricParams = { ...params }
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
const { isWidget = false, metric, isTemplate } = props;
|
||||
const { dashboardStore, metricStore, sessionStore } = useStore();
|
||||
const _metric: any = metricStore.instance;
|
||||
const period = dashboardStore.period;
|
||||
const drillDownPeriod = dashboardStore.drillDownPeriod;
|
||||
const drillDownFilter = dashboardStore.drillDownFilter;
|
||||
const colors = Styles.customMetricColors;
|
||||
const [loading, setLoading] = useState(true);
|
||||
const isOverviewWidget = metric.metricType === WEB_VITALS;
|
||||
const params = { density: isOverviewWidget ? 7 : 70 };
|
||||
const metricParams = { ...params };
|
||||
const prevMetricRef = useRef<any>();
|
||||
const isMounted = useIsMounted();
|
||||
const [data, setData] = useState<any>(metric.data);
|
||||
|
||||
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table';
|
||||
const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart';
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dashboardStore.resetDrillDownFilter();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
if (isTableWidget || isPieChart) { // get the filter of clicked row
|
||||
const periodTimestamps = drillDownPeriod.toTimestamps()
|
||||
drillDownFilter.merge({
|
||||
filters: event,
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
});
|
||||
} else { // get the filter of clicked chart point
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
|
||||
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const depsString = JSON.stringify(_metric.series);
|
||||
const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true)
|
||||
dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => {
|
||||
if (isMounted()) setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
const onChartClick = (event: any) => {
|
||||
if (event) {
|
||||
if (isTableWidget || isPieChart) { // get the filter of clicked row
|
||||
const periodTimestamps = drillDownPeriod.toTimestamps();
|
||||
drillDownFilter.merge({
|
||||
filters: event,
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp
|
||||
});
|
||||
} else { // get the filter of clicked chart point
|
||||
const payload = event.activePayload[0].payload;
|
||||
const timestamp = payload.timestamp;
|
||||
const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density);
|
||||
|
||||
drillDownFilter.merge({
|
||||
startTimestamp: periodTimestamps.startTimestamp,
|
||||
endTimestamp: periodTimestamps.endTimestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const depsString = JSON.stringify({
|
||||
..._metric.series, ..._metric.excludes, ..._metric.startPoint,
|
||||
hideExcess: _metric.hideExcess
|
||||
});
|
||||
const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => {
|
||||
if (!isMounted()) return;
|
||||
setLoading(true);
|
||||
dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => {
|
||||
if (isMounted()) setData(res);
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
|
||||
const loadPage = () => {
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return;
|
||||
}
|
||||
prevMetricRef.current = metric;
|
||||
const timestmaps = drillDownPeriod.toTimestamps();
|
||||
const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() };
|
||||
debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period);
|
||||
};
|
||||
useEffect(() => {
|
||||
_metric.updateKey('page', 1);
|
||||
loadPage();
|
||||
}, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue, metric.startType]);
|
||||
useEffect(loadPage, [_metric.page]);
|
||||
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType, metricOf } = metric;
|
||||
const metricWithData = { ...metric, data };
|
||||
|
||||
if (metricType === FUNNEL) {
|
||||
return <FunnelWidget metric={metric} data={data} isWidget={isWidget || isTemplate} />;
|
||||
}
|
||||
|
||||
const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []);
|
||||
const loadPage = () => {
|
||||
if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) {
|
||||
prevMetricRef.current = metric;
|
||||
return
|
||||
}
|
||||
prevMetricRef.current = metric;
|
||||
const timestmaps = drillDownPeriod.toTimestamps();
|
||||
const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() };
|
||||
debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period);
|
||||
if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) {
|
||||
const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric;
|
||||
if (isOverviewWidget) {
|
||||
return <CustomMetricOverviewChart data={data} />;
|
||||
}
|
||||
return <WidgetPredefinedChart isTemplate={isTemplate} metric={defaultMetric} data={data}
|
||||
predefinedKey={metric.metricOf} />;
|
||||
}
|
||||
useEffect(() => {
|
||||
_metric.updateKey('page', 1)
|
||||
loadPage();
|
||||
}, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue]);
|
||||
useEffect(loadPage, [_metric.page]);
|
||||
|
||||
|
||||
const renderChart = () => {
|
||||
const { metricType, viewType, metricOf } = metric;
|
||||
const metricWithData = { ...metric, data };
|
||||
|
||||
if (metricType === FUNNEL) {
|
||||
return <FunnelWidget metric={metric} data={data} isWidget={isWidget || isTemplate} />
|
||||
}
|
||||
|
||||
if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) {
|
||||
const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric
|
||||
if (isOverviewWidget) {
|
||||
return <CustomMetricOverviewChart data={data} />
|
||||
}
|
||||
return <WidgetPredefinedChart isTemplate={isTemplate} metric={defaultMetric} data={data} predefinedKey={metric.metricOf} />
|
||||
}
|
||||
|
||||
// TODO add USER_PATH, RETENTION, FEATUER_ADOPTION
|
||||
|
||||
if (metricType === TIMESERIES) {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
)
|
||||
} else if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (metricType === TABLE) {
|
||||
if (metricOf === FilterKey.SESSIONS) {
|
||||
return (
|
||||
<CustomMetricTableSessions
|
||||
metric={metric}
|
||||
data={data}
|
||||
isTemplate={isTemplate}
|
||||
isEdit={!isWidget && !isTemplate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (metricOf === FilterKey.ERRORS) {
|
||||
return (
|
||||
<CustomMetricTableErrors
|
||||
metric={metric}
|
||||
data={data}
|
||||
// isTemplate={isTemplate}
|
||||
isEdit={!isWidget && !isTemplate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (viewType === TABLE) {
|
||||
return (
|
||||
<CustomMetricTable
|
||||
metric={metric} data={data[0]}
|
||||
onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
)
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
// params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
if (metricType === CLICKMAP) {
|
||||
if (!props.isPreview) {
|
||||
return (
|
||||
<div style={{ height: '229px', overflow:'hidden', marginBottom: '10px'}}>
|
||||
<img src={metric.thumbnail} alt="clickmap thumbnail" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<ClickMapCard />
|
||||
)
|
||||
}
|
||||
|
||||
if (metricType === INSIGHTS) {
|
||||
return <InsightsCard data={data} />
|
||||
}
|
||||
|
||||
return <div>Unknown metric type</div>;
|
||||
if (metricType === TIMESERIES) {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'progress') {
|
||||
return (
|
||||
<CustomMetricPercentage
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
params={params}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Loader loading={loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
|
||||
<div style={{ minHeight: isOverviewWidget ? 100 : 240 }}>{renderChart()}</div>
|
||||
</Loader>
|
||||
);
|
||||
|
||||
if (metricType === TABLE) {
|
||||
if (metricOf === FilterKey.SESSIONS) {
|
||||
return (
|
||||
<CustomMetricTableSessions
|
||||
metric={metric}
|
||||
data={data}
|
||||
isTemplate={isTemplate}
|
||||
isEdit={!isWidget && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (metricOf === FilterKey.ERRORS) {
|
||||
return (
|
||||
<CustomMetricTableErrors
|
||||
metric={metric}
|
||||
data={data}
|
||||
// isTemplate={isTemplate}
|
||||
isEdit={!isWidget && !isTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewType === TABLE) {
|
||||
return (
|
||||
<CustomMetricTable
|
||||
metric={metric} data={data[0]}
|
||||
onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
<CustomMetricPieChart
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
colors={colors}
|
||||
// params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (metricType === CLICKMAP) {
|
||||
if (!props.isPreview) {
|
||||
return (
|
||||
<div style={{ height: '229px', overflow: 'hidden', marginBottom: '10px' }}>
|
||||
<img src={metric.thumbnail} alt='clickmap thumbnail' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ClickMapCard />
|
||||
);
|
||||
}
|
||||
|
||||
if (metricType === INSIGHTS) {
|
||||
return <InsightsCard data={data} />;
|
||||
}
|
||||
|
||||
if (metricType === USER_PATH && data && data.links) {
|
||||
return <SankeyChart
|
||||
height={props.isPreview ? 500 : 240}
|
||||
data={data}
|
||||
onChartClick={(filters: any) => {
|
||||
dashboardStore.drillDownFilter.merge({ filters });
|
||||
}} />;
|
||||
}
|
||||
|
||||
if (metricType === RETENTION) {
|
||||
if (viewType === 'trend') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
onClick={onChartClick}
|
||||
/>
|
||||
);
|
||||
} else if (viewType === 'cohort') {
|
||||
return (
|
||||
<CohortCard data={data[0]} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <div>Unknown metric type</div>;
|
||||
};
|
||||
return (
|
||||
<Loader loading={loading} style={{ height: `${isOverviewWidget ? 100 : 240}px` }}>
|
||||
<div style={{ minHeight: isOverviewWidget ? 100 : 240 }}>{renderChart()}</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(WidgetChart);
|
||||
|
|
|
|||
|
|
@ -19,10 +19,14 @@ import {
|
|||
PERFORMANCE,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION
|
||||
} from 'App/constants/card';
|
||||
import { eventKeys } from 'App/types/filter/newFilter';
|
||||
import { eventKeys, filtersMap } from 'App/types/filter/newFilter';
|
||||
import { renderClickmapThumbnail } from './renderMap';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import FilterItem from 'Shared/Filters/FilterItem';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
|
|
@ -33,8 +37,8 @@ function WidgetForm(props: Props) {
|
|||
const {
|
||||
history,
|
||||
match: {
|
||||
params: { siteId, dashboardId },
|
||||
},
|
||||
params: { siteId, dashboardId }
|
||||
}
|
||||
} = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const isSaving = metricStore.isSaving;
|
||||
|
|
@ -47,6 +51,8 @@ function WidgetForm(props: Props) {
|
|||
const isClickmap = metric.metricType === CLICKMAP;
|
||||
const isFunnel = metric.metricType === FUNNEL;
|
||||
const isInsights = metric.metricType === INSIGHTS;
|
||||
const isPathAnalysis = metric.metricType === USER_PATH;
|
||||
const isRetention = metric.metricType === RETENTION;
|
||||
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);
|
||||
|
|
@ -55,7 +61,7 @@ function WidgetForm(props: Props) {
|
|||
metric.metricType
|
||||
);
|
||||
|
||||
const excludeFilterKeys = isClickmap ? eventKeys : [];
|
||||
const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : [];
|
||||
|
||||
useEffect(() => {
|
||||
if (!!metric && !initialInstance) {
|
||||
|
|
@ -91,7 +97,7 @@ function WidgetForm(props: Props) {
|
|||
}
|
||||
}
|
||||
const savedMetric = await metricStore.save(metric);
|
||||
setInitialInstance(metric.toJson())
|
||||
setInitialInstance(metric.toJson());
|
||||
if (wasCreating) {
|
||||
if (parseInt(dashboardId, 10) > 0) {
|
||||
history.replace(
|
||||
|
|
@ -112,50 +118,83 @@ function WidgetForm(props: Props) {
|
|||
await confirm({
|
||||
header: 'Confirm',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this card?`,
|
||||
confirmation: `Are you sure you want to permanently delete this card?`
|
||||
})
|
||||
) {
|
||||
metricStore.delete(metric).then(props.onDelete);
|
||||
}
|
||||
};
|
||||
|
||||
const undoChnages = () => {
|
||||
const undoChanges = () => {
|
||||
const w = new Widget();
|
||||
metricStore.merge(w.fromJson(initialInstance), false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="form-group">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">Card showing</span>
|
||||
<div className='p-6'>
|
||||
<div className='form-group'>
|
||||
<div className='flex items-center'>
|
||||
<span className='mr-2'>Card showing</span>
|
||||
<MetricTypeDropdown onSelect={writeOption} />
|
||||
<MetricSubtypeDropdown onSelect={writeOption} />
|
||||
|
||||
{isPathAnalysis && (
|
||||
<>
|
||||
<span className='mx-3'></span>
|
||||
<Select
|
||||
name='startType'
|
||||
options={[
|
||||
{ value: 'start', label: 'With Start Point' },
|
||||
{ value: 'end', label: 'With End Point' }
|
||||
]}
|
||||
defaultValue={metric.startType}
|
||||
// value={metric.metricOf}
|
||||
onChange={writeOption}
|
||||
placeholder='All Issues'
|
||||
/>
|
||||
|
||||
<span className='mx-3'>showing</span>
|
||||
<Select
|
||||
name='metricValue'
|
||||
options={[
|
||||
{ value: 'location', label: 'Pages' },
|
||||
{ value: 'click', label: 'Clicks' },
|
||||
{ value: 'input', label: 'Input' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]}
|
||||
defaultValue={metric.metricValue}
|
||||
isMulti={true}
|
||||
// value={metric.metricValue}
|
||||
onChange={writeOption}
|
||||
placeholder='All Issues'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && (
|
||||
<>
|
||||
<span className="mx-3">issue type</span>
|
||||
<span className='mx-3'>issue type</span>
|
||||
<Select
|
||||
name="metricValue"
|
||||
name='metricValue'
|
||||
options={issueOptions}
|
||||
value={metric.metricValue}
|
||||
onChange={writeOption}
|
||||
isMulti={true}
|
||||
placeholder="All Issues"
|
||||
placeholder='All Issues'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{metric.metricType === INSIGHTS && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<span className='mx-3'>of</span>
|
||||
<Select
|
||||
name="metricValue"
|
||||
name='metricValue'
|
||||
options={issueCategories}
|
||||
value={metric.metricValue}
|
||||
onChange={writeOption}
|
||||
isMulti={true}
|
||||
placeholder="All Categories"
|
||||
placeholder='All Categories'
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -163,9 +202,9 @@ function WidgetForm(props: Props) {
|
|||
{metric.metricType === 'table' &&
|
||||
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
|
||||
<>
|
||||
<span className="mx-3">showing</span>
|
||||
<span className='mx-3'>showing</span>
|
||||
<Select
|
||||
name="metricFormat"
|
||||
name='metricFormat'
|
||||
options={[{ value: 'sessionCount', label: 'Session Count' }]}
|
||||
defaultValue={metric.metricFormat}
|
||||
onChange={writeOption}
|
||||
|
|
@ -175,23 +214,38 @@ function WidgetForm(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{isPathAnalysis && (
|
||||
<div className='form-group flex flex-col'>
|
||||
{metric.startType === 'start' ? 'Start Point' : 'End Point'}
|
||||
|
||||
<FilterItem
|
||||
hideDelete={true}
|
||||
filter={metric.startPoint}
|
||||
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
|
||||
onUpdate={(val) => {
|
||||
metric.updateStartPoint(val);
|
||||
}} onRemoveFilter={() => {
|
||||
}} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPredefined && (
|
||||
<div className="flex items-center my-6 justify-center">
|
||||
<Icon name="info-circle" size="18" color="gray-medium" />
|
||||
<div className="ml-2">
|
||||
<div className='flex items-center my-6 justify-center'>
|
||||
<Icon name='info-circle' size='18' color='gray-medium' />
|
||||
<div className='ml-2'>
|
||||
Filtering and drill-downs will be supported soon for this card type.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isPredefined && (
|
||||
<div className="form-group">
|
||||
<div className="flex items-center font-medium py-2">
|
||||
{`${isTable || isFunnel || isClickmap || isInsights ? 'Filter by' : 'Chart Series'}`}
|
||||
{!isTable && !isFunnel && !isClickmap && !isInsights && (
|
||||
<div className='form-group'>
|
||||
<div className='flex items-center font-medium py-2'>
|
||||
{`${isTable || isFunnel || isClickmap || isInsights || isPathAnalysis || isRetention ? 'Filter by' : 'Chart Series'}`}
|
||||
{!isTable && !isFunnel && !isClickmap && !isInsights && !isPathAnalysis && !isRetention && (
|
||||
<Button
|
||||
className="ml-2"
|
||||
variant="text-primary"
|
||||
className='ml-2'
|
||||
variant='text-primary'
|
||||
onClick={() => metric.addSeries()}
|
||||
disabled={!canAddSeries}
|
||||
>
|
||||
|
|
@ -202,14 +256,15 @@ function WidgetForm(props: Props) {
|
|||
|
||||
{metric.series.length > 0 &&
|
||||
metric.series
|
||||
.slice(0, isTable || isFunnel || isClickmap || isInsights ? 1 : metric.series.length)
|
||||
.slice(0, isTable || isFunnel || isClickmap || isInsights || isRetention ? 1 : metric.series.length)
|
||||
.map((series: any, index: number) => (
|
||||
<div className="mb-2" key={series.name}>
|
||||
<div className='mb-2' key={series.name}>
|
||||
<FilterSeries
|
||||
supportsEmpty={!isClickmap}
|
||||
canExclude={isPathAnalysis}
|
||||
supportsEmpty={!isClickmap && !isPathAnalysis}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
observeChanges={() => metric.updateKey('hasChanged', true)}
|
||||
hideHeader={isTable || isClickmap || isInsights}
|
||||
hideHeader={isTable || isClickmap || isInsights || isPathAnalysis || isFunnel}
|
||||
seriesIndex={index}
|
||||
series={series}
|
||||
onRemoveSeries={() => metric.removeSeries(index)}
|
||||
|
|
@ -225,30 +280,30 @@ function WidgetForm(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-groups flex items-center justify-between">
|
||||
<div className='form-groups flex items-center justify-between'>
|
||||
<Tooltip
|
||||
title="Cannot save funnel metric without at least 2 events"
|
||||
title='Cannot save funnel metric without at least 2 events'
|
||||
disabled={!cannotSaveFunnel}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Button variant="primary" onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
|
||||
<div className='flex items-center'>
|
||||
<Button variant='primary' onClick={onSave} disabled={isSaving || cannotSaveFunnel}>
|
||||
{metric.exists()
|
||||
? 'Update'
|
||||
: parseInt(dashboardId) > 0
|
||||
? 'Create & Add to Dashboard'
|
||||
: 'Create'}
|
||||
? 'Create & Add to Dashboard'
|
||||
: 'Create'}
|
||||
</Button>
|
||||
{metric.exists() && metric.hasChanged && (
|
||||
<Button onClick={undoChnages} variant="text" icon="arrow-counterclockwise" className="ml-2">
|
||||
<Button onClick={undoChanges} variant='text' icon='arrow-counterclockwise' className='ml-2'>
|
||||
Undo
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="flex items-center">
|
||||
<div className='flex items-center'>
|
||||
{metric.exists() && (
|
||||
<Button variant="text-primary" onClick={onDelete}>
|
||||
<Icon name="trash" size="14" className="mr-2" color="teal" />
|
||||
<Button variant='text-primary' onClick={onDelete}>
|
||||
<Icon name='trash' size='14' className='mr-2' color='teal' />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { DROPDOWN_OPTIONS, INSIGHTS, Option } from 'App/constants/card';
|
||||
import { DROPDOWN_OPTIONS, INSIGHTS, Option, USER_PATH } from 'App/constants/card';
|
||||
import Select from 'Shared/Select';
|
||||
import { components } from 'react-select';
|
||||
import CustomDropdownOption from 'Shared/CustomDropdownOption';
|
||||
|
|
@ -37,6 +37,7 @@ function MetricTypeDropdown(props: Props) {
|
|||
}
|
||||
setTimeout(() => onChange(type.value), 0);
|
||||
}
|
||||
setTimeout(() => onChange(USER_PATH), 0);
|
||||
}, []);
|
||||
|
||||
const onChange = (type: string) => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import { FilterKey } from 'Types/filter/filterType';
|
|||
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
|
||||
import ClickMapRagePicker from "Components/Dashboard/components/ClickMapRagePicker";
|
||||
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
import { CLICKMAP, TABLE, TIMESERIES } from "App/constants/card";
|
||||
import { CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH } from 'App/constants/card';
|
||||
import { Space, Switch } from 'antd';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -23,6 +24,7 @@ function WidgetPreview(props: Props) {
|
|||
const metric: any = metricStore.instance;
|
||||
const isTimeSeries = metric.metricType === TIMESERIES;
|
||||
const isTable = metric.metricType === TABLE;
|
||||
const isRetention = metric.metricType === RETENTION;
|
||||
const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS;
|
||||
|
||||
const changeViewType = (_, { name, value }: any) => {
|
||||
|
|
@ -39,6 +41,23 @@ function WidgetPreview(props: Props) {
|
|||
{props.name}
|
||||
</h2>
|
||||
<div className="flex items-center">
|
||||
{metric.metricType === USER_PATH && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
metric.update({ hideExcess: !metric.hideExcess });
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Switch
|
||||
checked={metric.hideExcess}
|
||||
size="small"
|
||||
/>
|
||||
<span className="mr-4 color-gray-medium">Hide Minor Paths</span>
|
||||
</Space>
|
||||
</a>
|
||||
)}
|
||||
{isTimeSeries && (
|
||||
<>
|
||||
<span className="mr-4 color-gray-medium">Visualization</span>
|
||||
|
|
@ -75,6 +94,25 @@ function WidgetPreview(props: Props) {
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isRetention && (
|
||||
<>
|
||||
<span className="mr-4 color-gray-medium">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary={true}
|
||||
size="small"
|
||||
onSelect={ changeViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={[
|
||||
{ value: 'trend', name: 'Trend', icon: 'graph-up-arrow' },
|
||||
{ value: 'cohort', name: 'Cohort', icon: 'dice-3' },
|
||||
]}
|
||||
disabledMessage="Chart view is not supported"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-4" />
|
||||
{metric.metricType === CLICKMAP ? (
|
||||
<ClickMapRagePicker />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ import {
|
|||
CLICKMAP,
|
||||
FUNNEL,
|
||||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION,
|
||||
} from 'App/constants/card';
|
||||
import CardIssues from '../CardIssues';
|
||||
import CardUserList from '../CardUserList/CardUserList';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
|
|
@ -124,6 +128,9 @@ function WidgetView(props: Props) {
|
|||
{widget.metricType === FUNNEL && <FunnelIssues />}
|
||||
</>
|
||||
)}
|
||||
|
||||
{widget.metricType === USER_PATH && <CardIssues />}
|
||||
{widget.metricType === RETENTION && <CardUserList />}
|
||||
</NoContent>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
|
@ -44,7 +45,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
isGridView = false,
|
||||
} = props;
|
||||
const widget: any = props.widget;
|
||||
const isTimeSeries = widget.metricType === 'timeseries';
|
||||
const isTimeSeries = widget.metricType === TIMESERIES;
|
||||
const isPredefined = widget.metricType === 'predefined';
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import React from 'react';
|
|||
import FilterOperator from '../FilterOperator';
|
||||
import FilterSelection from '../FilterSelection';
|
||||
import FilterValue from '../FilterValue';
|
||||
import {Button} from 'UI';
|
||||
import { Button } from 'UI';
|
||||
import FilterSource from '../FilterSource';
|
||||
import {FilterKey, FilterType} from 'App/types/filter/filterType';
|
||||
import { FilterKey, FilterType } from 'App/types/filter/filterType';
|
||||
import SubFilterItem from '../SubFilterItem';
|
||||
import {toJS} from "mobx";
|
||||
|
||||
interface Props {
|
||||
filterIndex: number;
|
||||
filterIndex?: number;
|
||||
filter: any; // event/filter
|
||||
onUpdate: (filter: any) => void;
|
||||
onRemoveFilter: () => void;
|
||||
|
|
@ -17,7 +17,10 @@ interface Props {
|
|||
saveRequestPayloads?: boolean;
|
||||
disableDelete?: boolean;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
readonly?: boolean;
|
||||
hideIndex?: boolean;
|
||||
hideDelete?: boolean;
|
||||
}
|
||||
|
||||
function FilterItem(props: Props) {
|
||||
|
|
@ -27,8 +30,10 @@ function FilterItem(props: Props) {
|
|||
filter,
|
||||
saveRequestPayloads,
|
||||
disableDelete = false,
|
||||
hideDelete = false,
|
||||
allowedFilterKeys = [],
|
||||
excludeFilterKeys = []
|
||||
} = props;
|
||||
, hideIndex = false } = props;
|
||||
const canShowValues = !(filter.operator === 'isAny' || filter.operator === 'onAny' || filter.operator === 'isUndefined');
|
||||
const isSubFilter = filter.type === FilterType.SUB_FILTERS;
|
||||
const replaceFilter = (filter: any) => {
|
||||
|
|
@ -62,7 +67,7 @@ function FilterItem(props: Props) {
|
|||
return (
|
||||
<div className="flex items-center hover:bg-active-blue -mx-5 px-5 py-2">
|
||||
<div className="flex items-start w-full">
|
||||
{!isFilter && (
|
||||
{!isFilter && !hideIndex && !!filterIndex && (
|
||||
<div
|
||||
className="mt-1 flex-shrink-0 border w-6 h-6 text-xs flex items-center justify-center rounded-full bg-gray-light-shade mr-2">
|
||||
<span>{filterIndex + 1}</span>
|
||||
|
|
@ -71,6 +76,7 @@ function FilterItem(props: Props) {
|
|||
<FilterSelection
|
||||
filter={filter}
|
||||
onFilterClick={replaceFilter}
|
||||
allowedFilterKeys={allowedFilterKeys}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
disabled={disableDelete || props.readonly}
|
||||
/>
|
||||
|
|
@ -140,7 +146,7 @@ function FilterItem(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
{props.readonly ? null :
|
||||
{(props.readonly || props.hideDelete) ? null :
|
||||
<div className="flex flex-shrink-0 self-start ml-auto">
|
||||
<Button
|
||||
disabled={disableDelete}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ function FilterList(props: Props) {
|
|||
{hasEvents && (
|
||||
<>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
<div className="text-sm color-gray-medium mr-auto">{filter.eventsHeader}</div>
|
||||
{!hideEventsOrder && (
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -6,19 +6,23 @@ import stl from './FilterModal.module.css';
|
|||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
function filterJson(jsonObj: Record<string, any>, excludeKeys: string[] = []): Record<string, any> {
|
||||
let filtered: Record<string, any> = {};
|
||||
|
||||
for (const key in jsonObj) {
|
||||
const arr = jsonObj[key].filter((i: any) => !excludeKeys.includes(i.key));
|
||||
if (arr.length) {
|
||||
filtered[key] = arr;
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
function filterJson(
|
||||
jsonObj: Record<string, any>,
|
||||
excludeKeys: string[] = [],
|
||||
allowedFilterKeys: string[] = []
|
||||
): Record<string, any> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(jsonObj).map(([key, value]) => {
|
||||
const arr = value.filter((i: { key: string }) => {
|
||||
if (excludeKeys.includes(i.key)) return false;
|
||||
return !(allowedFilterKeys.length > 0 && !allowedFilterKeys.includes(i.key));
|
||||
});
|
||||
return [key, arr];
|
||||
}).filter(([_, arr]) => arr.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const getMatchingEntries = (searchQuery: string, filters: Record<string, any>) => {
|
||||
const matchingCategories: string[] = [];
|
||||
const matchingFilters: Record<string, any> = {};
|
||||
|
|
@ -27,7 +31,7 @@ export const getMatchingEntries = (searchQuery: string, filters: Record<string,
|
|||
if (lowerCaseQuery.length === 0)
|
||||
return {
|
||||
matchingCategories: Object.keys(filters),
|
||||
matchingFilters: filters,
|
||||
matchingFilters: filters
|
||||
};
|
||||
|
||||
Object.keys(filters).forEach((name) => {
|
||||
|
|
@ -56,7 +60,9 @@ interface Props {
|
|||
fetchingFilterSearchList: boolean;
|
||||
searchQuery?: string;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
}
|
||||
|
||||
function FilterModal(props: Props) {
|
||||
const {
|
||||
filters,
|
||||
|
|
@ -66,18 +72,19 @@ function FilterModal(props: Props) {
|
|||
fetchingFilterSearchList,
|
||||
searchQuery = '',
|
||||
excludeFilterKeys = [],
|
||||
allowedFilterKeys = []
|
||||
} = props;
|
||||
const showSearchList = isMainSearch && searchQuery.length > 0;
|
||||
|
||||
const onFilterSearchClick = (filter: any) => {
|
||||
const _filter = {...filtersMap[filter.type]};
|
||||
const _filter = { ...filtersMap[filter.type] };
|
||||
_filter.value = [filter.value];
|
||||
onFilterClick(_filter);
|
||||
};
|
||||
|
||||
const { matchingCategories, matchingFilters } = getMatchingEntries(
|
||||
searchQuery,
|
||||
filterJson(filters, excludeFilterKeys)
|
||||
filterJson(filters, excludeFilterKeys, allowedFilterKeys)
|
||||
);
|
||||
|
||||
const isResultEmpty =
|
||||
|
|
@ -93,8 +100,8 @@ function FilterModal(props: Props) {
|
|||
>
|
||||
{matchingCategories.map((key) => {
|
||||
return (
|
||||
<div className="mb-6 flex flex-col gap-2 break-inside-avoid" key={key}>
|
||||
<div className="uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm">
|
||||
<div className='mb-6 flex flex-col gap-2 break-inside-avoid' key={key}>
|
||||
<div className='uppercase font-medium mb-1 color-gray-medium tracking-widest text-sm'>
|
||||
{key}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -108,8 +115,8 @@ function FilterModal(props: Props) {
|
|||
)}
|
||||
onClick={() => onFilterClick({ ...filter, value: [''] })}
|
||||
>
|
||||
<Icon name={filter.icon} size="16" />
|
||||
<span className="ml-2">{filter.label}</span>
|
||||
<Icon name={filter.icon} size='16' />
|
||||
<span className='ml-2'>{filter.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -118,12 +125,12 @@ function FilterModal(props: Props) {
|
|||
})}
|
||||
</div>
|
||||
{showSearchList && (
|
||||
<Loader size="small" loading={fetchingFilterSearchList}>
|
||||
<div className="-mx-6 px-6">
|
||||
<Loader size='small' loading={fetchingFilterSearchList}>
|
||||
<div className='-mx-6 px-6'>
|
||||
{isResultEmpty && !fetchingFilterSearchList ? (
|
||||
<div className="flex items-center flex-col">
|
||||
<div className='flex items-center flex-col'>
|
||||
<AnimatedSVG name={ICONS.NO_SEARCH_RESULTS} size={180} />
|
||||
<div className="color-gray-medium font-medium px-3"> No Suggestions Found </div>
|
||||
<div className='color-gray-medium font-medium px-3'> No Suggestions Found</div>
|
||||
</div>
|
||||
) : (
|
||||
Object.keys(filterSearchList).map((key, index) => {
|
||||
|
|
@ -131,7 +138,7 @@ function FilterModal(props: Props) {
|
|||
const option = filtersMap[key];
|
||||
return option ? (
|
||||
<div key={index} className={cn('mb-3')}>
|
||||
<div className="font-medium uppercase color-gray-medium mb-2">
|
||||
<div className='font-medium uppercase color-gray-medium mb-2'>
|
||||
{option.label}
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -144,8 +151,8 @@ function FilterModal(props: Props) {
|
|||
)}
|
||||
onClick={() => onFilterSearchClick({ type: key, value: f.value })}
|
||||
>
|
||||
<Icon className="mr-2" name={option.icon} size="16" />
|
||||
<div className="whitespace-nowrap text-ellipsis overflow-hidden">
|
||||
<Icon className='mr-2' name={option.icon} size='16' />
|
||||
<div className='whitespace-nowrap text-ellipsis overflow-hidden'>
|
||||
{f.value}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -174,6 +181,6 @@ export default connect((state: any, props: any) => {
|
|||
: state.getIn(['search', 'filterSearchList']),
|
||||
fetchingFilterSearchList: props.isLive
|
||||
? state.getIn(['liveSearch', 'fetchFilterSearch', 'loading'])
|
||||
: state.getIn(['search', 'fetchFilterSearch', 'loading']),
|
||||
: state.getIn(['search', 'fetchFilterSearch', 'loading'])
|
||||
};
|
||||
})(FilterModal);
|
||||
|
|
|
|||
|
|
@ -15,19 +15,21 @@ interface Props {
|
|||
onFilterClick: (filter: any) => void;
|
||||
children?: any;
|
||||
isLive?: boolean;
|
||||
excludeFilterKeys?: Array<string>
|
||||
disabled?: boolean
|
||||
excludeFilterKeys?: Array<string>;
|
||||
allowedFilterKeys?: Array<string>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function FilterSelection(props: Props) {
|
||||
const { filter, onFilterClick, children, excludeFilterKeys = [], disabled = false } = props;
|
||||
const { filter, onFilterClick, children, excludeFilterKeys = [], allowedFilterKeys = [], disabled = false } = props;
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative flex-shrink-0">
|
||||
<div className='relative flex-shrink-0'>
|
||||
<OutsideClickDetectingDiv
|
||||
className="relative"
|
||||
className='relative'
|
||||
onClickOutside={() =>
|
||||
setTimeout(function () {
|
||||
setTimeout(function() {
|
||||
setShowModal(false);
|
||||
}, 200)
|
||||
}
|
||||
|
|
@ -43,26 +45,27 @@ function FilterSelection(props: Props) {
|
|||
})
|
||||
) : (
|
||||
<div
|
||||
className={cn("rounded py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis hover:bg-gray-light-shade", { 'opacity-50 pointer-events-none' : disabled })}
|
||||
className={cn('rounded py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis hover:bg-gray-light-shade', { 'opacity-50 pointer-events-none': disabled })}
|
||||
style={{ width: '150px', height: '26px', border: 'solid thin #e9e9e9' }}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate"
|
||||
className='overflow-hidden whitespace-nowrap text-ellipsis mr-auto truncate'
|
||||
style={{ textOverflow: 'ellipsis' }}
|
||||
>
|
||||
{filter.label}
|
||||
</div>
|
||||
<Icon name="chevron-down" size="14" />
|
||||
<Icon name='chevron-down' size='14' />
|
||||
</div>
|
||||
)}
|
||||
</OutsideClickDetectingDiv>
|
||||
{showModal && (
|
||||
<div className="absolute left-0 border shadow rounded bg-white z-50">
|
||||
<div className='absolute left-0 border shadow rounded bg-white z-50'>
|
||||
<FilterModal
|
||||
isLive={isRoute(ASSIST_ROUTE, window.location.pathname)}
|
||||
onFilterClick={onFilterClick}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
allowedFilterKeys={allowedFilterKeys}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -74,7 +77,7 @@ export default connect(
|
|||
(state: any) => ({
|
||||
filterList: state.getIn(['search', 'filterList']),
|
||||
filterListLive: state.getIn(['search', 'filterListLive']),
|
||||
isLive: state.getIn(['sessions', 'activeTab']).type === 'live',
|
||||
isLive: state.getIn(['sessions', 'activeTab']).type === 'live'
|
||||
}),
|
||||
{}
|
||||
)(FilterSelection);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { Layer } from 'recharts';
|
||||
|
||||
function CustomLink(props: any) {
|
||||
const [fill, setFill] = React.useState('url(#linkGradient)');
|
||||
const { payload, sourceX, targetX, sourceY, targetY, sourceControlX, targetControlX, linkWidth, index, activeLink } =
|
||||
props;
|
||||
const activeSource = activeLink?.payload.source;
|
||||
const activeTarget = activeLink?.payload.target;
|
||||
const isActive = activeSource?.name === payload.source.name && activeTarget?.name === payload.target.name;
|
||||
|
||||
const onClick = () => {
|
||||
if (props.onClick) {
|
||||
props.onClick(props);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layer key={`CustomLink${index}`} onClick={onClick}>
|
||||
<path
|
||||
d={`
|
||||
M${sourceX},${sourceY + linkWidth / 2}
|
||||
C${sourceControlX},${sourceY + linkWidth / 2}
|
||||
${targetControlX},${targetY + linkWidth / 2}
|
||||
${targetX},${targetY + linkWidth / 2}
|
||||
L${targetX},${targetY - linkWidth / 2}
|
||||
C${targetControlX},${targetY - linkWidth / 2}
|
||||
${sourceControlX},${sourceY - linkWidth / 2}
|
||||
${sourceX},${sourceY - linkWidth / 2}
|
||||
Z
|
||||
`}
|
||||
fill={isActive ? 'rgba(57, 78, 255, 0.5)' : fill}
|
||||
strokeWidth='0'
|
||||
onMouseEnter={() => {
|
||||
setFill('rgba(57, 78, 255, 0.5)');
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setFill('url(#linkGradient)');
|
||||
}}
|
||||
/>
|
||||
</Layer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomLink;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { Layer, Rectangle } from 'recharts';
|
||||
import NodeButton from './NodeButton';
|
||||
import NodeDropdown from './NodeDropdown';
|
||||
|
||||
function CustomNode(props: any) {
|
||||
const { x, y, width, height, index, payload, containerWidth } = props;
|
||||
const isOut = x + width + 6 > containerWidth;
|
||||
|
||||
return (
|
||||
<Layer key={`CustomNode${index}`} style={{ cursor: 'pointer' }}>
|
||||
<Rectangle x={x} y={y} width={width} height={height} fill='#394EFF' fillOpacity='1' />
|
||||
|
||||
{/*<foreignObject*/}
|
||||
{/* x={isOut ? x - 6 : x + width + 5}*/}
|
||||
{/* y={0}*/}
|
||||
{/* height={48}*/}
|
||||
{/* style={{ width: '150px', padding: '2px' }}*/}
|
||||
{/*>*/}
|
||||
{/* <NodeDropdown payload={payload} />*/}
|
||||
{/*</foreignObject>*/}
|
||||
|
||||
<foreignObject
|
||||
x={isOut ? x - 6 : x + width + 5}
|
||||
y={y + 5}
|
||||
height='200'
|
||||
style={{ width: '150px' }}
|
||||
>
|
||||
<NodeButton payload={payload} />
|
||||
</foreignObject>
|
||||
</Layer>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomNode;
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import React from 'react';
|
||||
import { Icon } from 'UI';
|
||||
import { Popover } from 'antd';
|
||||
|
||||
interface Props {
|
||||
payload: any;
|
||||
}
|
||||
|
||||
function NodeButton(props: Props) {
|
||||
const { payload } = props;
|
||||
const [show, setShow] = React.useState(false);
|
||||
console.log('payload', payload, props)
|
||||
|
||||
const toggleMenu = (e: React.MouseEvent) => {
|
||||
setShow(!show);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Popover content={
|
||||
<div className='bg-white rounded w-fit mt-1 text-xs'>
|
||||
<div className='border-b py-1 px-2 flex items-center'>
|
||||
<div className='w-6 shrink-0'>
|
||||
<Icon name='link-45deg' size={18} />
|
||||
</div>
|
||||
<div className='ml-1'>{payload.name}</div>
|
||||
</div>
|
||||
<div className='border-b py-1 px-2 flex items-center'>
|
||||
<div className='w-6 shrink-0'>
|
||||
<Icon name='arrow-right-short' size={18} color='green' />
|
||||
</div>
|
||||
<div className='ml-1 font-medium'>Continuing {payload.value}</div>
|
||||
</div>
|
||||
<div className='border-b py-1 px-2 flex items-center'>
|
||||
<div className='w-6 shrink-0'>
|
||||
<Icon name='clock-history' size={16} />
|
||||
</div>
|
||||
<div className='ml-1 font-medium'>
|
||||
Average time from previous step <span>{payload.avgTimeFromPrevious}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} title={<div className='text-sm'>Title</div>}>
|
||||
<div
|
||||
className='copy-popover select-none rounded shadow'
|
||||
style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '3px 6px',
|
||||
width: 'fit-content',
|
||||
fontSize: '12px'
|
||||
}}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
{payload.name} <span style={{ fontWeight: 'bold' }}>{payload.value + '%'}</span>
|
||||
{/*{' '} <span style={{}}>{payload.avgTimeFromPrevious}</span>*/}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeButton;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
// import Select from 'Shared/Select';
|
||||
import { Dropdown, MenuProps, Select, Space } from 'antd';
|
||||
import { DownOutlined, SmileOutlined } from '@ant-design/icons';
|
||||
|
||||
interface Props {
|
||||
payload: any;
|
||||
}
|
||||
|
||||
function NodeDropdown(props: Props) {
|
||||
const items: MenuProps['items'] = [
|
||||
{
|
||||
key: '1',
|
||||
label: (
|
||||
<a target='_blank' rel='noopener noreferrer' href='https://www.antgroup.com'>
|
||||
1st menu item
|
||||
</a>
|
||||
)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Select style={{ width: 120 }} placeholder='Slect Event' dropdownStyle={{
|
||||
border: 'none'
|
||||
}}>
|
||||
<Select.Option value='jack'>Jack</Select.Option>
|
||||
<Select.Option value='lucy'>Lucy</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
export default NodeDropdown;
|
||||
|
|
@ -1,134 +1,100 @@
|
|||
import React from 'react';
|
||||
import { Sankey, Tooltip, Rectangle, Layer, ResponsiveContainer } from 'recharts';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Sankey, ResponsiveContainer } from 'recharts';
|
||||
import CustomLink from './CustomLink';
|
||||
import CustomNode from './CustomNode';
|
||||
import { NoContent } from 'UI';
|
||||
|
||||
type Node = {
|
||||
interface Node {
|
||||
name: string;
|
||||
eventType: string;
|
||||
avgTimeFromPrevious: number | null;
|
||||
}
|
||||
|
||||
type Link = {
|
||||
interface Link {
|
||||
eventType: string;
|
||||
value: number;
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface SankeyChartData {
|
||||
links: Link[];
|
||||
interface Data {
|
||||
nodes: Node[];
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: SankeyChartData;
|
||||
data: Data;
|
||||
nodePadding?: number;
|
||||
nodeWidth?: number;
|
||||
onChartClick?: (data: any) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
|
||||
function SankeyChart(props: Props) {
|
||||
const { data, nodePadding = 50, nodeWidth = 10 } = props;
|
||||
const { data, nodeWidth = 10, height = 240 } = props;
|
||||
const [activeLink, setActiveLink] = React.useState<any>(null);
|
||||
|
||||
data.nodes = data.nodes.map((node: any) => {
|
||||
return {
|
||||
...node,
|
||||
avgTimeFromPrevious: 200
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLink) return;
|
||||
const { source, target } = activeLink.payload;
|
||||
const filters = [];
|
||||
if (source) {
|
||||
filters.push({
|
||||
operator: 'is',
|
||||
type: source.eventType,
|
||||
value: [source.name],
|
||||
isEvent: true
|
||||
});
|
||||
}
|
||||
|
||||
if (target) {
|
||||
filters.push({
|
||||
operator: 'is',
|
||||
type: target.eventType,
|
||||
value: [target.name],
|
||||
isEvent: true
|
||||
});
|
||||
}
|
||||
|
||||
props.onChartClick?.(filters);
|
||||
}, [activeLink]);
|
||||
|
||||
return (
|
||||
<div className="rounded border shadow">
|
||||
<div className="text-lg p-3 border-b bg-gray-lightest">Sankey Chart</div>
|
||||
<div className="">
|
||||
<ResponsiveContainer height={500} width="100%">
|
||||
<Sankey
|
||||
width={960}
|
||||
height={500}
|
||||
data={data}
|
||||
// node={{ stroke: '#77c878', strokeWidth: 0 }}
|
||||
node={<CustomNodeComponent />}
|
||||
nodePadding={nodePadding}
|
||||
nodeWidth={nodeWidth}
|
||||
margin={{
|
||||
left: 10,
|
||||
right: 100,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
}}
|
||||
link={<CustomLinkComponent />}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={'linkGradient'}>
|
||||
<stop offset="0%" stopColor="rgba(0, 136, 254, 0.5)" />
|
||||
<stop offset="100%" stopColor="rgba(0, 197, 159, 0.3)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</Sankey>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
<NoContent show={!(data && data.nodes && data.nodes.length && data.links)}>
|
||||
<ResponsiveContainer height={height} width='100%'>
|
||||
<Sankey
|
||||
data={data}
|
||||
node={<CustomNode />}
|
||||
nodeWidth={nodeWidth}
|
||||
sort={false}
|
||||
// linkCurvature={0.5}
|
||||
// iterations={128}
|
||||
margin={{
|
||||
left: 0,
|
||||
right: 200,
|
||||
top: 0,
|
||||
bottom: 10
|
||||
}}
|
||||
link={<CustomLink onClick={(props: any) => setActiveLink(props)} activeLink={activeLink} />}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={'linkGradient'}>
|
||||
<stop offset='0%' stopColor='rgba(57, 78, 255, 0.2)' />
|
||||
<stop offset='100%' stopColor='rgba(57, 78, 255, 0.2)' />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</Sankey>
|
||||
</ResponsiveContainer>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SankeyChart;
|
||||
|
||||
const CustomTooltip = (props: any) => {
|
||||
return <div className="rounded bg-white border p-0 px-1 text-sm">test</div>;
|
||||
// if (active && payload && payload.length) {
|
||||
// return (
|
||||
// <div className="custom-tooltip">
|
||||
// <p className="label">{`${label} : ${payload[0].value}`}</p>
|
||||
// <p className="intro">{getIntroOfPage(label)}</p>
|
||||
// <p className="desc">Anything you want can be displayed here.</p>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
function CustomNodeComponent({ x, y, width, height, index, payload, containerWidth }: any) {
|
||||
const isOut = x + width + 6 > containerWidth;
|
||||
return (
|
||||
<Layer key={`CustomNode${index}`}>
|
||||
<Rectangle x={x} y={y} width={width} height={height} fill="#5192ca" fillOpacity="1" />
|
||||
<text
|
||||
textAnchor={isOut ? 'end' : 'start'}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2}
|
||||
fontSize="8"
|
||||
// stroke="#333"
|
||||
>
|
||||
{payload.name}
|
||||
</text>
|
||||
<text
|
||||
textAnchor={isOut ? 'end' : 'start'}
|
||||
x={isOut ? x - 6 : x + width + 6}
|
||||
y={y + height / 2 + 13}
|
||||
fontSize="12"
|
||||
// stroke="#333"
|
||||
// strokeOpacity="0.5"
|
||||
>
|
||||
{payload.value + 'k'}
|
||||
</text>
|
||||
</Layer>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomLinkComponent = (props: any) => {
|
||||
const [fill, setFill] = React.useState('url(#linkGradient)');
|
||||
const { sourceX, targetX, sourceY, targetY, sourceControlX, targetControlX, linkWidth, index } =
|
||||
props;
|
||||
return (
|
||||
<Layer key={`CustomLink${index}`}>
|
||||
<path
|
||||
d={`
|
||||
M${sourceX},${sourceY + linkWidth / 2}
|
||||
C${sourceControlX},${sourceY + linkWidth / 2}
|
||||
${targetControlX},${targetY + linkWidth / 2}
|
||||
${targetX},${targetY + linkWidth / 2}
|
||||
L${targetX},${targetY - linkWidth / 2}
|
||||
C${targetControlX},${targetY - linkWidth / 2}
|
||||
${sourceControlX},${sourceY - linkWidth / 2}
|
||||
${sourceX},${sourceY - linkWidth / 2}
|
||||
Z
|
||||
`}
|
||||
fill={fill}
|
||||
strokeWidth="0"
|
||||
onMouseEnter={() => {
|
||||
setFill('rgba(0, 136, 254, 0.5)');
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setFill('url(#linkGradient)');
|
||||
}}
|
||||
/>
|
||||
</Layer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function LiveSessionList(props: Props) {
|
|||
<div className="flex mb-6 justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-2xl capitalize mr-2">
|
||||
<span>Live Sessions</span>
|
||||
<span>Cobrowse</span>
|
||||
{/* <span className="ml-2 font-normal color-gray-medium">{numberWithCommas(total)}</span> */}
|
||||
</h3>
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ function LiveSessionList(props: Props) {
|
|||
subtext={
|
||||
<div className="text-center flex justify-center items-center flex-col">
|
||||
<span>
|
||||
Support users with live sessions, co-browsing, and video calls.
|
||||
Support users with live sessions, cobrowsing, and video calls.
|
||||
<a
|
||||
target="_blank"
|
||||
className="link ml-1"
|
||||
|
|
|
|||
|
|
@ -2,177 +2,199 @@ import React from 'react';
|
|||
import Select, { components, DropdownIndicatorProps } from 'react-select';
|
||||
import { Icon } from 'UI';
|
||||
import colors from 'App/theme/colors';
|
||||
|
||||
const { ValueContainer } = components;
|
||||
|
||||
type ValueObject = {
|
||||
value: string | number,
|
||||
label: React.ReactNode,
|
||||
value: string | number,
|
||||
label: React.ReactNode,
|
||||
}
|
||||
|
||||
interface Props<Value extends ValueObject> {
|
||||
options: Value[];
|
||||
isSearchable?: boolean;
|
||||
defaultValue?: string | number;
|
||||
plain?: boolean;
|
||||
components?: any;
|
||||
styles?: Record<string, any>;
|
||||
controlStyle?: Record<string, any>;
|
||||
onChange: (newValue: { name: string, value: Value }) => void;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
[x:string]: any;
|
||||
options: Value[];
|
||||
isSearchable?: boolean;
|
||||
defaultValue?: string | number;
|
||||
plain?: boolean;
|
||||
components?: any;
|
||||
styles?: Record<string, any>;
|
||||
controlStyle?: Record<string, any>;
|
||||
onChange: (newValue: { name: string, value: Value }) => void;
|
||||
name?: string;
|
||||
placeholder?: string;
|
||||
|
||||
[x: string]: any;
|
||||
}
|
||||
|
||||
export default function<Value extends ValueObject>({ placeholder='Select', name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', controlStyle = {}, ...rest }: Props<Value>) {
|
||||
const defaultSelected = defaultValue ? (options.find(o => o.value === defaultValue) || options[0]): null;
|
||||
const customStyles = {
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.3s',
|
||||
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
|
||||
color: state.isFocused ? colors.teal : 'black',
|
||||
fontSize: '14px',
|
||||
'&:hover': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue'],
|
||||
},
|
||||
'&:focus': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue'],
|
||||
}
|
||||
}),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
top: 31,
|
||||
borderRadius: '3px',
|
||||
right: right ? 0 : undefined,
|
||||
border: `1px solid ${colors['gray-light']}`,
|
||||
// borderRadius: '3px',
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
|
||||
position: 'absolute',
|
||||
minWidth: 'fit-content',
|
||||
// zIndex: 99,
|
||||
overflow: 'hidden',
|
||||
zIndex: 100,
|
||||
...(right && { right: 0 })
|
||||
}),
|
||||
menuList: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
padding: 0,
|
||||
}),
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin #ddd',
|
||||
cursor: 'pointer',
|
||||
minHeight: '36px',
|
||||
transition: 'all 0.5s',
|
||||
['&:hover']: {
|
||||
backgroundColor: colors['gray-lightest'],
|
||||
transition: 'all 0.2s ease-in-out'
|
||||
},
|
||||
...controlStyle,
|
||||
}
|
||||
if (plain) {
|
||||
obj['backgroundColor'] = 'transparent';
|
||||
obj['border'] = '1px solid transparent'
|
||||
obj['backgroundColor'] = 'transparent'
|
||||
obj['&:hover'] = {
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: colors['gray-light'],
|
||||
transition: 'all 0.2s ease-in-out'
|
||||
}
|
||||
obj['&:focus'] = {
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
obj['&:active'] = {
|
||||
borderColor: 'transparent'
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
export default function <Value extends ValueObject>({
|
||||
placeholder = 'Select',
|
||||
name = '',
|
||||
onChange,
|
||||
right = false,
|
||||
plain = false,
|
||||
options,
|
||||
isSearchable = false,
|
||||
components = {},
|
||||
styles = {},
|
||||
defaultValue = '',
|
||||
controlStyle = {},
|
||||
...rest
|
||||
}: Props<Value>) {
|
||||
|
||||
const defaultSelected = Array.isArray(defaultValue) ?
|
||||
defaultValue.map((value) => options.find((option) => option.value === value)) :
|
||||
options.find((option) => option.value === defaultValue
|
||||
) || null;
|
||||
if (Array.isArray(defaultSelected) && defaultSelected.length === 0) {
|
||||
console.log('defaultSelected', defaultSelected);
|
||||
}
|
||||
const customStyles = {
|
||||
option: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'all 0.3s',
|
||||
backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent',
|
||||
color: state.isFocused ? colors.teal : 'black',
|
||||
fontSize: '14px',
|
||||
'&:hover': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue']
|
||||
},
|
||||
'&:focus': {
|
||||
transition: 'all 0.2s',
|
||||
backgroundColor: colors['active-blue']
|
||||
}
|
||||
}),
|
||||
menu: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
top: 31,
|
||||
borderRadius: '3px',
|
||||
right: right ? 0 : undefined,
|
||||
border: `1px solid ${colors['gray-light']}`,
|
||||
// borderRadius: '3px',
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)',
|
||||
position: 'absolute',
|
||||
minWidth: 'fit-content',
|
||||
// zIndex: 99,
|
||||
overflow: 'hidden',
|
||||
zIndex: 100,
|
||||
...(right && { right: 0 })
|
||||
}),
|
||||
menuList: (provided: any, state: any) => ({
|
||||
...provided,
|
||||
padding: 0
|
||||
}),
|
||||
control: (provided: any) => {
|
||||
const obj = {
|
||||
...provided,
|
||||
border: 'solid thin #ddd',
|
||||
cursor: 'pointer',
|
||||
minHeight: '36px',
|
||||
transition: 'all 0.5s',
|
||||
['&:hover']: {
|
||||
backgroundColor: colors['gray-lightest'],
|
||||
transition: 'all 0.2s ease-in-out'
|
||||
},
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
...provided,
|
||||
maxHeight: '34px',
|
||||
padding: 0,
|
||||
}),
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
paddingRight: '0px',
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any; }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
...controlStyle
|
||||
};
|
||||
if (plain) {
|
||||
obj['backgroundColor'] = 'transparent';
|
||||
obj['border'] = '1px solid transparent';
|
||||
obj['backgroundColor'] = 'transparent';
|
||||
obj['&:hover'] = {
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: colors['gray-light'],
|
||||
transition: 'all 0.2s ease-in-out'
|
||||
};
|
||||
obj['&:focus'] = {
|
||||
borderColor: 'transparent'
|
||||
};
|
||||
obj['&:active'] = {
|
||||
borderColor: 'transparent'
|
||||
};
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
indicatorsContainer: (provided: any) => ({
|
||||
...provided,
|
||||
maxHeight: '34px',
|
||||
padding: 0
|
||||
}),
|
||||
valueContainer: (provided: any) => ({
|
||||
...provided,
|
||||
paddingRight: '0px'
|
||||
}),
|
||||
singleValue: (provided: any, state: { isDisabled: any; }) => {
|
||||
const opacity = state.isDisabled ? 0.5 : 1;
|
||||
const transition = 'opacity 300ms';
|
||||
|
||||
return { ...provided, opacity, transition, fontWeight: '500' };
|
||||
},
|
||||
input: (provided: any) => ({
|
||||
...provided,
|
||||
'& input:focus': {
|
||||
border: 'none !important',
|
||||
}
|
||||
}),
|
||||
noOptionsMessage: (provided: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap !important',
|
||||
// minWidth: 'fit-content',
|
||||
}),
|
||||
}
|
||||
return { ...provided, opacity, transition, fontWeight: '500' };
|
||||
},
|
||||
input: (provided: any) => ({
|
||||
...provided,
|
||||
'& input:focus': {
|
||||
border: 'none !important'
|
||||
}
|
||||
}),
|
||||
noOptionsMessage: (provided: any) => ({
|
||||
...provided,
|
||||
whiteSpace: 'nowrap !important'
|
||||
// minWidth: 'fit-content',
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
defaultValue={defaultSelected}
|
||||
components={{
|
||||
IndicatorSeparator: () => null,
|
||||
DropdownIndicator,
|
||||
ValueContainer: CustomValueContainer,
|
||||
...components,
|
||||
}}
|
||||
onChange={(value) => onChange({ name, value: value })}
|
||||
styles={{ ...customStyles, ...styles }}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: '#394EFF',
|
||||
}
|
||||
})}
|
||||
blurInputOnSelect={true}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
defaultValue={defaultSelected}
|
||||
components={{
|
||||
IndicatorSeparator: () => null,
|
||||
DropdownIndicator,
|
||||
ValueContainer: CustomValueContainer,
|
||||
...components
|
||||
}}
|
||||
onChange={(value) => onChange({ name, value: value })}
|
||||
styles={{ ...customStyles, ...styles }}
|
||||
theme={(theme) => ({
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
primary: '#394EFF'
|
||||
}
|
||||
})}
|
||||
blurInputOnSelect={true}
|
||||
placeholder={placeholder}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const DropdownIndicator = (
|
||||
props: DropdownIndicatorProps<true>
|
||||
) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<Icon name="chevron-down" size="16" />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
props: DropdownIndicatorProps<true>
|
||||
) => {
|
||||
return (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<Icon name='chevron-down' size='16' />
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomValueContainer = ({ children, ...rest }: any) => {
|
||||
const selectedCount = rest.getValue().length
|
||||
const conditional = (selectedCount < 3)
|
||||
const selectedCount = rest.getValue().length;
|
||||
const conditional = (selectedCount < 3);
|
||||
|
||||
let firstChild: any = []
|
||||
let firstChild: any = [];
|
||||
|
||||
if (!conditional) {
|
||||
firstChild = [children[0].shift(), children[1]]
|
||||
}
|
||||
|
||||
return (
|
||||
<ValueContainer {...rest}>
|
||||
{conditional ? children : firstChild}
|
||||
{!conditional && ` and ${selectedCount - 1} others`}
|
||||
</ValueContainer>
|
||||
)
|
||||
if (!conditional) {
|
||||
firstChild = [children[0].shift(), children[1]];
|
||||
}
|
||||
|
||||
return (
|
||||
<ValueContainer {...rest}>
|
||||
{conditional ? children : firstChild}
|
||||
{!conditional && ` and ${selectedCount - 1} others`}
|
||||
</ValueContainer>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -20,7 +20,7 @@ export const ERRORS = 'errors';
|
|||
export const PERFORMANCE = 'performance';
|
||||
export const RESOURCE_MONITORING = 'resources';
|
||||
export const WEB_VITALS = 'webVitals';
|
||||
export const USER_PATH = 'userPath';
|
||||
export const USER_PATH = 'pathAnalysis';
|
||||
export const RETENTION = 'retention';
|
||||
export const FEATURE_ADOPTION = 'featureAdoption';
|
||||
export const INSIGHTS = 'insights';
|
||||
|
|
@ -185,18 +185,19 @@ export const TYPES: CardType[] = [
|
|||
{ title: 'Captured Sessions', slug: FilterKey.COUNT_SESSIONS, description: '' },
|
||||
],
|
||||
},
|
||||
// {
|
||||
// title: 'Path Analysis',
|
||||
// icon: 'signpost-split',
|
||||
// description: 'See where users are flowing and explore their journeys.',
|
||||
// slug: USER_PATH,
|
||||
// },
|
||||
// {
|
||||
// title: 'Retention',
|
||||
// icon: 'arrow-repeat',
|
||||
// description: 'Get an understanding of how many users are returning.',
|
||||
// slug: RETENTION,
|
||||
// },
|
||||
{
|
||||
title: 'Path Analysis',
|
||||
icon: 'signpost-split',
|
||||
description: 'See where users are flowing and explore their journeys.',
|
||||
slug: USER_PATH,
|
||||
},
|
||||
{
|
||||
title: 'Retention',
|
||||
icon: 'arrow-repeat',
|
||||
description: 'Get an understanding of how many users are returning.',
|
||||
slug: RETENTION,
|
||||
disabled: true,
|
||||
},
|
||||
// {
|
||||
// title: 'Feature Adoption',
|
||||
// icon: 'card-checklist',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const customTheme: ThemeConfig = {
|
|||
bodyBg: colors['gray-lightest'],
|
||||
headerBg: colors['gray-lightest'],
|
||||
siderBg: colors['gray-lightest'],
|
||||
|
||||
},
|
||||
Menu: {
|
||||
colorPrimary: colors.teal,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ function SideMenu(props: Props) {
|
|||
<>
|
||||
<Menu
|
||||
defaultSelectedKeys={['1']} mode='inline' onClick={handleClick}
|
||||
style={{ border: 'none', marginTop: '8px' }}
|
||||
style={{ marginTop: '8px', border: 'none' }}
|
||||
inlineCollapsed={isCollapsed}
|
||||
>
|
||||
{isPreferencesActive && (
|
||||
|
|
@ -151,7 +151,7 @@ function SideMenu(props: Props) {
|
|||
{index > 0 && <Divider style={{ margin: '6px 0' }} />}
|
||||
<Menu.ItemGroup
|
||||
key={category.key}
|
||||
title={category.title}
|
||||
title={<div style={{ paddingLeft: isCollapsed ? '' : '6px' }} className={cn({ 'text-center' : isCollapsed })}>{category.title}</div>}
|
||||
>
|
||||
{category.items.filter((item: any) => !item.hidden).map((item: any) => {
|
||||
const isActive = isMenuItemActive(item.key);
|
||||
|
|
@ -160,6 +160,7 @@ function SideMenu(props: Props) {
|
|||
key={item.key}
|
||||
title={<Text className={cn('ml-5 !rounded')}>{item.label}</Text>}
|
||||
icon={<SVG name={item.icon} size={16} />}>
|
||||
{/*style={{ paddingLeft: '30px' }}*/}
|
||||
{item.children.map((child: any) => <Menu.Item
|
||||
className={cn('ml-8', { 'ant-menu-item-selected !bg-active-dark-blue': isMenuItemActive(child.key) })}
|
||||
key={child.key}>{child.label}</Menu.Item>)}
|
||||
|
|
@ -168,7 +169,7 @@ function SideMenu(props: Props) {
|
|||
<Menu.Item
|
||||
key={item.key}
|
||||
icon={<Icon name={item.icon} size={16} color={isActive ? 'teal' : ''} />}
|
||||
// style={{ color: '#333', height: '32px' }}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
className={cn('!rounded')}
|
||||
itemIcon={item.leading ?
|
||||
<Icon name={item.leading} size={16} color={isActive ? 'teal' : ''} /> : null}>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function TopHeader() {
|
|||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
padding: '0 15px',
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: '60px'
|
||||
|
|
@ -30,6 +30,7 @@ function TopHeader() {
|
|||
onClick={() => {
|
||||
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
|
||||
}}
|
||||
style={{ paddingTop: '4px' }}
|
||||
className='cursor-pointer'
|
||||
>
|
||||
<Icon name={settingsStore.menuCollapsed ? 'side_menu_closed' : 'side_menu_open'} size={20} />
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export const categories: Category[] = [
|
|||
title: 'Assist',
|
||||
key: 'assist',
|
||||
items: [
|
||||
{ label: 'Live Sessions', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
{ label: 'Cobrowse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' },
|
||||
{ label: 'Recordings', key: MENU.RECORDINGS, icon: 'record-btn', isEnterprise: true }
|
||||
]
|
||||
},
|
||||
|
|
@ -112,7 +112,7 @@ export const preferences: Category[] = [
|
|||
{ label: 'Integrations', key: PREFERENCES_MENU.INTEGRATIONS, icon: 'plug' },
|
||||
{ label: 'Metadata', key: PREFERENCES_MENU.METADATA, icon: 'tags' },
|
||||
{ label: 'Webhooks', key: PREFERENCES_MENU.WEBHOOKS, icon: 'link-45deg' },
|
||||
{ label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'people' },
|
||||
{ label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'puzzle' },
|
||||
{ label: 'Projects', key: PREFERENCES_MENU.PROJECTS, icon: 'folder2' },
|
||||
{
|
||||
label: 'Roles & Access',
|
||||
|
|
|
|||
|
|
@ -423,21 +423,19 @@ export default class DashboardStore {
|
|||
params['limit'] = metric.limit;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.pendingRequests += 1;
|
||||
return metricService
|
||||
.getMetricChartData(metric, params, isWidget)
|
||||
.then((data: any) => {
|
||||
resolve(metric.setData(data, period));
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(err);
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.pendingRequests = this.pendingRequests - 1;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await metricService.getMetricChartData(metric, params, isWidget);
|
||||
resolve(metric.setData(data, period));
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
this.pendingRequests -= 1;
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import {
|
|||
PERFORMANCE,
|
||||
WEB_VITALS,
|
||||
INSIGHTS,
|
||||
CLICKMAP
|
||||
CLICKMAP,
|
||||
USER_PATH,
|
||||
RETENTION
|
||||
} from 'App/constants/card';
|
||||
import { clickmapFilter } from 'App/types/filter/newFilter';
|
||||
import { getRE } from 'App/utils';
|
||||
|
|
@ -109,8 +111,14 @@ export default class MetricStore {
|
|||
this.changeType(type);
|
||||
}
|
||||
|
||||
if (obj.hasOwnProperty('metricOf') && obj.metricOf !== this.instance.metricOf && (obj.metricOf === 'sessions' || obj.metricOf === 'jsErrors')) {
|
||||
obj.viewType = 'table'
|
||||
if (obj.hasOwnProperty('metricOf') && obj.metricOf !== this.instance.metricOf) {
|
||||
if (obj.metricOf === 'sessions' || obj.metricOf === 'jsErrors') {
|
||||
obj.viewType = 'table'
|
||||
}
|
||||
|
||||
if (this.instance.metricType === USER_PATH) {
|
||||
this.instance.series[0].filter.eventsHeader = obj.metricOf === 'start-point' ? 'START POINT' : 'END POINT';
|
||||
}
|
||||
}
|
||||
|
||||
// handle metricValue change
|
||||
|
|
@ -140,6 +148,9 @@ export default class MetricStore {
|
|||
if (value === TIMESERIES) {
|
||||
obj['viewType'] = 'lineChart';
|
||||
}
|
||||
if (value === RETENTION) {
|
||||
obj['viewType'] = 'cohort';
|
||||
}
|
||||
if (
|
||||
value === ERRORS ||
|
||||
value === RESOURCE_MONITORING ||
|
||||
|
|
@ -156,11 +167,21 @@ export default class MetricStore {
|
|||
obj.series[0].filter.eventsOrderSupport = ['then']
|
||||
}
|
||||
|
||||
if (value === USER_PATH) {
|
||||
obj.series[0].filter.eventsHeader = 'START POINT';
|
||||
} else {
|
||||
obj.series[0].filter.eventsHeader = 'EVENTS'
|
||||
}
|
||||
|
||||
if (value === INSIGHTS) {
|
||||
obj['metricOf'] = 'issueCategories';
|
||||
obj['viewType'] = 'list';
|
||||
}
|
||||
|
||||
if (value === USER_PATH) {
|
||||
// obj['startType'] = 'start';
|
||||
}
|
||||
|
||||
if (value === CLICKMAP) {
|
||||
obj.series = obj.series.slice(0, 1);
|
||||
if (this.instance.metricType !== CLICKMAP) {
|
||||
|
|
@ -174,6 +195,8 @@ export default class MetricStore {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('obj', obj);
|
||||
this.instance.update(obj);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import { makeAutoObservable, runInAction, observable, action } from "mobx"
|
||||
import FilterItem from "./filterItem"
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
|
||||
export default class Filter {
|
||||
public static get ID_KEY():string { return "filterId" }
|
||||
filterId: string = ''
|
||||
name: string = ''
|
||||
filters: FilterItem[] = []
|
||||
excludes: FilterItem[] = []
|
||||
eventsOrder: string = 'then'
|
||||
eventsOrderSupport: string[] = ['then', 'or', 'and']
|
||||
startTimestamp: number = 0
|
||||
endTimestamp: number = 0
|
||||
eventsHeader: string = "EVENTS"
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
|
|
@ -22,6 +25,7 @@ export default class Filter {
|
|||
removeFilter: action,
|
||||
updateKey: action,
|
||||
merge: action,
|
||||
addExcludeFilter: action,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +77,10 @@ export default class Filter {
|
|||
return json
|
||||
}
|
||||
|
||||
createFilterBykey(key: string) {
|
||||
return filtersMap[key] ? new FilterItem(filtersMap[key]) : new FilterItem()
|
||||
}
|
||||
|
||||
toJson() {
|
||||
const json = {
|
||||
name: this.name,
|
||||
|
|
@ -81,4 +89,16 @@ export default class Filter {
|
|||
}
|
||||
return json
|
||||
}
|
||||
|
||||
addExcludeFilter(filter: FilterItem) {
|
||||
this.excludes.push(filter)
|
||||
}
|
||||
|
||||
updateExcludeFilter(index: number, filter: FilterItem) {
|
||||
this.excludes[index] = new FilterItem(filter)
|
||||
}
|
||||
|
||||
removeExcludeFilter(index: number) {
|
||||
this.excludes.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
frontend/app/mstore/types/issue.ts
Normal file
41
frontend/app/mstore/types/issue.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const ISSUE_MAP: any = {
|
||||
dead_click: { name: 'Dead Click', icon: 'funnel/emoji-dizzy-fill', color: '#9C001F' },
|
||||
rage_click: { name: 'Rage Click', icon: 'funnel/emoji-angry-fill', color: '#CC0000' },
|
||||
click_rage: { name: 'Click Rage', icon: 'funnel/emoji-angry-fill', color: '#CC0000' },
|
||||
excessive_scrolling: { name: 'Excessive Scrolling', icon: 'funnel/mouse', color: '#D3545F' },
|
||||
bad_request: { name: 'Bad Request', icon: 'funnel/patch-exclamation-fill', color: '#D70072' },
|
||||
missing_resource: { name: 'Missing Resource', icon: 'funnel/image-fill', color: '#B89C50' },
|
||||
memory: { name: 'Memory', icon: 'funnel/cpu-fill', color: '#8A5A83' },
|
||||
cpu: { name: 'CPU', icon: 'funnel/hdd-fill', color: '#8A5A83' },
|
||||
slow_resource: { name: 'Slow Resource', icon: 'funnel/hourglass-top', color: '#8B006D' },
|
||||
slow_page_load: { name: 'Slow Page Load', icon: 'funnel/hourglass-top', color: '#8B006D' },
|
||||
custom_event_error: { name: 'Custom Event Error', icon: 'funnel/exclamation-circle-fill', color: '#BF6C00' },
|
||||
custom: { name: 'Custom', icon: 'funnel/exclamation-circle-fill', color: '#BF6C00' },
|
||||
crash: { name: 'Crash', icon: 'funnel/file-x', color: '#BF2D00' },
|
||||
js_exception: { name: 'JS Exception', icon: 'funnel/exclamation-circle', color: '#BF2D00' }
|
||||
};
|
||||
|
||||
export default class Issue {
|
||||
type: string = '';
|
||||
name: string = '';
|
||||
sessionCount: number = 0;
|
||||
icon: string = '';
|
||||
source: string = '';
|
||||
|
||||
constructor() {
|
||||
this.type = '';
|
||||
this.name = '';
|
||||
this.sessionCount = 0;
|
||||
this.icon = '';
|
||||
this.source = '';
|
||||
}
|
||||
|
||||
fromJSON(json: any) {
|
||||
this.type = json.name;
|
||||
this.name = ISSUE_MAP[json.name].name || '';
|
||||
this.sessionCount = json.sessionCount;
|
||||
this.icon = ISSUE_MAP[json.name].icon || '';
|
||||
this.source = json.source;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,16 @@ import Funnelissue from 'App/mstore/types/funnelIssue';
|
|||
import { issueOptions, issueCategories, issueCategoriesMap } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import Period, { LAST_24_HOURS } from 'Types/app/period';
|
||||
import Funnel from "../types/funnel";
|
||||
import Funnel from '../types/funnel';
|
||||
import { metricService } from 'App/services';
|
||||
import { FUNNEL, INSIGHTS, TABLE, WEB_VITALS } from 'App/constants/card';
|
||||
import { FUNNEL, INSIGHTS, TABLE, USER_PATH, WEB_VITALS } from 'App/constants/card';
|
||||
import Error from '../types/error';
|
||||
import { getChartFormatter } from 'Types/dashboard/helper';
|
||||
import FilterItem from './filterItem';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import Issue from '../types/issue';
|
||||
|
||||
export class InishtIssue {
|
||||
export class InsightIssue {
|
||||
icon: string;
|
||||
iconColor: string;
|
||||
change: number;
|
||||
|
|
@ -46,10 +49,20 @@ export class InishtIssue {
|
|||
}
|
||||
}
|
||||
|
||||
function cleanFilter(filter: any) {
|
||||
delete filter['operatorOptions'];
|
||||
delete filter['placeholder'];
|
||||
delete filter['category'];
|
||||
delete filter['label'];
|
||||
delete filter['icon'];
|
||||
delete filter['key'];
|
||||
}
|
||||
|
||||
export default class Widget {
|
||||
public static get ID_KEY(): string {
|
||||
return 'metricId';
|
||||
}
|
||||
|
||||
metricId: any = undefined;
|
||||
widgetId: any = undefined;
|
||||
category?: string = undefined;
|
||||
|
|
@ -63,7 +76,7 @@ export default class Widget {
|
|||
sessions: [] = [];
|
||||
isPublic: boolean = true;
|
||||
owner: string = '';
|
||||
lastModified: number = new Date().getTime();
|
||||
lastModified: DateTime | null = new Date().getTime();
|
||||
dashboards: any[] = [];
|
||||
dashboardIds: any[] = [];
|
||||
config: any = {};
|
||||
|
|
@ -71,6 +84,11 @@ export default class Widget {
|
|||
limit: number = 5;
|
||||
thumbnail?: string;
|
||||
params: any = { density: 70 };
|
||||
startType: string = 'start';
|
||||
// startPoint: FilterItem = filtersMap[FilterKey.LOCATION];
|
||||
startPoint: FilterItem = new FilterItem(filtersMap[FilterKey.LOCATION]);
|
||||
excludes: FilterItem[] = [];
|
||||
hideExcess?: boolean = false;
|
||||
|
||||
period: Record<string, any> = Period({ rangeName: LAST_24_HOURS }); // temp value in detail view
|
||||
hasChanged: boolean = false;
|
||||
|
|
@ -83,7 +101,7 @@ export default class Widget {
|
|||
chart: [],
|
||||
namesMap: {},
|
||||
avg: 0,
|
||||
percentiles: [],
|
||||
percentiles: []
|
||||
};
|
||||
isLoading: boolean = false;
|
||||
isValid: boolean = false;
|
||||
|
|
@ -156,13 +174,13 @@ export default class Widget {
|
|||
config: {
|
||||
position: this.position,
|
||||
col: this.config.col,
|
||||
row: this.config.row,
|
||||
},
|
||||
row: this.config.row
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const data: any = {
|
||||
metricId: this.metricId,
|
||||
widgetId: this.widgetId,
|
||||
metricOf: this.metricOf,
|
||||
|
|
@ -184,10 +202,25 @@ export default class Widget {
|
|||
this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION
|
||||
? 4
|
||||
: this.metricType === WEB_VITALS
|
||||
? 1
|
||||
: 2,
|
||||
},
|
||||
? 1
|
||||
: 2
|
||||
}
|
||||
};
|
||||
|
||||
if (this.metricType === USER_PATH) {
|
||||
data.hideExcess = this.hideExcess;
|
||||
data.startType = this.startType;
|
||||
data.startPoint = [this.startPoint.toJson()];
|
||||
console.log('excludes', this.excludes);
|
||||
data.excludes = this.series[0].filter.excludes.map((i: any) => i.toJson());
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
updateStartPoint(startPoint: any) {
|
||||
runInAction(() => {
|
||||
this.startPoint = new FilterItem(startPoint);
|
||||
});
|
||||
}
|
||||
|
||||
validate() {
|
||||
|
|
@ -214,10 +247,10 @@ export default class Widget {
|
|||
.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)
|
||||
new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew)
|
||||
);
|
||||
} else if (this.metricType === FUNNEL) {
|
||||
_data.funnel = new Funnel().fromJSON(_data);
|
||||
_data.funnel = new Funnel().fromJSON(_data);
|
||||
} else {
|
||||
if (data.hasOwnProperty('chart')) {
|
||||
_data['value'] = data.value;
|
||||
|
|
@ -237,20 +270,20 @@ export default class Widget {
|
|||
_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;
|
||||
}, [])
|
||||
.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;
|
||||
}, [])
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this.data, _data)
|
||||
Object.assign(this.data, _data);
|
||||
return _data;
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +294,7 @@ export default class Widget {
|
|||
response.map((cat: { sessions: any[] }) => {
|
||||
return {
|
||||
...cat,
|
||||
sessions: cat.sessions.map((s: any) => new Session().fromJson(s)),
|
||||
sessions: cat.sessions.map((s: any) => new Session().fromJson(s))
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
@ -269,17 +302,30 @@ export default class Widget {
|
|||
});
|
||||
}
|
||||
|
||||
fetchIssues(filter: any): Promise<any> {
|
||||
|
||||
fetchIssues(card: 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))
|
||||
: [];
|
||||
metricService.fetchIssues(card)
|
||||
.then((response: any) => {
|
||||
if (card.metricType === USER_PATH) {
|
||||
resolve({
|
||||
total: response.count,
|
||||
issues: response.values.map((issue: any) => new Issue().fromJSON(issue))
|
||||
});
|
||||
} else {
|
||||
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
|
||||
});
|
||||
}
|
||||
}).finally(() => {
|
||||
resolve({
|
||||
issues: significantIssues.length > 0 ? significantIssues : insignificantIssues,
|
||||
issues: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -292,7 +338,7 @@ export default class Widget {
|
|||
.then((response: any) => {
|
||||
resolve({
|
||||
issue: new Funnelissue().fromJSON(response.issue),
|
||||
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)),
|
||||
sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s))
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
|
|
|
|||
|
|
@ -1,113 +1,127 @@
|
|||
import Widget from "App/mstore/types/widget";
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import APIClient from 'App/api_client';
|
||||
import { CLICKMAP } from "App/constants/card";
|
||||
import { CLICKMAP, USER_PATH } from 'App/constants/card';
|
||||
|
||||
export default class MetricService {
|
||||
private client: APIClient;
|
||||
private client: APIClient;
|
||||
|
||||
constructor(client?: APIClient) {
|
||||
this.client = client ? client : new APIClient();
|
||||
constructor(client?: APIClient) {
|
||||
this.client = client ? client : new APIClient();
|
||||
}
|
||||
|
||||
initClient(client?: APIClient) {
|
||||
this.client = client || new APIClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getMetrics(): Promise<any> {
|
||||
return this.client.get('/cards')
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metric by metricId.
|
||||
* @param metricId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getMetric(metricId: string): Promise<any> {
|
||||
return this.client.get('/cards/' + metricId)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a metric.
|
||||
* @param metric
|
||||
* @returns
|
||||
*/
|
||||
saveMetric(metric: Widget): Promise<any> {
|
||||
const data = metric.toJson();
|
||||
const isCreating = !data[Widget.ID_KEY];
|
||||
const url = isCreating ? '/cards' : '/cards/' + data[Widget.ID_KEY];
|
||||
return this.client.post(url, data)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a metric.
|
||||
* @param metricId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
deleteMetric(metricId: string): Promise<any> {
|
||||
return this.client.delete('/cards/' + metricId)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all templates.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getTemplates(): Promise<any> {
|
||||
return this.client.get('/cards/templates')
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
async getMetricChartData(metric: Widget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
if (
|
||||
metric.metricType === CLICKMAP
|
||||
&& document.location.pathname.split('/').pop() === 'metrics'
|
||||
&& (document.location.pathname.indexOf('dashboard') !== -1 && document.location.pathname.indexOf('metric') === -1)
|
||||
) {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
const path = isWidget ? `/cards/${metric.metricId}/chart` : `/cards/try`;
|
||||
if (metric.metricType === USER_PATH) {
|
||||
data.density = 4;
|
||||
data.metricOf = 'sessionCount';
|
||||
}
|
||||
try {
|
||||
const r = await this.client.post(path, data);
|
||||
const response = await r.json();
|
||||
return response.data || {};
|
||||
} catch (e) {
|
||||
return await Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sessions from the server.
|
||||
* @param metricId {String}
|
||||
* @param filter
|
||||
* @returns
|
||||
*/
|
||||
fetchSessions(metricId: string | null, filter: any): Promise<any> {
|
||||
return this.client.post(metricId ? `/cards/${metricId}/sessions` : '/cards/try/sessions', filter)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
async fetchIssues(filter: any): Promise<any> {
|
||||
if (filter.metricType === USER_PATH) {
|
||||
const widget = new Widget().fromJson(filter);
|
||||
const drillDownFilter = filter.filters;
|
||||
filter = widget.toJson();
|
||||
filter.filters = drillDownFilter;
|
||||
}
|
||||
|
||||
initClient(client?: APIClient) {
|
||||
this.client = client || new APIClient();
|
||||
}
|
||||
let resp: Response = await this.client.post(`/cards/try/issues`, filter);
|
||||
const json: any = await resp.json();
|
||||
return await json.data || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getMetrics(): Promise<any> {
|
||||
return this.client.get('/cards')
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a metric by metricId.
|
||||
* @param metricId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getMetric(metricId: string): Promise<any> {
|
||||
return this.client.get('/cards/' + metricId)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a metric.
|
||||
* @param metric
|
||||
* @returns
|
||||
*/
|
||||
saveMetric(metric: Widget): Promise<any> {
|
||||
const data = metric.toJson()
|
||||
const isCreating = !data[Widget.ID_KEY];
|
||||
const url = isCreating ? '/cards' : '/cards/' + data[Widget.ID_KEY];
|
||||
return this.client.post(url, data)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a metric.
|
||||
* @param metricId
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
deleteMetric(metricId: string): Promise<any> {
|
||||
return this.client.delete('/cards/' + metricId)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all templates.
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
getTemplates(): Promise<any> {
|
||||
return this.client.get('/cards/templates')
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
getMetricChartData(metric: Widget, data: any, isWidget: boolean = false): Promise<any> {
|
||||
if (
|
||||
metric.metricType === CLICKMAP
|
||||
&& document.location.pathname.split('/').pop() === 'metrics'
|
||||
&& (document.location.pathname.indexOf('dashboard') !== -1 && document.location.pathname.indexOf('metric') === -1)
|
||||
) {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
const path = isWidget ? `/cards/${metric.metricId}/chart` : `/cards/try`;
|
||||
return this.client.post(path, data)
|
||||
.then(r => r.json())
|
||||
.then((response: { data: any; }) => response.data || {})
|
||||
.catch(e => Promise.reject(e))
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sessions from the server.
|
||||
* @param metricId {String}
|
||||
* @param filter
|
||||
* @returns
|
||||
*/
|
||||
fetchSessions(metricId: string, filter: any): Promise<any> {
|
||||
return this.client.post(metricId ? `/cards/${metricId}/sessions` : '/cards/try/sessions', filter)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || []);
|
||||
}
|
||||
|
||||
fetchIssues(filter: string): Promise<any> {
|
||||
return this.client.post(`/cards/try/issues`, filter)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
|
||||
fetchIssue(metricId: string, issueId: string, params: any): Promise<any> {
|
||||
return this.client.post(`/cards/${metricId}/issues/${issueId}/sessions`, params)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
fetchIssue(metricId: string, issueId: string, params: any): Promise<any> {
|
||||
return this.client.post(`/cards/${metricId}/issues/${issueId}/sessions`, params)
|
||||
.then((response: { json: () => any; }) => response.json())
|
||||
.then((response: { data: any; }) => response.data || {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
frontend/app/svg/icons/clock-history.svg
Normal file
5
frontend/app/svg/icons/clock-history.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-clock-history" viewBox="0 0 16 16">
|
||||
<path d="M8.515 1.019A7 7 0 0 0 8 1V0a8 8 0 0 1 .589.022l-.074.997zm2.004.45a7.003 7.003 0 0 0-.985-.299l.219-.976c.383.086.76.2 1.126.342l-.36.933zm1.37.71a7.01 7.01 0 0 0-.439-.27l.493-.87a8.025 8.025 0 0 1 .979.654l-.615.789a6.996 6.996 0 0 0-.418-.302zm1.834 1.79a6.99 6.99 0 0 0-.653-.796l.724-.69c.27.285.52.59.747.91l-.818.576zm.744 1.352a7.08 7.08 0 0 0-.214-.468l.893-.45a7.976 7.976 0 0 1 .45 1.088l-.95.313a7.023 7.023 0 0 0-.179-.483zm.53 2.507a6.991 6.991 0 0 0-.1-1.025l.985-.17c.067.386.106.778.116 1.17l-1 .025zm-.131 1.538c.033-.17.06-.339.081-.51l.993.123a7.957 7.957 0 0 1-.23 1.155l-.964-.267c.046-.165.086-.332.12-.501zm-.952 2.379c.184-.29.346-.594.486-.908l.914.405c-.16.36-.345.706-.555 1.038l-.845-.535zm-.964 1.205c.122-.122.239-.248.35-.378l.758.653a8.073 8.073 0 0 1-.401.432l-.707-.707z"/>
|
||||
<path d="M8 1a7 7 0 1 0 4.95 11.95l.707.707A8.001 8.001 0 1 1 8 0v1z"/>
|
||||
<path d="M7.5 3a.5.5 0 0 1 .5.5v5.21l3.248 1.856a.5.5 0 0 1-.496.868l-3.5-2A.5.5 0 0 1 7 9V3.5a.5.5 0 0 1 .5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3 KiB After Width: | Height: | Size: 3 KiB |
4
frontend/app/svg/icons/dice-3.svg
Normal file
4
frontend/app/svg/icons/dice-3.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-dice-3" viewBox="0 0 16 16">
|
||||
<path d="M13 1a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10zM3 0a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V3a3 3 0 0 0-3-3H3z"/>
|
||||
<path d="M5.5 4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm8 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm-4-4a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 387 B |
|
|
@ -1,75 +1,531 @@
|
|||
import { KEYS } from 'Types/filter/customFilter';
|
||||
import Record from 'Types/Record';
|
||||
import { FilterType, FilterKey, FilterCategory } from './filterType'
|
||||
import { FilterType, FilterKey, FilterCategory } from './filterType';
|
||||
import filterOptions, { countries, platformOptions } from 'App/constants';
|
||||
import { capitalize } from 'App/utils';
|
||||
|
||||
const countryOptions = Object.keys(countries).map(i => ({ label: countries[i], value: i }));
|
||||
const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }]
|
||||
const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }];
|
||||
|
||||
export const filters = [
|
||||
{ key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true },
|
||||
{ key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Text Input', placeholder: 'Enter input label name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true },
|
||||
{ key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Visited URL', placeholder: 'Enter path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true },
|
||||
{ key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', placeholder: 'Enter event key', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true },
|
||||
{
|
||||
key: FilterKey.CLICK,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.INTERACTIONS,
|
||||
label: 'Click',
|
||||
operator: 'on',
|
||||
operatorOptions: filterOptions.targetOperators,
|
||||
icon: 'filters/click',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.INPUT,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.INTERACTIONS,
|
||||
label: 'Text Input',
|
||||
placeholder: 'Enter input label name',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/input',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.LOCATION,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.INTERACTIONS,
|
||||
label: 'Visited URL',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/location',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.CUSTOM,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
label: 'Custom Events',
|
||||
placeholder: 'Enter event key',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/custom',
|
||||
isEvent: true
|
||||
},
|
||||
// { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true },
|
||||
{ key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [
|
||||
{ key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', placeholder: 'Enter path or URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', placeholder: 'Enter status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', placeholder: 'Select method type', operatorOptions: filterOptions.stringOperatorsLimited, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
||||
{ key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', placeholder: 'E.g. 12', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
], icon: 'filters/fetch', isEvent: true },
|
||||
{ key: FilterKey.GRAPHQL, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true, filters: [
|
||||
{ key: FilterKey.GRAPHQL_NAME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.GRAPHQL_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperatorsLimited, icon: 'filters/fetch', options: filterOptions.methodOptions },
|
||||
{ key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
{ key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' },
|
||||
]},
|
||||
{ key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'State Action', placeholder: 'E.g. 12', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true },
|
||||
{ key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error Message', placeholder: 'E.g. Uncaught SyntaxError', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true },
|
||||
{
|
||||
key: FilterKey.FETCH,
|
||||
type: FilterType.SUB_FILTERS,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
operator: 'is',
|
||||
label: 'Network Request',
|
||||
filters: [
|
||||
{
|
||||
key: FilterKey.FETCH_URL,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with URL',
|
||||
placeholder: 'Enter path or URL',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_STATUS_CODE,
|
||||
type: FilterType.NUMBER_MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with status code',
|
||||
placeholder: 'Enter status code',
|
||||
operator: '=',
|
||||
operatorOptions: filterOptions.customOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_METHOD,
|
||||
type: FilterType.MULTIPLE_DROPDOWN,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with method',
|
||||
operator: 'is',
|
||||
placeholder: 'Select method type',
|
||||
operatorOptions: filterOptions.stringOperatorsLimited,
|
||||
icon: 'filters/fetch',
|
||||
options: filterOptions.methodOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_DURATION,
|
||||
type: FilterType.NUMBER,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with duration (ms)',
|
||||
placeholder: 'E.g. 12',
|
||||
operator: '=',
|
||||
operatorOptions: filterOptions.customOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_REQUEST_BODY,
|
||||
type: FilterType.STRING,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with request body',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_RESPONSE_BODY,
|
||||
type: FilterType.STRING,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with response body',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
}
|
||||
],
|
||||
icon: 'filters/fetch',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.GRAPHQL,
|
||||
type: FilterType.SUB_FILTERS,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
label: 'GraphQL',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/graphql',
|
||||
isEvent: true,
|
||||
filters: [
|
||||
{
|
||||
key: FilterKey.GRAPHQL_NAME,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with name',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.GRAPHQL_METHOD,
|
||||
type: FilterType.MULTIPLE_DROPDOWN,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with method',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperatorsLimited,
|
||||
icon: 'filters/fetch',
|
||||
options: filterOptions.methodOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.GRAPHQL_REQUEST_BODY,
|
||||
type: FilterType.STRING,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with request body',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
},
|
||||
{
|
||||
key: FilterKey.GRAPHQL_RESPONSE_BODY,
|
||||
type: FilterType.STRING,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'with response body',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/fetch'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: FilterKey.STATEACTION,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
label: 'State Action',
|
||||
placeholder: 'E.g. 12',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/state-action',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.ERROR,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
label: 'Error Message',
|
||||
placeholder: 'E.g. Uncaught SyntaxError',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/error',
|
||||
isEvent: true
|
||||
},
|
||||
// { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true },
|
||||
|
||||
// FILTERS
|
||||
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
|
||||
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
|
||||
{ key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
|
||||
{ key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions },
|
||||
{ key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', placeholder: 'E.g. v1.0.8', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' },
|
||||
{ key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
|
||||
{ key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' },
|
||||
{ key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
{ key: FilterKey.USER_CITY, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User City', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
{ key: FilterKey.USER_STATE, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User State', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
// { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
|
||||
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' },
|
||||
{ key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },
|
||||
// FILTERS
|
||||
{
|
||||
key: FilterKey.USER_OS,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User OS',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/os'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_BROWSER,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User Browser',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/browser'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_DEVICE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User Device',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/device'
|
||||
},
|
||||
{
|
||||
key: FilterKey.PLATFORM,
|
||||
type: FilterType.MULTIPLE_DROPDOWN,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'Platform',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.baseOperators,
|
||||
icon: 'filters/platform',
|
||||
options: platformOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.REVID,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'Version ID',
|
||||
placeholder: 'E.g. v1.0.8',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'collection'
|
||||
},
|
||||
{
|
||||
key: FilterKey.REFERRER,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.RECORDING_ATTRIBUTES,
|
||||
label: 'Referrer',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/arrow-return-right'
|
||||
},
|
||||
{
|
||||
key: FilterKey.DURATION,
|
||||
type: FilterType.DURATION,
|
||||
category: FilterCategory.RECORDING_ATTRIBUTES,
|
||||
label: 'Duration',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is']),
|
||||
icon: 'filters/duration'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_COUNTRY,
|
||||
type: FilterType.MULTIPLE_DROPDOWN,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User Country',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_CITY,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User City',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_STATE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User State',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
// { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' },
|
||||
{
|
||||
key: FilterKey.USERID,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User Id',
|
||||
placeholder: 'E.g. Alex, or alex@domain.com, or EMP123',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined' }]),
|
||||
icon: 'filters/userid'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USERANONYMOUSID,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User AnonymousId',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/userid'
|
||||
},
|
||||
|
||||
// PERFORMANCE
|
||||
{ key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators, sourcePlaceholder: 'E.g. 12', },
|
||||
{ key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: '%', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'mb', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators },
|
||||
{ key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true },
|
||||
{ key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions },
|
||||
// PERFORMANCE
|
||||
{
|
||||
key: FilterKey.DOM_COMPLETE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'DOM Complete',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
source: [],
|
||||
icon: 'filters/dom-complete',
|
||||
isEvent: true,
|
||||
hasSource: true,
|
||||
sourceOperator: '>=',
|
||||
sourcePlaceholder: 'E.g. 12',
|
||||
sourceUnit: 'ms',
|
||||
sourceType: FilterType.NUMBER,
|
||||
sourceOperatorOptions: filterOptions.customOperators
|
||||
},
|
||||
{
|
||||
key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'Largest Contentful Paint',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
source: [],
|
||||
icon: 'filters/lcpt',
|
||||
isEvent: true,
|
||||
hasSource: true,
|
||||
sourceOperator: '>=',
|
||||
sourcePlaceholder: 'E.g. 12',
|
||||
sourceUnit: 'ms',
|
||||
sourceType: FilterType.NUMBER,
|
||||
sourceOperatorOptions: filterOptions.customOperators
|
||||
},
|
||||
{
|
||||
key: FilterKey.TTFB,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'Time to First Byte',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
source: [],
|
||||
icon: 'filters/ttfb',
|
||||
isEvent: true,
|
||||
hasSource: true,
|
||||
sourceOperator: '>=',
|
||||
sourceUnit: 'ms',
|
||||
sourceType: FilterType.NUMBER,
|
||||
sourceOperatorOptions: filterOptions.customOperators,
|
||||
sourcePlaceholder: 'E.g. 12'
|
||||
},
|
||||
{
|
||||
key: FilterKey.AVG_CPU_LOAD,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'Avg CPU Load',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
source: [],
|
||||
icon: 'filters/cpu-load',
|
||||
isEvent: true,
|
||||
hasSource: true,
|
||||
sourceOperator: '>=',
|
||||
sourcePlaceholder: 'E.g. 12',
|
||||
sourceUnit: '%',
|
||||
sourceType: FilterType.NUMBER,
|
||||
sourceOperatorOptions: filterOptions.customOperators
|
||||
},
|
||||
{
|
||||
key: FilterKey.AVG_MEMORY_USAGE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'Avg Memory Usage',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
source: [],
|
||||
icon: 'filters/memory-load',
|
||||
isEvent: true,
|
||||
hasSource: true,
|
||||
sourceOperator: '>=',
|
||||
sourcePlaceholder: 'E.g. 12',
|
||||
sourceUnit: 'mb',
|
||||
sourceType: FilterType.NUMBER,
|
||||
sourceOperatorOptions: filterOptions.customOperators
|
||||
},
|
||||
{
|
||||
key: FilterKey.FETCH_FAILED,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.PERFORMANCE,
|
||||
label: 'Failed Request',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'isAny',
|
||||
operatorOptions: filterOptions.stringOperatorsPerformance,
|
||||
icon: 'filters/fetch-failed',
|
||||
isEvent: true
|
||||
},
|
||||
{
|
||||
key: FilterKey.ISSUE,
|
||||
type: FilterType.ISSUE,
|
||||
category: FilterCategory.JAVASCRIPT,
|
||||
label: 'Issue',
|
||||
placeholder: 'Select an issue',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/click',
|
||||
options: filterOptions.issueOptions
|
||||
}
|
||||
];
|
||||
|
||||
export const flagConditionFilters = [
|
||||
{ key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' },
|
||||
{ key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' },
|
||||
{ key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' },
|
||||
{ key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' },
|
||||
{ key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
{ key: FilterKey.USER_CITY, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User City', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
{ key: FilterKey.USER_STATE, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User State', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions },
|
||||
{ key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'isUndefined', operatorOptions: [{ label: 'is undefined', value: 'isUndefined'}, { key: 'isAny', label: 'is any', value: 'isAny' }], icon: 'filters/userid' },
|
||||
]
|
||||
{
|
||||
key: FilterKey.USER_OS,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User OS',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/os'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_BROWSER,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User Browser',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/browser'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_DEVICE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.GEAR,
|
||||
label: 'User Device',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/device'
|
||||
},
|
||||
{
|
||||
key: FilterKey.REFERRER,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'Referrer',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/arrow-return-right'
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_COUNTRY,
|
||||
type: FilterType.MULTIPLE_DROPDOWN,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User Country',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_CITY,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User City',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.USER_STATE,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User State',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']),
|
||||
icon: 'filters/country',
|
||||
options: countryOptions
|
||||
},
|
||||
{
|
||||
key: FilterKey.USERID,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.USER,
|
||||
label: 'User Id',
|
||||
operator: 'isUndefined',
|
||||
operatorOptions: [{ label: 'is undefined', value: 'isUndefined' }, {
|
||||
key: 'isAny',
|
||||
label: 'is any',
|
||||
value: 'isAny'
|
||||
}],
|
||||
icon: 'filters/userid'
|
||||
}
|
||||
];
|
||||
|
||||
const pathAnalysisStartPoint = [
|
||||
{
|
||||
key: FilterKey.LOCATION,
|
||||
type: FilterType.MULTIPLE,
|
||||
category: FilterCategory.INTERACTIONS,
|
||||
label: 'Visited URL',
|
||||
placeholder: 'Enter path',
|
||||
operator: 'is',
|
||||
operatorOptions: filterOptions.stringOperators,
|
||||
icon: 'filters/location',
|
||||
isEvent: true
|
||||
}
|
||||
];
|
||||
|
||||
export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key);
|
||||
export const nonFlagFilters = filters.filter(i => {
|
||||
return flagConditionFilters.findIndex(f => f.key === i.key) === -1
|
||||
return flagConditionFilters.findIndex(f => f.key === i.key) === -1;
|
||||
}).map(i => i.key);
|
||||
|
||||
export const clickmapFilter = {
|
||||
|
|
@ -80,15 +536,16 @@ export const clickmapFilter = {
|
|||
operator: filterOptions.pageUrlOperators[0].value,
|
||||
operatorOptions: filterOptions.pageUrlOperators,
|
||||
icon: 'filters/location',
|
||||
isEvent: true,
|
||||
}
|
||||
isEvent: true
|
||||
};
|
||||
|
||||
const mapFilters = (list) => {
|
||||
return list.reduce((acc, filter) => {
|
||||
filter.value = [''];
|
||||
acc[filter.key] = filter;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
const liveFilterSupportedOperators = ['is', 'contains'];
|
||||
const mapLiveFilters = (list) => {
|
||||
|
|
@ -101,25 +558,25 @@ const mapLiveFilters = (list) => {
|
|||
filter.key !== FilterKey.DURATION &&
|
||||
filter.key !== FilterKey.REFERRER
|
||||
) {
|
||||
obj[filter.key] = {...filter};
|
||||
obj[filter.key] = { ...filter };
|
||||
obj[filter.key].operatorOptions = filter.operatorOptions.filter(operator => liveFilterSupportedOperators.includes(operator.value));
|
||||
if (filter.key === FilterKey.PLATFORM) {
|
||||
obj[filter.key].operator = 'is';
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
export const filterLabelMap = filters.reduce((acc, filter) => {
|
||||
acc[filter.key] = filter.label
|
||||
return acc
|
||||
}, {})
|
||||
acc[filter.key] = filter.label;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export let filtersMap = mapFilters(filters)
|
||||
export let liveFiltersMap = mapLiveFilters(filters)
|
||||
export let fflagsConditionsMap = mapFilters(flagConditionFilters)
|
||||
export let filtersMap = mapFilters(filters);
|
||||
export let liveFiltersMap = mapLiveFilters(filters);
|
||||
export let fflagsConditionsMap = mapFilters(flagConditionFilters);
|
||||
|
||||
export const clearMetaFilters = () => {
|
||||
filtersMap = mapFilters(filters);
|
||||
|
|
@ -143,19 +600,37 @@ export const addElementToFiltersMap = (
|
|||
operatorOptions = filterOptions.stringOperators,
|
||||
icon = 'filters/metadata'
|
||||
) => {
|
||||
filtersMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true }
|
||||
}
|
||||
filtersMap[key] = {
|
||||
key,
|
||||
type,
|
||||
category,
|
||||
label: capitalize(key),
|
||||
operator: operator,
|
||||
operatorOptions,
|
||||
icon,
|
||||
isLive: true
|
||||
};
|
||||
};
|
||||
|
||||
export const addElementToFlagConditionsMap = (
|
||||
category = FilterCategory.METADATA,
|
||||
key,
|
||||
type = FilterType.MULTIPLE,
|
||||
operator = 'is',
|
||||
operatorOptions = filterOptions.stringOperators,
|
||||
icon = 'filters/metadata'
|
||||
category = FilterCategory.METADATA,
|
||||
key,
|
||||
type = FilterType.MULTIPLE,
|
||||
operator = 'is',
|
||||
operatorOptions = filterOptions.stringOperators,
|
||||
icon = 'filters/metadata'
|
||||
) => {
|
||||
fflagsConditionsMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true }
|
||||
}
|
||||
fflagsConditionsMap[key] = {
|
||||
key,
|
||||
type,
|
||||
category,
|
||||
label: capitalize(key),
|
||||
operator: operator,
|
||||
operatorOptions,
|
||||
icon,
|
||||
isLive: true
|
||||
};
|
||||
};
|
||||
|
||||
export const addElementToLiveFiltersMap = (
|
||||
category = FilterCategory.METADATA,
|
||||
|
|
@ -166,14 +641,14 @@ export const addElementToLiveFiltersMap = (
|
|||
icon = 'filters/metadata'
|
||||
) => {
|
||||
liveFiltersMap[key] = {
|
||||
key, type, category, label: capitalize(key),
|
||||
operator: operator,
|
||||
operatorOptions,
|
||||
icon,
|
||||
operatorDisabled: true,
|
||||
isLive: true
|
||||
}
|
||||
}
|
||||
key, type, category, label: capitalize(key),
|
||||
operator: operator,
|
||||
operatorOptions,
|
||||
icon,
|
||||
operatorDisabled: true,
|
||||
isLive: true
|
||||
};
|
||||
};
|
||||
|
||||
export default Record({
|
||||
timestamp: 0,
|
||||
|
|
@ -182,8 +657,8 @@ export default Record({
|
|||
placeholder: '',
|
||||
icon: '',
|
||||
type: '',
|
||||
value: [""],
|
||||
source: [""],
|
||||
value: [''],
|
||||
source: [''],
|
||||
category: '',
|
||||
|
||||
custom: '',
|
||||
|
|
@ -195,7 +670,7 @@ export default Record({
|
|||
actualValue: '',
|
||||
|
||||
hasSource: false,
|
||||
source: [""],
|
||||
source: [''],
|
||||
sourceType: '',
|
||||
sourceOperator: '=',
|
||||
sourcePlaceholder: '',
|
||||
|
|
@ -208,20 +683,19 @@ export default Record({
|
|||
isEvent: false,
|
||||
index: 0,
|
||||
options: [],
|
||||
|
||||
filters: [],
|
||||
|
||||
excludes: []
|
||||
}, {
|
||||
keyKey: "_key",
|
||||
keyKey: '_key',
|
||||
fromJS: ({ value, type, subFilter = false, ...filter }) => {
|
||||
let _filter = {};
|
||||
if (subFilter) {
|
||||
const mainFilter = filtersMap[subFilter];
|
||||
const subFilterMap = {}
|
||||
const subFilterMap = {};
|
||||
mainFilter.filters.forEach(option => {
|
||||
subFilterMap[option.key] = option
|
||||
})
|
||||
_filter = subFilterMap[type]
|
||||
subFilterMap[option.key] = option;
|
||||
});
|
||||
_filter = subFilterMap[type];
|
||||
} else {
|
||||
if (type === FilterKey.METADATA) {
|
||||
_filter = filtersMap[filter.source];
|
||||
|
|
@ -233,8 +707,8 @@ export default Record({
|
|||
if (!_filter) {
|
||||
_filter = {
|
||||
key: filter.key,
|
||||
type: "MULTIPLE",
|
||||
}
|
||||
type: 'MULTIPLE'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -242,10 +716,10 @@ export default Record({
|
|||
...filter,
|
||||
key: _filter.key,
|
||||
type: _filter.type, // camelCased(filter.type.toLowerCase()),
|
||||
value: value.length === 0 || !value ? [""] : value,
|
||||
}
|
||||
},
|
||||
})
|
||||
value: value.length === 0 || !value ? [''] : value
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Group filters by category
|
||||
|
|
@ -263,7 +737,7 @@ export const generateFilterOptions = (map) => {
|
|||
}
|
||||
});
|
||||
return filterSection;
|
||||
}
|
||||
};
|
||||
|
||||
export const generateFlagConditionOptions = (map) => {
|
||||
const filterSection = {};
|
||||
|
|
@ -276,8 +750,7 @@ export const generateFlagConditionOptions = (map) => {
|
|||
}
|
||||
});
|
||||
return filterSection;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
export const generateLiveFilterOptions = (map) => {
|
||||
|
|
@ -294,4 +767,4 @@ export const generateLiveFilterOptions = (map) => {
|
|||
}
|
||||
});
|
||||
return filterSection;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
"react-tippy": "^1.4.0",
|
||||
"react-toastify": "^9.1.1",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"recharts": "^2.1.13",
|
||||
"recharts": "^2.8.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-immutable": "^4.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
|
|
|
|||
|
|
@ -11270,10 +11270,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-equals@npm:^2.0.0":
|
||||
version: 2.0.4
|
||||
resolution: "fast-equals@npm:2.0.4"
|
||||
checksum: 2867aa148c995e4ea921242ab605b157e5cbdc44793ebddf83684a5e6673be5016eb790ec4d8329317b92887e1108fb67ed3d4334529f2a7650c1338e6aa2c5f
|
||||
"fast-equals@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "fast-equals@npm:5.0.1"
|
||||
checksum: d7077b8b681036c2840ed9860a3048e44fc268fad2b525b8f25b43458be0c8ad976152eb4b475de9617170423c5b802121ebb61ed6641c3ac035fadaf805c8c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -17733,7 +17733,7 @@ __metadata:
|
|||
react-tippy: ^1.4.0
|
||||
react-toastify: ^9.1.1
|
||||
react-virtualized: ^9.22.3
|
||||
recharts: ^2.1.13
|
||||
recharts: ^2.8.0
|
||||
redux: ^4.0.5
|
||||
redux-immutable: ^4.0.0
|
||||
redux-thunk: ^2.3.0
|
||||
|
|
@ -20589,15 +20589,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-resize-detector@npm:^7.1.2":
|
||||
version: 7.1.2
|
||||
resolution: "react-resize-detector@npm:7.1.2"
|
||||
"react-resize-detector@npm:^8.0.4":
|
||||
version: 8.1.0
|
||||
resolution: "react-resize-detector@npm:8.1.0"
|
||||
dependencies:
|
||||
lodash: ^4.17.21
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 2285b0024bcc736c7d5e80279e819835a5e8bef0899778100d2434b1c4b10971e6ae253df073ca96a20cacc47b3c249bc675479e5fc4ec1f6652fcca7f48ec22
|
||||
checksum: 2ae9927c6e53de460a1f216e008acd30d84d11297bbb2c38687206b998309dfc3928992141e2176a00af642ef8b1c7f56e97681cc7d1c3a4f389e29d46a443af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -20657,17 +20657,17 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-smooth@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "react-smooth@npm:2.0.1"
|
||||
"react-smooth@npm:^2.0.2":
|
||||
version: 2.0.4
|
||||
resolution: "react-smooth@npm:2.0.4"
|
||||
dependencies:
|
||||
fast-equals: ^2.0.0
|
||||
fast-equals: ^5.0.0
|
||||
react-transition-group: 2.9.0
|
||||
peerDependencies:
|
||||
prop-types: ^15.6.0
|
||||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 3227aecfb2c2783a53045e0b5931432fed3376c98893d1190c259ed4281182dc0b25f200f50b55733e01d9d6d1950641d1b5177d899277e3d4fe8264b6bddcc2
|
||||
checksum: a67103136ef7f7378183dce3baecb28f1828ac71ce9901f1dfd92a4ec3a637014a7a6787f79552bb5cd2b779d27afd978d0f5eb33693b6a1210f774ab6e131cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -20964,16 +20964,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"recharts@npm:^2.1.13":
|
||||
version: 2.3.2
|
||||
resolution: "recharts@npm:2.3.2"
|
||||
"recharts@npm:^2.8.0":
|
||||
version: 2.8.0
|
||||
resolution: "recharts@npm:2.8.0"
|
||||
dependencies:
|
||||
classnames: ^2.2.5
|
||||
eventemitter3: ^4.0.1
|
||||
lodash: ^4.17.19
|
||||
react-is: ^16.10.2
|
||||
react-resize-detector: ^7.1.2
|
||||
react-smooth: ^2.0.1
|
||||
react-resize-detector: ^8.0.4
|
||||
react-smooth: ^2.0.2
|
||||
recharts-scale: ^0.4.4
|
||||
reduce-css-calc: ^2.1.8
|
||||
victory-vendor: ^36.6.8
|
||||
|
|
@ -20981,7 +20981,7 @@ __metadata:
|
|||
prop-types: ^15.6.0
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 88e86118388df94c07dc9b39d4e0a0c47defb1a6348fdbc0e2ad457f1329e5fbc4525fbd67bb94485a7ef0e8b23320e4e9066640b18241b3e0fad80b5eabfd28
|
||||
checksum: 9f78bdf67fd5394f472a37679d2f9c9194f1d572c5bc086caae578409fceb7bc87a4df07dd096820c103d2002d9714c8db838c99b2722950ab9edab38be2daf9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue