feature(ui): new dashboard modal

This commit is contained in:
Shekar Siri 2024-06-17 14:28:58 +02:00
parent 0f5b80fe97
commit b0785d1435
24 changed files with 840 additions and 267 deletions

View file

@ -0,0 +1,94 @@
import React from 'react';
import {Button, Space} from "antd";
import {ArrowLeft, ArrowRight} from "lucide-react";
import CardBuilder from "Components/Dashboard/components/WidgetForm/CardBuilder";
import {useHistory} from "react-router";
import {useStore} from "App/mstore";
import {CLICKMAP} from "App/constants/card";
import {renderClickmapThumbnail} from "Components/Dashboard/components/WidgetForm/renderMap";
import WidgetPreview from "Components/Dashboard/components/WidgetPreview/WidgetPreview";
const getTitleByType = (type: string) => {
switch (type) {
case CLICKMAP:
return 'Clickmap';
default:
return 'Trend Single';
}
}
interface Props {
// cardType: string,
onBack: () => void
}
function CreateCard(props: Props) {
const history = useHistory();
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric = metricStore.instance;
const siteId = history.location.pathname.split('/')[1];
// const title = getTitleByType(metric.metricType)
const createNewDashboard = async () => {
dashboardStore.initDashboard();
return await dashboardStore
.save(dashboardStore.dashboardInstance)
.then(async (syncedDashboard) => {
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
return syncedDashboard.dashboardId;
});
}
const addCardToDashboard = async (dashboardId: string, metricId: string) => {
return dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [metricId]
);
}
const createCard = async () => {
const isClickmap = metric.metricType === CLICKMAP;
if (isClickmap) {
try {
metric.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(metric);
return savedMetric.metricId;
}
const createDashboardAndAddCard = async () => {
const dashboardId = await createNewDashboard();
const cardId = await createCard();
await addCardToDashboard(dashboardId, cardId);
history.replace(`${history.location.pathname}/${dashboardId}`);
}
return (
<div className="flex gap-4 flex-col">
<div className="flex items-center justify-between">
<Space>
<Button type="text" onClick={props.onBack}>
<ArrowLeft size={16}/>
</Button>
<div className="text-2xl leading-4 font-semibold">
{metric.name}
</div>
</Space>
<Button type="primary" onClick={createDashboardAndAddCard}>
<Space>
<ArrowRight size={14}/>Create
</Space>
</Button>
</div>
<CardBuilder siteId={siteId}/>
<WidgetPreview className="mt-8" name={metric.name} isEditing={true}/>
</div>
);
}
export default CreateCard;

View file

@ -16,7 +16,7 @@ const TYPES = {
Users: 'users',
};
function ExampleCount({ onCard }: { onCard: (card: string) => void }) {
function ExampleCount(props: any) {
const [type, setType] = React.useState(TYPES.Frustrations);
const el = {
@ -26,11 +26,10 @@ function ExampleCount({ onCard }: { onCard: (card: string) => void }) {
};
return (
<ExCard
onCard={onCard}
type={'count' + `-${type}`}
{...props}
title={
<div className={'flex items-center gap-2'}>
<div>Sessions by</div>
<div>{props.title}</div>
<div className={'font-normal'}>
<Segmented
options={[

View file

@ -14,7 +14,7 @@ function ExCard({
return (
<div
className={'rounded overflow-hidden border p-4 bg-white hover:border-gray-light hover:shadow'}
style={{ width: 400, height: 286 }}
style={{ width: '100%', height: 286 }}
>
<div className={'font-semibold text-lg'}>{title}</div>
<div className={'flex flex-col gap-2 mt-2 cursor-pointer'} onClick={() => onCard(type)}>{children}</div>
@ -22,4 +22,4 @@ function ExCard({
);
}
export default ExCard
export default ExCard

View file

@ -2,8 +2,9 @@ import { ArrowRight } from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {FUNNEL} from "App/constants/card";
function ExampleFunnel({ onCard }: { onCard: (card: string) => void }) {
function ExampleFunnel(props: any) {
const steps = [
{
progress: 500,
@ -17,9 +18,7 @@ function ExampleFunnel({ onCard }: { onCard: (card: string) => void }) {
];
return (
<ExCard
title={'Funnel'}
onCard={onCard}
type={'funnel'}
{...props}
>
<>
{steps.map((step, index) => (

View file

@ -1,59 +1,59 @@
import React from 'react';
import { ResponsiveContainer, Sankey } from 'recharts';
import {ResponsiveContainer, Sankey} from 'recharts';
import CustomLink from 'App/components/shared/Insights/SankeyChart/CustomLink';
import CustomNode from 'App/components/shared/Insights/SankeyChart/CustomNode';
import ExCard from './ExCard';
import {USER_PATH} from "App/constants/card";
function ExamplePath({ onCard }: { onCard: (card: string) => void }) {
const data = {
nodes: [
{ idd: 0, name: 'Home' },
{ idd: 1, name: 'Google' },
{ idd: 2, name: 'Facebook' },
{ idd: 3, name: 'Search' },
{ idd: 4, name: 'Product' },
{ idd: 5, name: 'Chart' },
],
links: [
{ source: 0, target: 3, value: 40 },
{ source: 0, target: 4, value: 60 },
function ExamplePath(props: any) {
const data = {
nodes: [
{idd: 0, name: 'Home'},
{idd: 1, name: 'Google'},
{idd: 2, name: 'Facebook'},
{idd: 3, name: 'Search'},
{idd: 4, name: 'Product'},
{idd: 5, name: 'Chart'},
],
links: [
{source: 0, target: 3, value: 40},
{source: 0, target: 4, value: 60},
{ source: 1, target: 3, value: 100 },
{ source: 2, target: 3, value: 100 },
{source: 1, target: 3, value: 100},
{source: 2, target: 3, value: 100},
{ source: 3, target: 4, value: 50 },
{ source: 3, target: 5, value: 50 },
{source: 3, target: 4, value: 50},
{source: 3, target: 5, value: 50},
{ source: 4, target: 5, value: 15 },
],
};
return (
<ExCard
title={'Path Finder'}
onCard={onCard}
type={'path-finder'}
>
<ResponsiveContainer width={'100%'} height={230}>
<Sankey
nodeWidth={6}
sort={false}
iterations={128}
node={<CustomNode isDemo />}
link={(linkProps) => <CustomLink {...linkProps} />}
data={data}
{source: 4, target: 5, value: 15},
],
};
return (
<ExCard
{...props}
>
<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>
</ExCard>
);
<ResponsiveContainer width={'100%'} height={230}>
<Sankey
nodeWidth={6}
sort={false}
iterations={128}
node={<CustomNode />}
link={(linkProps) => <CustomLink {...linkProps} />}
data={data}
>
<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>
</ExCard>
);
}
export default ExamplePath

View file

@ -2,8 +2,9 @@ import { GitCommitHorizontal } from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {PERFORMANCE} from "App/constants/card";
function PerfBreakdown({ onCard }: { onCard: (card: string) => void }) {
function PerfBreakdown(props: any) {
const rows = [
['5K', '1K'],
['4K', '750'],
@ -22,9 +23,7 @@ function PerfBreakdown({ onCard }: { onCard: (card: string) => void }) {
const bgs = ['#E2E4F6', '#A7BFFF', '#394EFF'];
return (
<ExCard
title={'Breakdown'}
onCard={onCard}
type={'perf-breakdown'}
{...props}
>
<div className={'relative'}>
<div className={'flex flex-col gap-4'}>

View file

@ -5,7 +5,7 @@ import { Icon } from 'UI';
import ExCard from '../ExCard';
import ByComponent from './Component';
function ByBrowser({ onCard }: { onCard: (card: string) => void }) {
function ByBrowser(props: any) {
const rows = [
{
label: 'Chrome',
@ -42,9 +42,7 @@ function ByBrowser({ onCard }: { onCard: (card: string) => void }) {
const lineWidth = 200;
return (
<ByComponent
onCard={onCard}
type={'sessions-by-browser'}
title={'Sessions by Browser'}
{...props}
rows={rows}
lineWidth={lineWidth}
/>

View file

@ -4,7 +4,7 @@ import { Icon } from 'UI';
import ByComponent from './Component';
function ByCountry({ onCard }: { onCard: (card: string) => void }) {
function ByCountry(props: any) {
const rows = [
{
label: 'United States',
@ -41,10 +41,8 @@ function ByCountry({ onCard }: { onCard: (card: string) => void }) {
return (
<ByComponent
rows={rows}
title={'Sessions by Country'}
lineWidth={180}
onCard={onCard}
type={'sessions-by-country'}
{...props}
/>
);
}

View file

@ -4,7 +4,7 @@ import { Icon } from 'UI';
import ByComponent from './Component';
function BySystem({ onCard }: { onCard: (card: string) => void }) {
function BySystem(props: any) {
const rows = [
{
label: 'Windows',
@ -41,9 +41,7 @@ function BySystem({ onCard }: { onCard: (card: string) => void }) {
const lineWidth = 200;
return (
<ByComponent
onCard={onCard}
type={'sessions-by-system'}
title={'Sessions by Operating System'}
{...props}
rows={rows}
lineWidth={lineWidth}
/>

View file

@ -5,7 +5,7 @@ import React from 'react';
import { Circle } from '../Count';
import ExCard from '../ExCard';
function ByUrl({ onCard }: { onCard: (card: string) => void }) {
function ByUrl(props: any) {
const [mode, setMode] = React.useState(0);
const rows = [
{
@ -48,11 +48,10 @@ function ByUrl({ onCard }: { onCard: (card: string) => void }) {
const lineWidth = 240;
return (
<ExCard
onCard={onCard}
type={'sessions-by-url'}
{...props}
title={
<div className={'flex gap-2 items-center'}>
<div>Sessions by</div>
<div>{props.title}</div>
<div className={'font-normal'}><Segmented
options={[
{ label: 'URL', value: '0' },

View file

@ -2,16 +2,14 @@ import React from 'react'
import ExCard from "./ExCard";
import { Errors } from "./Count";
function SessionsByErrors({ onCard }: { onCard: (card: string) => void }) {
function SessionsByErrors(props: any) {
return (
<ExCard
onCard={onCard}
type={'sessions-by-errors'}
title={'Sessions by Errors'}
{...props}
>
<Errors />
</ExCard>
);
}
export default SessionsByErrors
export default SessionsByErrors

View file

@ -2,16 +2,14 @@ import React from 'react'
import ExCard from "./ExCard";
import { Frustrations } from "./Count";
function SessionsByIssues({ onCard }: { onCard: (card: string) => void }) {
function SessionsByIssues(props: any) {
return (
<ExCard
onCard={onCard}
type={'sessions-by-issues'}
title={'Sessions by Issues'}
{...props}
>
<Frustrations />
</ExCard>
);
}
export default SessionsByIssues
export default SessionsByIssues

View file

@ -4,7 +4,7 @@ import React from 'react';
import { Circle } from './Count';
import ExCard from './ExCard';
function SlowestDomain({ onCard }: { onCard: (card: string) => void }) {
function SlowestDomain(props: any) {
const rows = [
{
label: 'kroger.com',
@ -42,9 +42,7 @@ function SlowestDomain({ onCard }: { onCard: (card: string) => void }) {
return (
<ExCard
type={'slowest'}
onCard={onCard}
title={'Slowest Domain'}
{...props}
>
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (

View file

@ -0,0 +1,20 @@
import React from 'react';
import PerfBreakdown from '../PerfBreakdown';
import SlowestDomain from '../SlowestDomain';
import SessionsByIssues from '../SessionsByIssues';
import SessionsByErrors from '../SessionsByErrors';
interface ExampleProps {
onCard: (card: string) => void;
}
const CoreWebVitals: React.FC<ExampleProps> = ({onCard}) => (
<>
<PerfBreakdown onCard={onCard}/>
<SlowestDomain onCard={onCard}/>
<SessionsByIssues onCard={onCard}/>
<SessionsByErrors onCard={onCard}/>
</>
);
export default CoreWebVitals;

View file

@ -0,0 +1,20 @@
import React from 'react';
import PerfBreakdown from '../PerfBreakdown';
import SlowestDomain from '../SlowestDomain';
import SessionsByErrors from '../SessionsByErrors';
import SessionsByIssues from '../SessionsByIssues';
interface ExampleProps {
onCard: (card: string) => void;
}
const PerformanceMonitoring: React.FC<ExampleProps> = ({onCard}) => (
<>
<PerfBreakdown onCard={onCard}/>
<SlowestDomain onCard={onCard}/>
<SessionsByErrors onCard={onCard}/>
<SessionsByIssues onCard={onCard}/>
</>
);
export default PerformanceMonitoring;

View file

@ -0,0 +1,20 @@
import React from 'react';
import ExampleFunnel from '../Funnel';
import ExamplePath from '../Path';
import ExampleTrend from '../Trend';
import ExampleCount from '../Count';
interface ExampleProps {
onCard: (card: string) => void;
}
const ProductAnalytics: React.FC<ExampleProps> = ({ onCard }) => (
<>
<ExampleFunnel onCard={onCard} />
<ExamplePath onCard={onCard} />
<ExampleTrend onCard={onCard} />
<ExampleCount onCard={onCard} />
</>
);
export default ProductAnalytics;

View file

@ -0,0 +1,20 @@
import React from 'react';
import ByBrowser from '../SessionsBy/ByBrowser';
import BySystem from '../SessionsBy/BySystem';
import ByCountry from '../SessionsBy/ByCountry';
import ByUrl from '../SessionsBy/ByUrl';
interface ExampleProps {
onCard: (card: string) => void;
}
const WebAnalytics: React.FC<ExampleProps> = ({onCard}) => (
<>
<ByBrowser onCard={onCard}/>
<BySystem onCard={onCard}/>
<ByCountry onCard={onCard}/>
<ByUrl onCard={onCard}/>
</>
);
export default WebAnalytics;

View file

@ -3,18 +3,19 @@ import React from 'react';
import ExCard from './ExCard';
function ExampleTrend({ onCard }: { onCard: (card: string) => void }) {
function ExampleTrend(props: any) {
const rows = [50, 40, 30, 20, 10];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May'];
const [isMulti, setIsMulti] = React.useState(false);
return (
<ExCard
onCard={onCard}
type={'trend' + (isMulti ? '-multi' : '-single')}
{...props}
// onCard={onCard}
// type={'trend' + (isMulti ? '-multi' : '-single')}
title={
<div className={'flex items-center gap-2'}>
<div>Trend</div>
<div>{props.title}</div>
<div className={'font-normal'}>
<Segmented
options={[

View file

@ -0,0 +1,51 @@
import React, {useEffect} from 'react';
import {Modal} from 'antd';
import SelectCard from './SelectCard';
import CreateCard from "Components/Dashboard/components/DashboardList/NewDashModal/CreateCard";
import {useStore} from "App/mstore";
import {TIMESERIES} from "App/constants/card";
interface NewDashboardModalProps {
onClose: () => void;
open: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({onClose, open}) => {
const [step, setStep] = React.useState<number>(0);
const [selectedCard, setSelectedCard] = React.useState<string>('trend-single');
const {metricStore} = useStore();
const onCard = (card: any) => {
setStep(step + 1);
// setSelectedCard(card);
// console.log('Selected card:', card)
console.log('Selected card:', card)
metricStore.merge({
name: card.title,
});
metricStore.changeType(card.cardType);
};
const [modalOpen, setModalOpen] = React.useState<boolean>(false);
useEffect(() => {
return () => {
setStep(1);
}
}, [open]);
return (
<>
<Modal open={open} onCancel={onClose} width={900} destroyOnClose={true} footer={null} closeIcon={false}>
<div>
<div className="flex flex-col gap-4">
{step === 0 && <SelectCard onClose={onClose} onCard={onCard}/>}
{step === 1 && <CreateCard onBack={() => setStep(0)}/>}
</div>
</div>
</Modal>
</>
);
};
export default NewDashboardModal;

View file

@ -0,0 +1,16 @@
import React from 'react';
import {LucideIcon} from "lucide-react";
interface OptionProps {
label: string;
Icon: LucideIcon;
}
const Option: React.FC<OptionProps> = ({label, Icon}) => (
<div className="flex items-center gap-2">
<Icon size={16} strokeWidth={1}/>
<div>{label}</div>
</div>
);
export default Option;

View file

@ -0,0 +1,228 @@
import React, {useMemo} from 'react';
import {Segmented} from 'antd';
import Option from './Option';
// import ProductAnalytics from './Examples/ProductAnalytics';
// import PerformanceMonitoring from './Examples/PerformanceMonitoring';
// import WebAnalytics from './Examples/WebAnalytics';
// import CoreWebVitals from './Examples/CoreWebVitals';
import {TrendingUp, Activity, BarChart, TableCellsMerge} from "lucide-react";
import ExampleFunnel from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/Funnel";
import ExamplePath from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/Path";
import ExampleTrend from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/Trend";
import ExampleCount from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/Count";
import PerfBreakdown from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/PerfBreakdown";
import SlowestDomain from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SlowestDomain";
import SessionsByErrors from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByErrors";
import SessionsByIssues from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsByIssues";
import ByBrowser from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByBrowser";
import BySystem from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/BySystem";
import ByCountry from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByCountry";
import ByUrl from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUrl";
import {ERRORS, FUNNEL, TIMESERIES, USER_PATH} from "App/constants/card";
interface SelectCardProps {
onClose: () => void;
onCard: (card: any) => void;
}
const CARD_CATEGORY = {
PRODUCT_ANALYTICS: 'product-analytics',
PERFORMANCE_MONITORING: 'performance-monitoring',
WEB_ANALYTICS: 'web-analytics',
CORE_WEB_VITALS: 'core-web-vitals',
}
const segmentedOptions = [
{label: 'Product Analytics', Icon: TrendingUp, value: CARD_CATEGORY.PRODUCT_ANALYTICS},
{label: 'Performance Monitoring', Icon: Activity, value: CARD_CATEGORY.PERFORMANCE_MONITORING},
{label: 'Web Analytics', Icon: BarChart, value: CARD_CATEGORY.WEB_ANALYTICS},
{label: 'Core Web Vitals', Icon: TableCellsMerge, value: CARD_CATEGORY.CORE_WEB_VITALS},
];
const TYPE = {
FUNNEL: 'funnel',
PATH_FINDER: 'path-finder',
TREND: 'trend',
SESSIONS_BY: 'sessions-by',
BREAKDOWN: 'breakdown',
SLOWEST_DOMAIN: 'slowest-domain',
SESSIONS_BY_ERRORS: 'sessions-by-errors',
SESSIONS_BY_ISSUES: 'sessions-by-issues',
SESSIONS_BY_BROWSER: 'sessions-by-browser',
SESSIONS_BY_SYSTEM: 'sessions-by-system',
SESSIONS_BY_COUNTRY: 'sessions-by-country',
SESSIONS_BY_URL: 'sessions-by-url',
}
const CARD_TYPE_MAP = {
[TYPE.FUNNEL]: FUNNEL,
[TYPE.PATH_FINDER]: USER_PATH,
[TYPE.TREND]: TIMESERIES,
[TYPE.SESSIONS_BY]: TIMESERIES,
[TYPE.BREAKDOWN]: TIMESERIES,
[TYPE.SLOWEST_DOMAIN]: TIMESERIES,
[TYPE.SESSIONS_BY_ERRORS]: ERRORS,
[TYPE.SESSIONS_BY_ISSUES]: TIMESERIES,
[TYPE.SESSIONS_BY_BROWSER]: TIMESERIES,
[TYPE.SESSIONS_BY_SYSTEM]: TIMESERIES,
[TYPE.SESSIONS_BY_COUNTRY]: TIMESERIES,
[TYPE.SESSIONS_BY_URL]: TIMESERIES,
}
export const CARD_LIST = [
{
title: 'Funnel',
key: TYPE.FUNNEL,
cardType: FUNNEL,
category: CARD_CATEGORY.PRODUCT_ANALYTICS,
example: ExampleFunnel,
},
{
title: 'Path Finder',
key: TYPE.PATH_FINDER,
cardType: USER_PATH,
category: CARD_CATEGORY.PRODUCT_ANALYTICS,
example: ExamplePath,
},
{
title: 'Trend',
key: TYPE.TREND,
cardType: TIMESERIES,
category: CARD_CATEGORY.PRODUCT_ANALYTICS,
example: ExampleTrend,
},
{
title: 'Sessions by',
key: TYPE.SESSIONS_BY,
cardType: TIMESERIES,
category: CARD_CATEGORY.PRODUCT_ANALYTICS,
example: ExampleCount,
},
{
title: 'Breakdown',
key: TYPE.BREAKDOWN,
cardType: TIMESERIES,
category: CARD_CATEGORY.PERFORMANCE_MONITORING,
example: PerfBreakdown,
},
{
title: 'Slowest Domain',
key: TYPE.SLOWEST_DOMAIN,
cardType: TIMESERIES,
category: CARD_CATEGORY.PERFORMANCE_MONITORING,
example: SlowestDomain,
},
{
title: 'Sessions by Errors',
key: TYPE.SESSIONS_BY_ERRORS,
cardType: TIMESERIES,
category: CARD_CATEGORY.PERFORMANCE_MONITORING,
example: SessionsByErrors,
},
{
title: 'Sessions by Issues',
key: TYPE.SESSIONS_BY_ISSUES,
cardType: TIMESERIES,
category: CARD_CATEGORY.PERFORMANCE_MONITORING,
example: SessionsByIssues,
},
{
title: 'Sessions by Browser',
key: TYPE.SESSIONS_BY_BROWSER,
cardType: TIMESERIES,
category: CARD_CATEGORY.WEB_ANALYTICS,
example: ByBrowser,
},
{
title: 'Sessions by System',
key: TYPE.SESSIONS_BY_SYSTEM,
cardType: TIMESERIES,
category: CARD_CATEGORY.WEB_ANALYTICS,
example: BySystem,
},
{
title: 'Sessions by Country',
key: TYPE.SESSIONS_BY_COUNTRY,
cardType: TIMESERIES,
category: CARD_CATEGORY.WEB_ANALYTICS,
example: ByCountry,
},
{
title: 'Sessions by URL',
key: TYPE.SESSIONS_BY_URL,
cardType: TIMESERIES,
category: CARD_CATEGORY.WEB_ANALYTICS,
example: ByUrl,
},
// {
// title: 'Breakdown',
// key: TYPE.BREAKDOWN,
// category: CARD_CATEGORY.CORE_WEB_VITALS,
// example: PerfBreakdown,
// },
// {
// title: 'Slowest Domain',
// key: TYPE.SLOWEST_DOMAIN,
// category: CARD_CATEGORY.CORE_WEB_VITALS,
// example: SlowestDomain,
// },
// {
// title: 'Sessions by Issues',
// key: TYPE.SESSIONS_BY_ISSUES,
// category: CARD_CATEGORY.CORE_WEB_VITALS,
// example: SessionsByIssues,
// },
// {
// title: 'Sessions by Errors',
// key: TYPE.SESSIONS_BY_ISSUES,
// category: CARD_CATEGORY.CORE_WEB_VITALS,
// example: SessionsByErrors,
// },
]
const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
const [selected, setSelected] = React.useState<string>('product-analytics');
// const item = getSelectedItem(selected, onCard);
const onCard = (card: string) => {
const _card = CARD_LIST.find((c) => c.key === card);
props.onCard(_card);
// props.onClose();
}
const item = useMemo(() => {
return CARD_LIST.filter((card) => card.category === selected).map((card) => (
<div key={card.key}>
<card.example onCard={onCard} type={card.key} title={card.title}/>
</div>
));
}, [selected]);
return (
<>
<div className="flex items-center justify-between">
<div className="text-2xl leading-4 font-semibold">
Select your first card type to add to the dashboard
</div>
</div>
<div>
<Segmented
options={segmentedOptions.map(({label, Icon, value}) => ({
label: <Option key={value} label={label} Icon={Icon}/>,
value,
}))}
onChange={setSelected}
/>
</div>
<div className="w-full grid grid-cols-2 gap-4 overflow-scroll"
style={{maxHeight: 'calc(100vh - 210px)'}}>
{item}
</div>
</>
);
};
export default SelectCard;

View file

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

View file

@ -1,174 +0,0 @@
import { Segmented } from 'antd';
import { Activity, BarChart, TableCellsMerge, TrendingUp } from 'lucide-react';
import React from 'react';
import { Modal } from 'UI';
import ExampleCount from './Examples/Count';
import ExampleFunnel from './Examples/Funnel';
import ExamplePath from './Examples/Path';
import PerfBreakdown from './Examples/PerfBreakdown';
import ByBrowser from './Examples/SessionsBy/ByBrowser';
import ByCountry from './Examples/SessionsBy/ByCountry';
import BySystem from './Examples/SessionsBy/BySystem';
import ByUrl from './Examples/SessionsBy/ByUrl';
import SessionsByErrors from './Examples/SessionsByErrors';
import SessionsByIssues from './Examples/SessionsByIssues';
import SlowestDomain from './Examples/SlowestDomain';
import ExampleTrend from './Examples/Trend';
function NewDashboardModal(props: { onClose: () => void; open: boolean }) {
const [step, setStep] = React.useState(0);
const onCard = (card: string) => {
console.log(card);
};
return (
<Modal onClose={props.onClose} open={props.open} size={'xlarge'}>
<Modal.Content className={'bg-[#FAFAFA]'}>
<div className={'flex flex-col gap-4'}>
{step === 0 ? (
<SelectCard onClose={props.onClose} onCard={onCard} />
) : null}
</div>
</Modal.Content>
</Modal>
);
}
function SelectCard({
onClose,
onCard,
}: {
onClose: () => void;
onCard: (card: string) => void;
}) {
const initial = 'product-analytics';
const [selected, setSelected] = React.useState(initial);
let item;
switch (selected) {
case 'product-analytics':
item = <ProductAnalytics onCard={onCard} />;
break;
case 'performance-monitoring':
item = <PerformanceMonitoring onCard={onCard} />;
break;
case 'web-analytics':
item = <WebAnalytics onCard={onCard} />;
break;
case 'core-web-vitals':
item = <CoreWebVitals onCard={onCard} />;
break;
default:
item = <div>under construction</div>;
break;
}
return (
<>
<div className={'flex items-center justify-between'}>
<div className={'text-2xl leading-4 font-semibold'}>
Select your first card type to add to the dashboard
</div>
<div className={'link'} onClick={onClose}>
Close
</div>
</div>
<div>
<Segmented
options={[
{
label: (
<div className={'flex items-center gap-2'}>
<TrendingUp size={16} strokeWidth={1} />
<div>Product Analytics</div>
</div>
),
value: 'product-analytics',
},
{
label: (
<div className={'flex items-center gap-2'}>
<Activity size={16} strokeWidth={1} />
<div>Performance Monitoring</div>
</div>
),
value: 'performance-monitoring',
},
{
label: (
<div className={'flex items-center gap-2'}>
<BarChart size={16} strokeWidth={1} />
<div>Web Analytics</div>
</div>
),
value: 'web-analytics',
},
{
label: (
<div className={'flex items-center gap-2'}>
<TableCellsMerge size={16} strokeWidth={1} />
<div>Core Web Vitals</div>
</div>
),
value: 'core-web-vitals',
},
]}
onChange={(v) => setSelected(v)}
/>
<div
style={{ maxHeight: 'calc(100vh - 210px)' }}
className={'mt-2 w-full flex flex-wrap gap-2 overflow-scroll'}
>
{item}
</div>
</div>
</>
);
}
function ProductAnalytics({ onCard }: { onCard: (card: string) => void }) {
return (
<>
<ExampleFunnel onCard={onCard} />
<ExamplePath onCard={onCard} />
<ExampleTrend onCard={onCard} />
<ExampleCount onCard={onCard} />
</>
);
}
function PerformanceMonitoring({ onCard }: { onCard: (card: string) => void }) {
return (
<>
<PerfBreakdown onCard={onCard} />
<SlowestDomain onCard={onCard} />
<SessionsByErrors onCard={onCard} />
<SessionsByIssues onCard={onCard} />
</>
);
}
function WebAnalytics({ onCard }: { onCard: (card: string) => void }) {
return (
<>
<ByBrowser onCard={onCard} />
<BySystem onCard={onCard} />
<ByCountry onCard={onCard} />
<ByUrl onCard={onCard} />
</>
);
}
function CoreWebVitals({ onCard }: { onCard: (card: string) => void }) {
return (
<>
<PerfBreakdown onCard={onCard} />
<SlowestDomain onCard={onCard} />
<SessionsByIssues onCard={onCard} />
<SessionsByErrors onCard={onCard} />
</>
);
}
export default NewDashboardModal;

View file

@ -0,0 +1,292 @@
import React, {useEffect, useState, useCallback} from 'react';
import {observer} from 'mobx-react-lite';
import {useStore} from 'App/mstore';
import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions';
import {FilterKey} from 'Types/filter/filterType';
import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes';
import {Icon, confirm} from 'UI';
import {Card, Input, Space, Button} from 'antd';
import {AudioWaveform} from "lucide-react";
import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select';
import MetricTypeDropdown from './components/MetricTypeDropdown';
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
import {eventKeys} from 'App/types/filter/newFilter';
import {renderClickmapThumbnail} from './renderMap';
import FilterItem from 'Shared/Filters/FilterItem';
import {
TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING,
PERFORMANCE, WEB_VITALS, INSIGHTS, USER_PATH, RETENTION
} from 'App/constants/card';
import {useParams} from 'react-router-dom';
import {useHistory} from "react-router";
const AIInput = ({value, setValue, placeholder, onEnter}) => (
<Input
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.target.value)}
className='w-full mb-2'
onKeyDown={(e) => e.key === 'Enter' && onEnter()}
/>
);
const PredefinedMessage = () => (
<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>
);
const MetricOptions = ({metric, writeOption}) => {
const isUserPath = metric.metricType === USER_PATH;
return (
<div className='form-group'>
<div className='flex items-center'>
<span className='mr-2'>Card showing</span>
<MetricTypeDropdown onSelect={writeOption}/>
<MetricSubtypeDropdown onSelect={writeOption}/>
{isUserPath && (
<>
<span className='mx-3'></span>
<Select
name='startType'
options={[
{value: 'start', label: 'With Start Point'},
{value: 'end', label: 'With End Point'}
]}
defaultValue={metric.startType}
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
onChange={writeOption}
placeholder='All Issues'
/>
</>
)}
{metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && (
<>
<span className='mx-3'>issue type</span>
<Select
name='metricValue'
options={issueOptions}
value={metric.metricValue}
onChange={writeOption}
isMulti
placeholder='All Issues'
/>
</>
)}
{metric.metricType === INSIGHTS && (
<>
<span className='mx-3'>of</span>
<Select
name='metricValue'
options={issueCategories}
value={metric.metricValue}
onChange={writeOption}
isMulti
placeholder='All Categories'
/>
</>
)}
{metric.metricType === TABLE &&
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
<>
<span className='mx-3'>showing</span>
<Select
name='metricFormat'
options={[{value: 'sessionCount', label: 'Session Count'}]}
defaultValue={metric.metricFormat}
onChange={writeOption}
/>
</>
)}
</div>
</div>
);
};
const PathAnalysisFilter = ({metric}) => (
<div className='form-group flex flex-col'>
{metric.startType === 'start' ? 'Start Point' : 'End Point'}
<FilterItem
hideDelete
filter={metric.startPoint}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
onUpdate={val => metric.updateStartPoint(val)}
onRemoveFilter={() => {
}}
/>
</div>
);
const SeriesList = observer(() => {
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric = metricStore.instance;
const excludeFilterKeys = [CLICKMAP, USER_PATH].includes(metric.metricType) ? eventKeys : [];
const hasSeries = ![TABLE, FUNNEL, CLICKMAP, INSIGHTS, USER_PATH, RETENTION].includes(metric.metricType);
const canAddSeries = metric.series.length < 3;
return (
<div>
{metric.series.length > 0 && metric.series
.slice(0, hasSeries ? metric.series.length : 1)
.map((series, index) => (
<div className='mb-2' key={series.name}>
<FilterSeries
canExclude={metric.metricType === USER_PATH}
supportsEmpty={![CLICKMAP, USER_PATH].includes(metric.metricType)}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={[TABLE, CLICKMAP, INSIGHTS, USER_PATH, FUNNEL].includes(metric.metricType)}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={
metric.metricType === TABLE
? 'Filter data using any event or attribute. Use Add Step button below to do so.'
: 'Add user event or filter to define the series by clicking Add Step.'
}
/>
</div>
))}
{hasSeries && (
<Card styles={{body: {padding: '4px'}}}>
<Button
type='link'
onClick={() => metric.addSeries()}
disabled={!canAddSeries}
size="small"
>
<Space>
<AudioWaveform size={16}/>
New Chart Series
</Space>
</Button>
</Card>
)}
</div>
);
});
interface RouteParams {
siteId: string;
dashboardId: string;
metricId: string;
}
const CardBuilder = observer(() => {
const history = useHistory();
const {siteId, dashboardId} = useParams<RouteParams>();
console.log('siteId', siteId);
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const [aiQuery, setAiQuery] = useState('');
const [aiAskChart, setAiAskChart] = useState('');
const [initialInstance, setInitialInstance] = useState(null);
const metric = metricStore.instance;
const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries');
const tableOptions = metricOf.filter(i => i.type === 'table');
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(metric.metricType);
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
useEffect(() => {
if (metric && !initialInstance) setInitialInstance(metric.toJson());
}, [metric]);
const writeOption = useCallback(({value, name}) => {
value = Array.isArray(value) ? value : value.value;
const obj: any = {[name]: value};
if (name === 'metricType') {
if (value === TIMESERIES) obj.metricOf = timeseriesOptions[0].value;
if (value === TABLE) obj.metricOf = tableOptions[0].value;
}
metricStore.merge(obj);
}, [metricStore, timeseriesOptions, tableOptions]);
const onSave = useCallback(async () => {
const wasCreating = !metric.exists();
if (metric.metricType === CLICKMAP) {
try {
metric.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(metric);
setInitialInstance(metric.toJson());
if (wasCreating) {
const route = parseInt(dashboardId, 10) > 0
? withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
: withSiteId(metricDetails(savedMetric.metricId), siteId);
history.replace(route);
if (parseInt(dashboardId, 10) > 0) {
dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10)),
[savedMetric.metricId]
);
}
}
}, [dashboardId, dashboardStore, history, metric, metricStore, siteId]);
const onDelete = useCallback(async () => {
if (await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: 'Are you sure you want to permanently delete this card?'
})) {
metricStore.delete(metric).then(onDelete);
}
}, [metric, metricStore]);
const undoChanges = useCallback(() => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
}, [initialInstance, metricStore]);
const fetchResults = useCallback(() => aiFiltersStore.getCardFilters(aiQuery, metric.metricType)
.then(f => metric.createSeries(f.filters)), [aiFiltersStore, aiQuery, metric]);
const fetchChartData = useCallback(() => aiFiltersStore.getCardData(aiAskChart, metric.toJson()),
[aiAskChart, aiFiltersStore, metric]);
return (
<div>
<MetricOptions
metric={metric}
writeOption={writeOption}
/>
{metric.metricType === USER_PATH && <PathAnalysisFilter metric={metric}/>}
{isPredefined && <PredefinedMessage/>}
{testingKey && (
<>
<AIInput value={aiQuery} setValue={setAiQuery} placeholder="AI Query" onEnter={fetchResults}/>
<AIInput value={aiAskChart} setValue={setAiAskChart} placeholder="AI Ask Chart"
onEnter={fetchChartData}/>
</>
)}
{aiFiltersStore.isLoading && (
<div>
<div className='flex items-center font-medium py-2'>Loading</div>
</div>
)}
{!isPredefined && <SeriesList/>}
</div>
);
});
export default CardBuilder;