feat ui: dashboards redesign (#2230)

* feat ui: dashboards redesign start

* more cards

* fix ui: more different cards...

* feat ui: finish cards, all trigger, all icons

* change(ui): added missin const

* feature(ui): new dashboard modal

* feature(ui): new dashboard modal

* change(ui): new cards

* change(ui): dashboard redesign

* change(ui): dashboard redesign

* change(ui): dashboard redesign

* change(ui): modal context and alert form

* change(ui): table card show more with modal

* change(ui): examples

* change(ui): example categorize and other improvements

* change(ui): example categorize and other improvements

* change(ui): performance cards

* change(ui): insights card

* Various style updates in dashboards and other pages. (#2308)

* Various minor style updates

* Various style improvements

* Update ExampleCards.tsx

* change(ui): fixed an issue with card create

* change(ui): fixed an issue with card create

* change(ui): default filters and events order

* change(ui): random data

* Dashboards redesign - improvments (#2313)

* Various minor style updates

* Various style improvements

* Update ExampleCards.tsx

* various minor improvements in dashbaords.

* revised dashboard widget header

* change(ui): sessions by user

* change(ui): funnel example

* change(ui): modal height and scroll

* change(ui): example cards with data

* change(ui): example cards with data

* change(ui): funnel bar text color

* change(ui): example cards overlay click

* change(ui): path analysis filter card

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
Co-authored-by: Sudheer Salavadi <connect.uxmaster@gmail.com>
This commit is contained in:
Delirium 2024-06-27 19:47:34 +02:00 committed by GitHub
parent d958549d64
commit d604f9920b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
346 changed files with 15161 additions and 2793 deletions

View file

@ -1,193 +1,196 @@
import React, { useEffect, useRef } from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { connect, ConnectedProps } from 'react-redux';
import { Loader } from 'UI';
import { fetchUserInfo, setJwt } from 'Duck/user';
import { fetchList as fetchSiteList } from 'Duck/site';
import { withStore } from 'App/mstore';
import { Map } from 'immutable';
import React, {useEffect, useRef} from 'react';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {connect, ConnectedProps} from 'react-redux';
import {Loader} from 'UI';
import {fetchUserInfo, setJwt} from 'Duck/user';
import {fetchList as fetchSiteList} from 'Duck/site';
import {withStore} from 'App/mstore';
import {Map} from 'immutable';
import * as routes from './routes';
import { fetchTenants } from 'Duck/user';
import { setSessionPath } from 'Duck/sessions';
import { ModalProvider } from 'Components/Modal';
import { GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM } from 'App/constants/storageKeys';
import {fetchTenants} from 'Duck/user';
import {setSessionPath} from 'Duck/sessions';
import {ModalProvider} from 'Components/Modal';
import {GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM} from 'App/constants/storageKeys';
import PublicRoutes from 'App/PublicRoutes';
import Layout from 'App/layout/Layout';
import { fetchListActive as fetchMetadata } from 'Duck/customField';
import { init as initSite } from 'Duck/site';
import {fetchListActive as fetchMetadata} from 'Duck/customField';
import {init as initSite} from 'Duck/site';
import PrivateRoutes from 'App/PrivateRoutes';
import { checkParam } from 'App/utils';
import {checkParam} from 'App/utils';
import IFrameRoutes from 'App/IFrameRoutes';
import {ModalProvider as NewModalProvider} from 'Components/ModalContext';
interface RouterProps extends RouteComponentProps, ConnectedProps<typeof connector> {
isLoggedIn: boolean;
sites: Map<string, any>;
loading: boolean;
changePassword: boolean;
isEnterprise: boolean;
fetchUserInfo: () => any;
fetchTenants: () => any;
setSessionPath: (path: any) => any;
fetchSiteList: (siteId?: number) => any;
match: {
params: {
siteId: string;
}
};
mstore: any;
setJwt: (jwt: string) => any;
fetchMetadata: (siteId: string) => void;
initSite: (site: any) => void;
isLoggedIn: boolean;
sites: Map<string, any>;
loading: boolean;
changePassword: boolean;
isEnterprise: boolean;
fetchUserInfo: () => any;
fetchTenants: () => any;
setSessionPath: (path: any) => any;
fetchSiteList: (siteId?: number) => any;
match: {
params: {
siteId: string;
}
};
mstore: any;
setJwt: (jwt: string) => any;
fetchMetadata: (siteId: string) => void;
initSite: (site: any) => void;
}
const Router: React.FC<RouterProps> = (props) => {
const {
isLoggedIn,
siteId,
sites,
loading,
location,
fetchUserInfo,
fetchSiteList,
history,
match: { params: { siteId: siteIdFromPath } },
setSessionPath,
} = props;
const [isIframe, setIsIframe] = React.useState(false);
const [isJwt, setIsJwt] = React.useState(false);
const {
isLoggedIn,
siteId,
sites,
loading,
location,
fetchUserInfo,
fetchSiteList,
history,
match: {params: {siteId: siteIdFromPath}},
setSessionPath,
} = props;
const [isIframe, setIsIframe] = React.useState(false);
const [isJwt, setIsJwt] = React.useState(false);
const handleJwtFromUrl = () => {
const urlJWT = new URLSearchParams(location.search).get('jwt');
if (urlJWT) {
props.setJwt(urlJWT);
const handleJwtFromUrl = () => {
const urlJWT = new URLSearchParams(location.search).get('jwt');
if (urlJWT) {
props.setJwt(urlJWT);
}
};
const handleDestinationPath = () => {
if (!isLoggedIn && location.pathname !== routes.login()) {
localStorage.setItem(GLOBAL_DESTINATION_PATH, location.pathname + location.search);
}
};
const handleUserLogin = async () => {
await fetchUserInfo();
const siteIdFromPath = parseInt(location.pathname.split('/')[1]);
await fetchSiteList(siteIdFromPath);
props.mstore.initClient();
const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH);
if (
destinationPath &&
destinationPath !== routes.login() &&
destinationPath !== routes.signup() &&
destinationPath !== '/'
) {
const url = new URL(destinationPath, window.location.origin);
checkParams(url.search)
history.push(destinationPath);
localStorage.removeItem(GLOBAL_DESTINATION_PATH);
}
};
const checkParams = (search?: string) => {
const _isIframe = checkParam('iframe', IFRAME, search);
const _isJwt = checkParam('jwt', JWT_PARAM, search);
setIsIframe(_isIframe);
setIsJwt(_isJwt);
}
};
const handleDestinationPath = () => {
if (!isLoggedIn && location.pathname !== routes.login()) {
localStorage.setItem(GLOBAL_DESTINATION_PATH, location.pathname + location.search);
}
};
const handleUserLogin = async () => {
await fetchUserInfo();
const siteIdFromPath = parseInt(location.pathname.split('/')[1]);
await fetchSiteList(siteIdFromPath);
props.mstore.initClient();
const destinationPath = localStorage.getItem(GLOBAL_DESTINATION_PATH);
if (
destinationPath &&
destinationPath !== routes.login() &&
destinationPath !== routes.signup() &&
destinationPath !== '/'
) {
const url = new URL(destinationPath, window.location.origin);
checkParams(url.search)
history.push(destinationPath);
localStorage.removeItem(GLOBAL_DESTINATION_PATH);
}
};
const checkParams = (search?: string) => {
const _isIframe = checkParam('iframe', IFRAME, search);
const _isJwt = checkParam('jwt', JWT_PARAM, search);
setIsIframe(_isIframe);
setIsJwt(_isJwt);
}
useEffect(() => {
checkParams();
handleJwtFromUrl();
}, []);
useEffect(() => {
// handleJwtFromUrl();
handleDestinationPath();
setSessionPath(previousLocation ? previousLocation : location);
}, [location]);
useEffect(() => {
if (prevIsLoggedIn !== isLoggedIn && isLoggedIn) {
handleUserLogin();
}
}, [isLoggedIn]);
useEffect(() => {
if (siteId && siteId !== lastFetchedSiteIdRef.current) {
const activeSite = sites.find((s) => s.id == siteId);
props.initSite(activeSite);
props.fetchMetadata(siteId);
lastFetchedSiteIdRef.current = siteId;
}
}, [siteId]);
const lastFetchedSiteIdRef = useRef<any>(null);
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
checkParams();
handleJwtFromUrl();
}, []);
const prevIsLoggedIn = usePrevious(isLoggedIn);
const previousLocation = usePrevious(location);
useEffect(() => {
// handleJwtFromUrl();
handleDestinationPath();
const hideHeader = (location.pathname && location.pathname.includes('/session/')) ||
location.pathname.includes('/assist/') || location.pathname.includes('multiview');
if (isIframe) {
return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading} />;
}
setSessionPath(previousLocation ? previousLocation : location);
}, [location]);
return isLoggedIn ? (
<ModalProvider>
<Loader loading={loading || !siteId} className='flex-1'>
<Layout hideHeader={hideHeader} siteId={siteId}>
<PrivateRoutes />
</Layout>
</Loader>
</ModalProvider>
) : <PublicRoutes />;
useEffect(() => {
if (prevIsLoggedIn !== isLoggedIn && isLoggedIn) {
handleUserLogin();
}
}, [isLoggedIn]);
useEffect(() => {
if (siteId && siteId !== lastFetchedSiteIdRef.current) {
const activeSite = sites.find((s) => s.id == siteId);
props.initSite(activeSite);
props.fetchMetadata(siteId);
lastFetchedSiteIdRef.current = siteId;
}
}, [siteId]);
const lastFetchedSiteIdRef = useRef<any>(null);
function usePrevious(value: any) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
const prevIsLoggedIn = usePrevious(isLoggedIn);
const previousLocation = usePrevious(location);
const hideHeader = (location.pathname && location.pathname.includes('/session/')) ||
location.pathname.includes('/assist/') || location.pathname.includes('multiview');
if (isIframe) {
return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading}/>;
}
return isLoggedIn ? (
<NewModalProvider>
<ModalProvider>
<Loader loading={loading || !siteId} className='flex-1'>
<Layout hideHeader={hideHeader} siteId={siteId}>
<PrivateRoutes/>
</Layout>
</Loader>
</ModalProvider>
</NewModalProvider>
) : <PublicRoutes/>;
};
const mapStateToProps = (state: Map<string, any>) => {
const siteId = state.getIn(['site', 'siteId']);
const jwt = state.getIn(['user', 'jwt']);
const changePassword = state.getIn(['user', 'account', 'changePassword']);
const userInfoLoading = state.getIn(['user', 'fetchUserInfoRequest', 'loading']);
const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']);
const siteId = state.getIn(['site', 'siteId']);
const jwt = state.getIn(['user', 'jwt']);
const changePassword = state.getIn(['user', 'account', 'changePassword']);
const userInfoLoading = state.getIn(['user', 'fetchUserInfoRequest', 'loading']);
const sitesLoading = state.getIn(['site', 'fetchListRequest', 'loading']);
return {
siteId,
changePassword,
sites: state.getIn(['site', 'list']),
isLoggedIn: jwt !== null && !changePassword,
loading: siteId === null || userInfoLoading || sitesLoading,
email: state.getIn(['user', 'account', 'email']),
account: state.getIn(['user', 'account']),
organisation: state.getIn(['user', 'account', 'name']),
tenantId: state.getIn(['user', 'account', 'tenantId']),
tenants: state.getIn(['user', 'tenants']),
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
};
return {
siteId,
changePassword,
sites: state.getIn(['site', 'list']),
isLoggedIn: jwt !== null && !changePassword,
loading: siteId === null || userInfoLoading || sitesLoading,
email: state.getIn(['user', 'account', 'email']),
account: state.getIn(['user', 'account']),
organisation: state.getIn(['user', 'account', 'name']),
tenantId: state.getIn(['user', 'account', 'tenantId']),
tenants: state.getIn(['user', 'tenants']),
isEnterprise:
state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'authDetails', 'edition']) === 'ee'
};
};
const mapDispatchToProps = {
fetchUserInfo,
fetchTenants,
setSessionPath,
fetchSiteList,
setJwt,
fetchMetadata,
initSite
fetchUserInfo,
fetchTenants,
setSessionPath,
fetchSiteList,
setJwt,
fetchMetadata,
initSite
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View file

@ -1,382 +1,384 @@
import React, { useEffect } from 'react';
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
import { alertConditions as conditions } from 'App/constants';
import React, {useEffect} from 'react';
import {Form, Input, SegmentSelection, Checkbox, Icon} from 'UI';
import {alertConditions as conditions} from 'App/constants';
import stl from './alertForm.module.css';
import DropdownChips from './DropdownChips';
import { validateEmail } from 'App/validate';
import {validateEmail} from 'App/validate';
import cn from 'classnames';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import {useStore} from 'App/mstore'
import {observer} from 'mobx-react-lite'
import Select from 'Shared/Select';
import {Button} from "antd";
const thresholdOptions = [
{ label: '15 minutes', value: 15 },
{ label: '30 minutes', value: 30 },
{ label: '1 hour', value: 60 },
{ label: '2 hours', value: 120 },
{ label: '4 hours', value: 240 },
{ label: '1 day', value: 1440 },
{label: '15 minutes', value: 15},
{label: '30 minutes', value: 30},
{label: '1 hour', value: 60},
{label: '2 hours', value: 120},
{label: '4 hours', value: 240},
{label: '1 day', value: 1440},
];
const changeOptions = [
{ label: 'change', value: 'change' },
{ label: '% change', value: 'percent' },
{label: 'change', value: 'change'},
{label: '% change', value: 'percent'},
];
const Circle = ({ text }) => (
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
{text}
</div>
const Circle = ({text}) => (
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
{text}
</div>
);
const Section = ({ index, title, description, content }) => (
<div className="w-full">
<div className="flex items-start">
<Circle text={index} />
<div>
<span className="font-medium">{title}</span>
{description && <div className="text-sm color-gray-medium">{description}</div>}
</div>
</div>
const Section = ({index, title, description, content}) => (
<div className="w-full">
<div className="flex items-start">
<Circle text={index}/>
<div>
<span className="font-medium">{title}</span>
{description && <div className="text-sm color-gray-medium">{description}</div>}
</div>
</div>
<div className="ml-10">{content}</div>
</div>
<div className="ml-10">{content}</div>
</div>
);
function AlertForm(props) {
const {
slackChannels,
msTeamsChannels,
webhooks,
onDelete,
style = { width: '580px', height: '100vh' },
} = props;
const { alertsStore } = useStore()
const {
triggerOptions,
loading,
} = alertsStore
const instance = alertsStore.instance
const deleting = loading
const {
slackChannels,
msTeamsChannels,
webhooks,
onDelete,
style = {height: "calc('100vh - 40px')"},
} = props;
const {alertsStore} = useStore()
const {
triggerOptions,
loading,
} = alertsStore
const instance = alertsStore.instance
const deleting = loading
const write = ({ target: { value, name } }) => alertsStore.edit({ [name]: value });
const writeOption = (e, { name, value }) => alertsStore.edit({ [name]: value.value });
const onChangeCheck = ({ target: { checked, name } }) => alertsStore.edit({ [name]: checked });
const write = ({target: {value, name}}) => alertsStore.edit({[name]: value});
const writeOption = (e, {name, value}) => alertsStore.edit({[name]: value.value});
const onChangeCheck = ({target: {checked, name}}) => alertsStore.edit({[name]: checked});
useEffect(() => {
void alertsStore.fetchTriggerOptions();
}, []);
useEffect(() => {
void alertsStore.fetchTriggerOptions();
}, []);
const writeQueryOption = (e, { name, value }) => {
const { query } = instance;
alertsStore.edit({ query: { ...query, [name]: value } });
};
const writeQueryOption = (e, {name, value}) => {
const {query} = instance;
alertsStore.edit({query: {...query, [name]: value}});
};
const writeQuery = ({ target: { value, name } }) => {
const { query } = instance;
alertsStore.edit({ query: { ...query, [name]: value } });
};
const writeQuery = ({target: {value, name}}) => {
const {query} = instance;
alertsStore.edit({query: {...query, [name]: value}});
};
const metric =
instance && instance.query.left
? triggerOptions.find((i) => i.value === instance.query.left)
: null;
const unit = metric ? metric.unit : '';
const isThreshold = instance.detectionMethod === 'threshold';
const metric =
instance && instance.query.left
? triggerOptions.find((i) => i.value === instance.query.left)
: null;
const unit = metric ? metric.unit : '';
const isThreshold = instance.detectionMethod === 'threshold';
return (
<Form
className={cn('p-6 pb-10', stl.wrapper)}
style={style}
onSubmit={() => props.onSubmit(instance)}
id="alert-form"
>
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
<input
autoFocus={true}
className="text-lg border border-gray-light rounded w-full"
name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
value={instance && instance.name}
onChange={write}
placeholder="Untiltled Alert"
id="name-field"
/>
<div className="mb-8" />
<Section
index="1"
title={'What kind of alert do you want to set?'}
content={
<div>
<SegmentSelection
primary
name="detectionMethod"
className="my-3"
onSelect={(e, { name, value }) => alertsStore.edit({ [name]: value })}
value={{ value: instance.detectionMethod }}
list={[
{ name: 'Threshold', value: 'threshold' },
{ name: 'Change', value: 'change' },
]}
/>
<div className="text-sm color-gray-medium">
{isThreshold &&
'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
{!isThreshold &&
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
</div>
<div className="my-4" />
</div>
}
/>
<hr className="my-8" />
<Section
index="2"
title="Condition"
content={
<div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
<Select
className="w-4/6"
placeholder="change"
options={changeOptions}
name="change"
defaultValue={instance.change}
onChange={({ value }) => writeOption(null, { name: 'change', value })}
id="change-dropdown"
/>
</div>
)}
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{isThreshold ? 'Trigger when' : 'of'}
</label>
<Select
className="w-4/6"
placeholder="Select Metric"
isSearchable={true}
options={triggerOptions}
name="left"
value={triggerOptions.find((i) => i.value === instance.query.left)}
// onChange={ writeQueryOption }
onChange={({ value }) =>
writeQueryOption(null, { name: 'left', value: value.value })
}
return (
<Form
className={cn('pb-10', stl.wrapper)}
style={style}
onSubmit={() => props.onSubmit(instance)}
id="alert-form"
>
<div className={cn('-mx-6 px-6 pb-12')}>
<input
autoFocus={true}
className="text-lg border border-gray-light rounded w-full"
name="name"
style={{fontSize: '18px', padding: '10px', fontWeight: '600'}}
value={instance && instance.name}
onChange={write}
placeholder="Untiltled Alert"
id="name-field"
/>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
<div className="w-4/6 flex items-center">
<Select
placeholder="Select Condition"
options={conditions}
name="operator"
defaultValue={instance.query.operator}
// onChange={ writeQueryOption }
onChange={({ value }) =>
writeQueryOption(null, { name: 'operator', value: value.value })
<div className="mb-8"/>
<Section
index="1"
title={'What kind of alert do you want to set?'}
content={
<div>
<SegmentSelection
primary
name="detectionMethod"
className="my-3"
onSelect={(e, {name, value}) => alertsStore.edit({[name]: value})}
value={{value: instance.detectionMethod}}
list={[
{name: 'Threshold', value: 'threshold'},
{name: 'Change', value: 'change'},
]}
/>
<div className="text-sm color-gray-medium">
{isThreshold &&
'Eg. Alert me if memory.avg is greater than 500mb over the past 4 hours.'}
{!isThreshold &&
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
</div>
<div className="my-4"/>
</div>
}
/>
<hr className="my-8"/>
<Section
index="2"
title="Condition"
content={
<div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'Trigger when'}</label>
<Select
className="w-4/6"
placeholder="change"
options={changeOptions}
name="change"
defaultValue={instance.change}
onChange={({value}) => writeOption(null, {name: 'change', value})}
id="change-dropdown"
/>
</div>
)}
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{isThreshold ? 'Trigger when' : 'of'}
</label>
<Select
className="w-4/6"
placeholder="Select Metric"
isSearchable={true}
options={triggerOptions}
name="left"
value={triggerOptions.find((i) => i.value === instance.query.left)}
// onChange={ writeQueryOption }
onChange={({value}) =>
writeQueryOption(null, {name: 'left', value: value.value})
}
/>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'is'}</label>
<div className="w-4/6 flex items-center">
<Select
placeholder="Select Condition"
options={conditions}
name="operator"
defaultValue={instance.query.operator}
// onChange={ writeQueryOption }
onChange={({value}) =>
writeQueryOption(null, {name: 'operator', value: value.value})
}
/>
{unit && (
<>
<Input
className="px-4"
style={{marginRight: '31px'}}
// label={{ basic: true, content: unit }}
// labelPosition='right'
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="E.g. 3"
/>
<span className="ml-2">{'test'}</span>
</>
)}
{!unit && (
<Input
wrapperClassName="ml-2"
// className="pl-4"
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="Specify value"
/>
)}
</div>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="currentPeriod"
defaultValue={instance.currentPeriod}
// onChange={ writeOption }
onChange={({value}) => writeOption(null, {name: 'currentPeriod', value})}
/>
</div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{'compared to previous'}
</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="previousPeriod"
defaultValue={instance.previousPeriod}
// onChange={ writeOption }
onChange={({value}) => writeOption(null, {name: 'previousPeriod', value})}
/>
</div>
)}
</div>
}
/>
<hr className="my-8"/>
<Section
index="3"
title="Notify Through"
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
content={
<div className="flex flex-col">
<div className="flex items-center my-4">
<Checkbox
name="slack"
className="mr-8"
type="checkbox"
checked={instance.slack}
onClick={onChangeCheck}
label="Slack"
/>
<Checkbox
name="msteams"
className="mr-8"
type="checkbox"
checked={instance.msteams}
onClick={onChangeCheck}
label="MS Teams"
/>
<Checkbox
name="email"
type="checkbox"
checked={instance.email}
onClick={onChangeCheck}
className="mr-8"
label="Email"
/>
<Checkbox
name="webhook"
type="checkbox"
checked={instance.webhook}
onClick={onChangeCheck}
label="Webhook"
/>
</div>
{instance.slack && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.slackInput}
options={slackChannels}
placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({slackInput: selected})}
/>
</div>
</div>
)}
{instance.msteams && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'MS Teams'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.msteamsInput}
options={msTeamsChannels}
placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({msteamsInput: selected})}
/>
</div>
</div>
)}
{instance.email && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
<div className="w-4/6">
<DropdownChips
textFiled
validate={validateEmail}
selected={instance.emailInput}
placeholder="Type and press Enter key"
onChange={(selected) => alertsStore.edit({emailInput: selected})}
/>
</div>
</div>
)}
{instance.webhook && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
<DropdownChips
fluid
selected={instance.webhookInput}
options={webhooks}
placeholder="Select Webhook"
onChange={(selected) => alertsStore.edit({webhookInput: selected})}
/>
</div>
)}
</div>
}
/>
{unit && (
<>
<Input
className="px-4"
style={{ marginRight: '31px' }}
// label={{ basic: true, content: unit }}
// labelPosition='right'
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="E.g. 3"
/>
<span className="ml-2">{'test'}</span>
</>
)}
{!unit && (
<Input
wrapperClassName="ml-2"
// className="pl-4"
name="right"
value={instance.query.right}
onChange={writeQuery}
placeholder="Specify value"
/>
)}
</div>
</div>
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">{'over the past'}</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="currentPeriod"
defaultValue={instance.currentPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
/>
</div>
{!isThreshold && (
<div className="flex items-center my-3">
<label className="w-2/6 flex-shrink-0 font-normal">
{'compared to previous'}
</label>
<Select
className="w-2/6"
placeholder="Select timeframe"
options={thresholdOptions}
name="previousPeriod"
defaultValue={instance.previousPeriod}
// onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
/>
</div>
)}
</div>
}
/>
<hr className="my-8" />
<Section
index="3"
title="Notify Through"
description="You'll be noticed in app notifications. Additionally opt in to receive alerts on:"
content={
<div className="flex flex-col">
<div className="flex items-center my-4">
<Checkbox
name="slack"
className="mr-8"
type="checkbox"
checked={instance.slack}
onClick={onChangeCheck}
label="Slack"
/>
<Checkbox
name="msteams"
className="mr-8"
type="checkbox"
checked={instance.msteams}
onClick={onChangeCheck}
label="MS Teams"
/>
<Checkbox
name="email"
type="checkbox"
checked={instance.email}
onClick={onChangeCheck}
className="mr-8"
label="Email"
/>
<Checkbox
name="webhook"
type="checkbox"
checked={instance.webhook}
onClick={onChangeCheck}
label="Webhook"
/>
</div>
{instance.slack && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Slack'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.slackInput}
options={slackChannels}
placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({ slackInput: selected })}
/>
</div>
<div
className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center">
<Button
loading={loading}
type="primary"
htmlType="submit"
disabled={loading || !instance.validate()}
id="submit-button"
>
{instance.exists() ? 'Update' : 'Create'}
</Button>
<div className="mx-1"/>
<Button onClick={props.onClose}>Cancel</Button>
</div>
)}
{instance.msteams && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'MS Teams'}</label>
<div className="w-4/6">
<DropdownChips
fluid
selected={instance.msteamsInput}
options={msTeamsChannels}
placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({ msteamsInput: selected })}
/>
</div>
<div>
{instance.exists() && (
<Button
hover
primary="text"
loading={deleting}
type="button"
onClick={() => onDelete(instance)}
id="trash-button"
>
<Icon name="trash" color="gray-medium" size="18"/>
</Button>
)}
</div>
)}
{instance.email && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Email'}</label>
<div className="w-4/6">
<DropdownChips
textFiled
validate={validateEmail}
selected={instance.emailInput}
placeholder="Type and press Enter key"
onChange={(selected) => alertsStore.edit({ emailInput: selected })}
/>
</div>
</div>
)}
{instance.webhook && (
<div className="flex items-start my-4">
<label className="w-2/6 flex-shrink-0 font-normal pt-2">{'Webhook'}</label>
<DropdownChips
fluid
selected={instance.webhookInput}
options={webhooks}
placeholder="Select Webhook"
onChange={(selected) => alertsStore.edit({ webhookInput: selected })}
/>
</div>
)}
</div>
}
/>
</div>
<div className="flex items-center justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center">
<Button
loading={loading}
variant="primary"
type="submit"
disabled={loading || !instance.validate()}
id="submit-button"
>
{instance.exists() ? 'Update' : 'Create'}
</Button>
<div className="mx-1" />
<Button onClick={props.onClose}>Cancel</Button>
</div>
<div>
{instance.exists() && (
<Button
hover
variant="text"
loading={deleting}
type="button"
onClick={() => onDelete(instance)}
id="trash-button"
>
<Icon name="trash" color="gray-medium" size="18" />
</Button>
)}
</div>
</div>
</Form>
);
</Form>
);
};
export default observer(AlertForm);

View file

@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import { SlideModal } from 'UI';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
import React, {useEffect, useState} from 'react';
import {SlideModal} from 'UI';
import {useStore} from 'App/mstore'
import {observer} from 'mobx-react-lite'
import AlertForm from '../AlertForm';
import { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule';
import { confirm } from 'UI';
import {SLACK, TEAMS, WEBHOOK} from 'App/constants/schedule';
import {confirm} from 'UI';
interface Select {
label: string;
@ -17,9 +17,10 @@ interface Props {
metricId?: number;
onClose?: () => void;
}
function AlertFormModal(props: Props) {
const { alertsStore, settingsStore } = useStore()
const { metricId = null, showModal = false } = props;
const {alertsStore, settingsStore} = useStore()
const {metricId = null, showModal = false} = props;
const [showForm, setShowForm] = useState(false);
const webhooks = settingsStore.webhooks
useEffect(() => {
@ -32,7 +33,7 @@ function AlertFormModal(props: Props) {
const msTeamsChannels: Select[] = []
webhooks.forEach((hook) => {
const option = { value: hook.webhookId, label: hook.name }
const option = {value: hook.webhookId, label: hook.name}
if (hook.type === SLACK) {
slackChannels.push(option)
}

View file

@ -26,7 +26,7 @@ function Recordings(props: Props) {
};
return (
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded py-4 border h-screen overflow-y-scroll'>
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded-lg py-4 border h-screen overflow-y-scroll'>
<div className='flex items-center mb-4 justify-between px-6'>
<div className='flex items-baseline mr-3'>
<PageTitle title='Training Videos' />

View file

@ -29,7 +29,7 @@ function AuditView() {
}
return useObserver(() => (
<div className="bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border">
<div className="flex items-center mb-4 px-5 pt-5">
<PageTitle title={
<div className="flex items-center">

View file

@ -68,7 +68,7 @@ function CustomFields(props) {
const { fields, loading } = props;
return (
<div className="p-5 bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border p-5 ">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}>

View file

@ -108,7 +108,7 @@ function Integrations(props: Props) {
return (
<>
<div className='mb-4 p-5 bg-white rounded-lg border'>
<div className='bg-white rounded-lg border shadow-sm p-5 mb-4'>
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} />
@ -117,15 +117,7 @@ function Integrations(props: Props) {
<div className='mb-4' />
<div className={cn(`
grid
gap-3
auto-cols-max
${allIntegrations.length > 0 ? 'p-2' : ''}
grid-cols-1 // default to 1 column
sm:grid-cols-1 // 1 column on small screens and up
md:grid-cols-2 // 2 columns on medium screens and up
lg:grid-cols-3 // 3 columns on large screens and up
xl:grid-cols-3 // 3 columns on extra-large screens
mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3
`)}>
{allIntegrations.map((integration: any) => (
<IntegrationItem

View file

@ -46,7 +46,7 @@ function Modules(props: Props) {
return (
<div>
<div className='bg-white rounded-lg border p-4'>
<div className='bg-white rounded-lg border shadow-sm p-4'>
<h3 className='text-2xl'>Modules</h3>
<ul className='mt-3 ml-4 list-disc'>
<li>OpenReplay's modules are a collection of advanced features that provide enhanced functionality.</li>
@ -54,7 +54,7 @@ function Modules(props: Props) {
</ul>
</div>
<div className='mt-4 grid grid-cols-3 gap-3'>
<div className='mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
{modulesState.map((module) => (
<div key={module.key} className='flex flex-col h-full'>
<ModuleCard module={module} onToggle={onToggle} />

View file

@ -19,7 +19,7 @@ export default class ProfileSettings extends React.PureComponent {
render() {
const { account, isEnterprise } = this.props;
return (
<div className="bg-white rounded-lg p-5">
<div className="bg-white rounded-lg border shadow-sm p-5">
<PageTitle title={<div>Account</div>} />
<div className="flex items-center">
<div className={styles.left}>

View file

@ -65,7 +65,7 @@ function Roles(props: Props) {
return (
<React.Fragment>
<Loader loading={loading}>
<div className="bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border">
<div className={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>

View file

@ -19,7 +19,7 @@ const connector = connect(mapStateToProps);
function SessionsListingSettings(props: Props) {
return (
<div className='bg-white rounded-lg p-5'>
<div className='bg-white rounded-lg border shadow-sm p-5'>
<PageTitle title={<div>Sessions Listing</div>} />
<div className='flex flex-col mt-4'>

View file

@ -111,7 +111,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
return (
<Loader loading={loading}>
<div className="bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border">
<div className={cn(stl.tabHeader, 'px-5 pt-5')}>
<PageTitle
title={<div className="mr-4">Projects</div>}

View file

@ -36,7 +36,7 @@ function UsersView(props: Props) {
}, []);
return (
<div className="bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border">
<div className="flex items-center justify-between px-5 pt-5">
<PageTitle
title={

View file

@ -44,7 +44,7 @@ function Webhooks() {
};
return (
<div className="p-5 bg-white rounded-lg">
<div className="bg-white rounded-lg shadow-sm border p-5">
<div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>

View file

@ -0,0 +1,60 @@
import React from 'react';
import { List, Progress, Typography } from "antd";
import cn from "classnames";
interface Props {
list: any;
selected: any;
onClickHandler: (event: any, data: any) => void;
}
function CardSessionsByList({ list, selected, onClickHandler }: Props) {
return (
<List
dataSource={list}
split={false}
renderItem={(row: any) => (
<List.Item
key={row.name}
onClick={(e) => onClickHandler(e, row)} // Remove onClick handler to disable click interaction
style={{
borderBottom: '1px dotted rgba(0, 0, 0, 0.05)',
padding: '4px 10px',
lineHeight: '1px'
}}
className={cn('rounded', selected === row.name ? 'bg-active-blue' : '')} // Remove hover:bg-active-blue and cursor-pointer
>
<List.Item.Meta
className="m-0"
avatar={row.icon ? row.icon : null}
title={(
<div className="m-0">
<div className="flex justify-between m-0 p-0">
<Typography.Text>{row.name}</Typography.Text>
<Typography.Text type="secondary"> {row.sessionCount}</Typography.Text>
</div>
<Progress
percent={row.progress}
showInfo={false}
strokeColor={{
'0%': '#394EFF',
'100%': '#394EFF',
}}
size={['small', 2]}
style={{
padding: '0 0px',
margin: '0 0px',
height: 4
}}
/>
</div>
)}
/>
</List.Item>
)}
/>
);
}
export default CardSessionsByList;

View file

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

View file

@ -1,7 +1,7 @@
import React from 'react'
import { Styles } from '../../common';
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
import { LineChart, Line, Legend } from 'recharts';
import {Styles} from '../../common';
import {ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts';
import {LineChart, Line, Legend} from 'recharts';
interface Props {
data: any;
@ -9,46 +9,56 @@ interface Props {
// seriesMap: any;
colors: any;
onClick?: (event, index) => void;
yaxis?: any;
label?: string;
}
function CustomMetriLineChart(props: Props) {
const { data = { chart: [], namesMap: [] }, params, colors, onClick = () => null } = props;
function CustomMetricLineChart(props: Props) {
const {
data = {chart: [], namesMap: []},
params,
colors,
onClick = () => null,
yaxis = {...Styles.yaxis},
label = 'Number of Sessions'
} = props;
return (
<ResponsiveContainer height={ 240 } width="100%">
<ResponsiveContainer height={240} width="100%">
<LineChart
data={ data.chart }
data={data.chart}
margin={Styles.chartMargins}
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
onClick={onClick}
// isAnimationActive={ false }
>
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis
{...Styles.xaxis}
dataKey="time"
interval={params.density/7}
interval={params.density / 7}
/>
<YAxis
{...Styles.yaxis}
<YAxis
{...yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{
label={{
...Styles.axisLabelLeft,
value: "Number of Sessions"
value: label || "Number of Sessions"
}}
/>
<Legend />
<Legend/>
<Tooltip {...Styles.tooltip} />
{ Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
{Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
<Line
key={key}
name={key}
type="monotone"
dataKey={key}
stroke={colors[index]}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.6 }
fillOpacity={1}
strokeWidth={2}
strokeOpacity={0.6}
// fill="url(#colorCount)"
dot={false}
/>
@ -58,4 +68,4 @@ function CustomMetriLineChart(props: Props) {
)
}
export default CustomMetriLineChart
export default CustomMetricLineChart

View file

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

View file

@ -1,51 +1,52 @@
import React from 'react'
import { Styles } from '../../common';
import { AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip } from 'recharts';
import {Styles} from '../../common';
import {AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip} from 'recharts';
import CountBadge from '../../common/CountBadge';
import { numberWithCommas } from 'App/utils';
import {numberWithCommas} from 'App/utils';
interface Props {
data: any;
}
function CustomMetricOverviewChart(props: Props) {
const { data } = props;
const {data} = props;
const gradientDef = Styles.gradientDef();
return (
<div className="relative -mx-4">
<div className="absolute flex items-start flex-col justify-start inset-0 p-3">
<div className="mb-2 flex items-center" >
<div className="mb-2 flex items-center">
</div>
<div className="flex items-center">
<CountBadge
// title={subtext}
count={ countView(Math.round(data.value), data.unit) }
change={ data.progress || 0 }
unit={ data.unit }
count={countView(Math.round(data.value), data.unit)}
change={data.progress || 0}
unit={data.unit}
// className={textClass}
/>
</div>
</div>
<ResponsiveContainer height={ 100 } width="100%">
<ResponsiveContainer height={100} width="100%">
<AreaChart
data={ data.chart }
margin={ {
data={data.chart}
margin={{
top: 50, right: 0, left: 0, bottom: 0,
} }
}}
>
{gradientDef}
<Tooltip {...Styles.tooltip} />
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
<YAxis hide interval={ 0 } />
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time"/>
<YAxis hide interval={0}/>
<Area
name={''}
// unit={unit && ' ' + unit}
type="monotone"
dataKey="value"
stroke={Styles.strokeColor}
fillOpacity={ 1 }
strokeWidth={ 2 }
strokeOpacity={ 0.8 }
fillOpacity={1}
strokeWidth={2}
strokeOpacity={0.8}
fill={'url(#colorCount)'}
/>
</AreaChart>
@ -57,16 +58,16 @@ function CustomMetricOverviewChart(props: Props) {
export default CustomMetricOverviewChart
const countView = (avg: any, unit: any) => {
const countView = (avg: any, unit: any) => {
if (unit === 'mb') {
if (!avg) return 0;
const count = Math.trunc(avg / 1024 / 1024);
return numberWithCommas(count);
if (!avg) return 0;
const count = Math.trunc(avg / 1024 / 1024);
return numberWithCommas(count);
}
if (unit === 'min') {
if (!avg) return 0;
const count = Math.trunc(avg);
return numberWithCommas(count > 1000 ? count +'k' : count);
if (!avg) return 0;
const count = Math.trunc(avg);
return numberWithCommas(count > 1000 ? count + 'k' : count);
}
return avg ? numberWithCommas(avg): 0;
}
return avg ? numberWithCommas(avg) : 0;
}

View file

@ -1,24 +1,25 @@
import React from 'react'
import { Table } from '../../common';
import { List } from 'immutable';
import { filtersMap } from 'Types/filter/newFilter';
import { NoContent, Icon } from 'UI';
import { tableColumnName } from 'App/constants/filterOptions';
import { numberWithCommas } from 'App/utils';
import {Table} from '../../common';
import {List} from 'immutable';
import {filtersMap} from 'Types/filter/newFilter';
import {NoContent, Icon} from 'UI';
import {tableColumnName} from 'App/constants/filterOptions';
import {numberWithCommas} from 'App/utils';
const getColumns = (metric) => {
return [
{
key: 'name',
title: tableColumnName[metric.metricOf],
toText: name => name || 'Unidentified',
width: '70%',
key: 'name',
title: tableColumnName[metric.metricOf],
toText: name => name || 'Unidentified',
width: '70%',
icon: true,
},
{
key: 'sessionCount',
title: 'Sessions',
toText: sessions => numberWithCommas(sessions),
width: '30%',
key: 'sessionCount',
title: 'Sessions',
toText: sessions => numberWithCommas(sessions),
width: '30%',
},
]
}
@ -29,13 +30,14 @@ interface Props {
onClick?: (filters: any) => void;
isTemplate?: boolean;
}
function CustomMetricTable(props: Props) {
const { metric = {}, data = { values: [] }, onClick = () => null, isTemplate } = props;
const {metric = {}, data = {values: []}, onClick = () => null, isTemplate} = props;
const rows = List(data.values);
const onClickHandler = (event: any, data: any) => {
const filters = Array<any>();
let filter = { ...filtersMap[metric.metricOf] }
let filter = {...filtersMap[metric.metricOf]}
filter.value = [data.name]
filter.type = filter.key
delete filter.key
@ -49,28 +51,28 @@ function CustomMetricTable(props: Props) {
onClick(filters);
}
return (
<div className="" style={{ height: 240 }}>
<NoContent
style={{ minHeight: 220 }}
show={data.values && data.values.length === 0}
size="small"
title={
<div className="flex items-center">
<Icon name="info-circle" className="mr-2" size="18" />
No data for the selected time period
</div>
}
>
<Table
small
cols={getColumns(metric)}
rows={rows}
rowClass="group"
onRowClick={onClickHandler}
isTemplate={isTemplate}
/>
</NoContent>
</div>
<div className="" style={{height: 240}}>
<NoContent
style={{minHeight: 220}}
show={data.values && data.values.length === 0}
size="small"
title={
<div className="flex items-center">
<Icon name="info-circle" className="mr-2" size="18"/>
No data for the selected time period
</div>
}
>
<Table
small
cols={getColumns(metric)}
rows={rows}
rowClass="group"
onRowClick={onClickHandler}
isTemplate={isTemplate}
/>
</NoContent>
</div>
);
}

View file

@ -0,0 +1,88 @@
import React from 'react';
import {Button, Space} from 'antd';
import {filtersMap} from 'Types/filter/newFilter';
import {Icon} from 'UI';
import {Empty} from 'antd';
import {ArrowRight} from "lucide-react";
import CardSessionsByList from "Components/Dashboard/Widgets/CardSessionsByList";
import {useModal} from "Components/ModalContext";
interface Props {
metric?: any;
data: any;
onClick?: (filters: any) => void;
isTemplate?: boolean;
}
function SessionsBy(props: Props) {
const {metric = {}, data = {values: []}, onClick = () => null, isTemplate} = props;
const [selected, setSelected] = React.useState<any>(null);
const total = data.values.length
const {openModal, closeModal} = useModal();
const onClickHandler = (event: any, data: any) => {
const filters = Array<any>();
let filter = {...filtersMap[metric.metricOf]};
filter.value = [data.name];
filter.type = filter.key;
delete filter.key;
delete filter.operatorOptions;
delete filter.category;
delete filter.icon;
delete filter.label;
delete filter.options;
setSelected(data.name)
filters.push(filter);
onClick(filters);
}
const showMore = () => {
openModal(
<CardSessionsByList list={data.values} onClickHandler={(e, item) => {
closeModal();
onClickHandler(null, item)
}} selected={selected}/>, {
title: metric.name,
width: 600,
})
}
return (
<div>
{data.values && data.values.length === 0 ? (
<Empty
image={null}
style={{minHeight: 220}}
className="flex flex-col items-center justify-center"
imageStyle={{height: 60}}
description={
<div className="flex items-center justify-center">
<Icon name="info-circle" className="mr-2" size="18"/>
No data for the selected time period
</div>
}
/>
) : (
<div className="flex flex-col justify-between w-full" style={{height: 220}}>
<CardSessionsByList list={data.values.slice(0, 3)}
selected={selected}
onClickHandler={onClickHandler}/>
{total > 3 && (
<div className="flex">
<Button type="link" onClick={showMore}>
<Space>
{total - 3} more
<ArrowRight size={16}/>
</Space>
</Button>
</div>
)}
</div>
)}
</div>
);
}
export default SessionsBy;

View file

@ -46,30 +46,28 @@ const cols = [
interface Props {
data: any
metric?: any
isTemplate?: boolean
}
function CallWithErrors(props: Props) {
const { data, metric } = props;
const { data } = props;
const [search, setSearch] = React.useState('')
const test = (value = '', serach: any) => getRE(serach, 'i').test(value);
const _data = search ? metric.data.chart.filter((i: any) => test(i.urlHostpath, search)) : metric.data.chart;
const _data = search ? data.chart.filter((i: any) => test(i.urlHostpath, search)) : data.chart;
const write = ({ target: { name, value } }: any) => {
setSearch(value)
};
return (
<NoContent
size="small"
title={NO_METRIC_DATA}
show={ metric.data.chart.length === 0 }
show={ data.chart.length === 0 }
style={{ height: '240px'}}
>
<div style={{ height: '240px'}}>
<div className={ cn(stl.topActions, 'py-3 flex text-right')}>
<input disabled={metric.data.chart.length === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={write} />
<input disabled={data.chart.length === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={write} />
</div>
<Table
small

View file

@ -7,21 +7,20 @@ import { NO_METRIC_DATA } from 'App/constants/messages'
interface Props {
data: any
metric?: any
}
function ErrorsPerDomain(props: Props) {
const { data, metric } = props;
const { data } = props;
// const firstAvg = 10;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].errorsCount;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
show={ data.chart.length === 0 }
style={{ height: '240px'}}
title={NO_METRIC_DATA}
>
<div className="w-full" style={{ height: '240px' }}>
{metric.data.chart.map((item, i) =>
{data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"

View file

@ -1,5 +1,6 @@
import React from 'react';
import { numberWithCommas } from 'App/utils';
import {numberWithCommas} from 'App/utils';
const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1'];
const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72'];
const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
@ -9,81 +10,81 @@ const customMetricColors = ['#3EAAAF', '#394EFF', '#565D97'];
const colorsPie = colors.concat(["#DDDDDD"]);
const countView = count => {
const isMoreThanK = count >= 1000;
return numberWithCommas(isMoreThanK ? Math.trunc(count / 1000) + 'k' : count);
const isMoreThanK = count >= 1000;
return numberWithCommas(isMoreThanK ? Math.trunc(count / 1000) + 'k' : count);
}
export default {
customMetricColors,
colors,
colorsTeal,
colorsPie,
colorsx,
compareColors,
compareColorsx,
lineColor: '#2A7B7F',
lineColorCompare: '#394EFF',
strokeColor: colors[2],
xaxis: {
axisLine: { stroke: '#CCCCCC' },
interval: 0,
dataKey: "time",
tick: { fill: '#999999', fontSize: 9 },
tickLine: { stroke: '#CCCCCC' },
strokeWidth: 0.5
},
yaxis: {
axisLine: { stroke: '#CCCCCC' },
tick: { fill: '#999999', fontSize: 9 },
tickLine: { stroke: '#CCCCCC' },
},
axisLabelLeft: {
angle: -90,
fill: '#999999',
offset: 10,
style: { textAnchor: 'middle' },
position: 'insideLeft',
fontSize: 11
},
tickFormatter: val => `${countView(val)}`,
tickFormatterBytes: val => Math.round(val / 1024 / 1024),
chartMargins: { left: 0, right: 20, top: 10, bottom: 5 },
tooltip: {
cursor: {
fill: '#f6f6f6'
customMetricColors,
colors,
colorsTeal,
colorsPie,
colorsx,
compareColors,
compareColorsx,
lineColor: '#2A7B7F',
lineColorCompare: '#394EFF',
strokeColor: compareColors[2],
xaxis: {
axisLine: {stroke: '#CCCCCC'},
interval: 0,
dataKey: "time",
tick: {fill: '#999999', fontSize: 9},
tickLine: {stroke: '#CCCCCC'},
strokeWidth: 0.5
},
contentStyle: {
padding: '5px',
background: 'white',
border: '1px solid #DDD',
borderRadius: '3px',
lineHeight: '1.25rem',
color: '#888',
fontSize: '10px'
yaxis: {
axisLine: {stroke: '#CCCCCC'},
tick: {fill: '#999999', fontSize: 9},
tickLine: {stroke: '#CCCCCC'},
},
labelStyle: {},
formatter: (value, name, { unit }) => {
if (unit && unit.trim() === 'mb') {
return numberWithCommas(Math.round(value / 1024 / 1024))
}
return numberWithCommas(Math.round(value))
axisLabelLeft: {
angle: -90,
fill: '#999999',
offset: 10,
style: {textAnchor: 'middle'},
position: 'insideLeft',
fontSize: 11
},
itemStyle: {
lineHeight: '0.75rem',
color: '#000',
fontSize: '12px'
}
},
gradientDef: () => (
<defs>
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={colors[2]} stopOpacity={ 0.5 } />
<stop offset="95%" stopColor={colors[2]} stopOpacity={ 0.2 } />
</linearGradient>
<linearGradient id="colorCountCompare" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={compareColors[4]} stopOpacity={ 0.9 } />
<stop offset="95%" stopColor={compareColors[4]} stopOpacity={ 0.2 } />
</linearGradient>
</defs>
)
};
tickFormatter: val => `${countView(val)}`,
tickFormatterBytes: val => Math.round(val / 1024 / 1024),
chartMargins: {left: 0, right: 20, top: 10, bottom: 5},
tooltip: {
cursor: {
fill: '#f6f6f6'
},
contentStyle: {
padding: '5px',
background: 'white',
border: '1px solid #DDD',
borderRadius: '3px',
lineHeight: '1.25rem',
color: '#888',
fontSize: '10px'
},
labelStyle: {},
formatter: (value, name, {unit}) => {
if (unit && unit.trim() === 'mb') {
return numberWithCommas(Math.round(value / 1024 / 1024))
}
return numberWithCommas(Math.round(value))
},
itemStyle: {
lineHeight: '0.75rem',
color: '#000',
fontSize: '12px'
}
},
gradientDef: () => (
<defs>
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={compareColors[2]} stopOpacity={0.5}/>
<stop offset="95%" stopColor={compareColors[2]} stopOpacity={0.2}/>
</linearGradient>
<linearGradient id="colorCountCompare" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={compareColors[4]} stopOpacity={0.9}/>
<stop offset="95%" stopColor={compareColors[4]} stopOpacity={0.2}/>
</linearGradient>
</defs>
)
};

View file

@ -0,0 +1,62 @@
import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {Grid2x2CheckIcon, Plus} from "lucide-react";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
import {useStore} from "App/mstore";
interface Props {
open: boolean;
onClose?: () => void;
}
function AddCardSelectionModal(props: Props) {
const {metricStore} = useStore();
const [open, setOpen] = React.useState(false);
const [isLibrary, setIsLibrary] = React.useState(false);
const onCloseModal = () => {
setOpen(false);
props.onClose && props.onClose();
}
const onClick = (isLibrary: boolean) => {
if (!isLibrary) {
metricStore.init();
}
setIsLibrary(isLibrary);
setOpen(true);
}
return (
<>
<Modal
title="Add card to dashboard"
open={props.open}
footer={null}
onCancel={props.onClose}
>
<Row gutter={16} justify="center">
<Col span={12}>
<Card hoverable onClick={() => onClick(true)}>
<div className="flex flex-col items-center justify-center" style={{height: '80px'}}>
<Grid2x2CheckIcon style={{fontSize: '24px', color: '#394EFF'}}/>
<Typography.Text strong>Add from library</Typography.Text>
{/*<p>Select from 12 available</p>*/}
</div>
</Card>
</Col>
<Col span={12}>
<Card hoverable onClick={() => onClick(false)}>
<div className="flex flex-col items-center justify-center" style={{height: '80px'}}>
<Plus style={{fontSize: '24px', color: '#394EFF'}}/>
<Typography.Text strong>Create New Card</Typography.Text>
</div>
</Card>
</Col>
</Row>
</Modal>
<NewDashboardModal open={open} onClose={onCloseModal} isAddingFromLibrary={isLibrary}/>
</>
);
}
export default AddCardSelectionModal;

View file

@ -0,0 +1,64 @@
import React from 'react';
import {Grid2x2Check} from "lucide-react"
import {Button, Modal} from "antd";
import Select from "Shared/Select/Select";
import {Form} from "UI";
import {useStore} from "App/mstore";
interface Props {
metricId: string;
}
function AddToDashboardButton({metricId}: Props) {
const {dashboardStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
value: i.dashboardId,
}));
const [selectedId, setSelectedId] = React.useState(dashboardOptions[0].value);
const onSave = (close: any) => {
const dashboard = dashboardStore.getDashboard(selectedId)
if (dashboard) {
dashboardStore.addWidgetToDashboard(dashboard, [metricId]).then(close)
}
}
const onClick = () => {
Modal.confirm({
title: 'Add to selected dashboard',
icon: null,
content: (
<Form.Field>
<Select
options={dashboardOptions}
defaultValue={dashboardOptions[0].value}
onChange={({value}: any) => setSelectedId(value.value)}
/>
</Form.Field>
),
cancelText: 'Cancel',
onOk: onSave,
okText: 'Add',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
})
}
return (
<Button
type="default"
onClick={onClick}
icon={<Grid2x2Check size={18}/>}
>
Add to Dashboard
</Button>
);
}
export default AddToDashboardButton;

View file

@ -26,7 +26,7 @@ function AlertsView({ siteId }: IAlertsView) {
return unmount;
}, [history]);
return (
<div style={{ maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded py-4 border">
<div style={{ maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded-lg shadow-sm py-4 border">
<div className="flex items-center mb-4 justify-between px-6">
<div className="flex items-baseline mr-3">
<PageTitle title="Alerts" />

View file

@ -0,0 +1,32 @@
import React from "react";
import {Tooltip} from "UI";
import {Button} from "antd";
import { PlusOutlined } from '@ant-design/icons';
import AddCardSelectionModal from "Components/Dashboard/components/AddCardSelectionModal";
import {useStore} from "App/mstore";
const MAX_CARDS = 29;
function CreateCardButton() {
const [open, setOpen] = React.useState(false);
const {dashboardStore} = useStore();
const dashboard: any = dashboardStore.selectedDashboard;
const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS;
return <>
<Tooltip delay={0} disabled={canAddMore}
title="The number of cards in one dashboard is limited to 30.">
<Button
type="primary"
disabled={!canAddMore}
onClick={() => setOpen(true)}
icon={<PlusOutlined />}
>
Add Card
</Button>
</Tooltip>
<AddCardSelectionModal open={open} onClose={() => setOpen(false)}/>
</>;
}
export default CreateCardButton;

View file

@ -0,0 +1,25 @@
import React from "react";
import {PlusOutlined} from "@ant-design/icons";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
import {Button} from "antd";
interface Props {
disabled?: boolean;
}
function CreateDashboardButton({disabled = false}: Props) {
const [showModal, setShowModal] = React.useState(false);
return <>
<Button
icon={<PlusOutlined/>}
type="primary"
onClick={() => setShowModal(true)}
>
Create Dashboard
</Button>
<NewDashboardModal onClose={() => setShowModal(false)} open={showModal}/>
</>;
}
export default CreateDashboardButton;

View file

@ -57,7 +57,7 @@ function DashboardEditModal(props: Props) {
/>
</Form.Field>
<Form.Field>
{/* <Form.Field>
<label>{'Description:'}</label>
<Input
className=""
@ -69,7 +69,7 @@ function DashboardEditModal(props: Props) {
maxLength={300}
autoFocus={!focusTitle}
/>
</Form.Field>
</Form.Field> */}
<Form.Field>
<div className="flex items-center">

View file

@ -1,134 +1,128 @@
import React from 'react';
import Breadcrumb from 'Shared/Breadcrumb';
import { withSiteId } from 'App/routes';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { Button, PageTitle, confirm, Tooltip } from 'UI';
import {withSiteId} from 'App/routes';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {Button, PageTitle, confirm, Tooltip} from 'UI';
import SelectDateRange from 'Shared/SelectDateRange';
import { useStore } from 'App/mstore';
import { useModal } from 'App/components/Modal';
import {useStore} from 'App/mstore';
import {useModal} from 'App/components/Modal';
import DashboardOptions from '../DashboardOptions';
import withModal from 'App/components/Modal/withModal';
import { observer } from 'mobx-react-lite';
import {observer} from 'mobx-react-lite';
import DashboardEditModal from '../DashboardEditModal';
import AddCardModal from '../AddCardModal';
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import CreateCard from "Components/Dashboard/components/DashboardList/NewDashModal/CreateCard";
import CreateCardButton from "Components/Dashboard/components/CreateCardButton";
interface IProps {
dashboardId: string;
siteId: string;
renderReport?: any;
dashboardId: string;
siteId: string;
renderReport?: any;
}
type Props = IProps & RouteComponentProps;
const MAX_CARDS = 29;
function DashboardHeader(props: Props) {
const { siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { showModal } = useModal();
const [focusTitle, setFocusedInput] = React.useState(true);
const [showEditModal, setShowEditModal] = React.useState(false);
const period = dashboardStore.period;
const {siteId, dashboardId} = props;
const {dashboardStore} = useStore();
const {showModal} = useModal();
const [focusTitle, setFocusedInput] = React.useState(true);
const [showEditModal, setShowEditModal] = React.useState(false);
const period = dashboardStore.period;
const dashboard: any = dashboardStore.selectedDashboard;
const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS;
const dashboard: any = dashboardStore.selectedDashboard;
const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS;
const onEdit = (isTitle: boolean) => {
dashboardStore.initDashboard(dashboard);
setFocusedInput(isTitle);
setShowEditModal(true);
};
const onEdit = (isTitle: boolean) => {
dashboardStore.initDashboard(dashboard);
setFocusedInput(isTitle);
setShowEditModal(true);
};
const onDelete = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
})
) {
dashboardStore.deleteDashboard(dashboard).then(() => {
props.history.push(withSiteId(`/dashboard`, siteId));
});
}
};
return (
<div>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
focusTitle={focusTitle}
/>
<Breadcrumb
items={[
{
label: 'Dashboards',
to: withSiteId('/dashboard', siteId),
},
{ label: (dashboard && dashboard.name) || '' },
]}
/>
<div className="flex items-center mb-2 justify-between">
<div className="flex items-center" style={{ flex: 3 }}>
<PageTitle
title={
// @ts-ignore
<Tooltip delay={100} arrow title="Double click to edit">
{dashboard?.name}
</Tooltip>
}
onDoubleClick={() => onEdit(true)}
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
/>
</div>
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
<Tooltip delay={0} disabled={canAddMore} title="The number of cards in one dashboard is limited to 30.">
<Button
disabled={!canAddMore}
variant="primary"
onClick={() =>
showModal(<AddCardModal dashboardId={dashboardId} siteId={siteId} />, { right: true })
}
icon="plus"
iconSize={24}
>
Add Card
</Button>
</Tooltip>
<div className="mx-4"></div>
<div
className="flex items-center flex-shrink-0 justify-end"
style={{ width: 'fit-content' }}
>
<SelectDateRange
style={{ width: '300px' }}
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
const onDelete = async () => {
if (
await confirm({
header: 'Delete Dashboard',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
})
) {
dashboardStore.deleteDashboard(dashboard).then(() => {
props.history.push(withSiteId(`/dashboard`, siteId));
});
}
};
return (
<div>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
focusTitle={focusTitle}
/>
</div>
<div className="mx-4" />
<div className="flex items-center flex-shrink-0">
<DashboardOptions
editHandler={onEdit}
deleteHandler={onDelete}
renderReport={props.renderReport}
isTitlePresent={!!dashboard?.description}
<Breadcrumb
items={[
{
label: 'Dashboards',
to: withSiteId('/dashboard', siteId),
},
{label: (dashboard && dashboard.name) || ''},
]}
/>
</div>
<div className="flex items-center mb-2 justify-between">
<div className="flex items-center" style={{flex: 3}}>
<PageTitle
title={
// @ts-ignore
<Tooltip delay={100} arrow title="Double click to edit">
{dashboard?.name}
</Tooltip>
}
onDoubleClick={() => onEdit(true)}
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
/>
</div>
<div className="flex items-center gap-2" style={{flex: 1, justifyContent: 'end'}}>
<CreateCardButton disabled={canAddMore} />
<div
className="flex items-center flex-shrink-0 justify-end dashboardDataPeriodSelector"
style={{width: 'fit-content'}}
>
<SelectDateRange
style={{width: '300px'}}
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
isAnt={true}
useButtonStyle={true}
/>
</div>
<div className="flex items-center flex-shrink-0">
<DashboardOptions
editHandler={onEdit}
deleteHandler={onDelete}
renderReport={props.renderReport}
isTitlePresent={!!dashboard?.description}
/>
</div>
</div>
</div>
<div className="pb-4">
{/* @ts-ignore */}
<Tooltip delay={100} arrow title="Double click to edit" className="w-fit !block">
<h2
className="my-2 font-normal w-fit text-disabled-text border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
onDoubleClick={() => onEdit(false)}
>
{/* {dashboard?.description || 'Describe the purpose of this dashboard'} */}
</h2>
</Tooltip>
</div>
</div>
</div>
<div className="pb-4">
{/* @ts-ignore */}
<Tooltip delay={100} arrow title="Double click to edit" className="w-fit !block">
<h2
className="my-2 font-normal w-fit text-disabled-text border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
onDoubleClick={() => onEdit(false)}
>
{dashboard?.description || 'Describe the purpose of this dashboard'}
</h2>
</Tooltip>
</div>
</div>
);
);
}
export default withRouter(withModal(observer(DashboardHeader)));

View file

@ -1,67 +1,122 @@
import { observer } from 'mobx-react-lite';
import {LockOutlined, TeamOutlined} from '@ant-design/icons';
import {Empty, Switch, Table, TableColumnsType, Tag, Tooltip, Typography} from 'antd';
import {observer} from 'mobx-react-lite';
import React from 'react';
import { NoContent, Pagination } from 'UI';
import { useStore } from 'App/mstore';
import { sliceListPerPage } from 'App/utils';
import DashboardListItem from './DashboardListItem';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { Tooltip } from 'antd';
import {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
function DashboardList() {
const { dashboardStore } = useStore();
const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.filter.query;
const lenth = list.length;
import {checkForRecent} from 'App/date';
import {useStore} from 'App/mstore';
import Dashboard from 'App/mstore/types/dashboard';
import {dashboardSelected, withSiteId} from 'App/routes';
return (
<NoContent
show={lenth === 0}
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} />
<div className="text-center mt-4">
{dashboardsSearch !== '' ? 'No matching results' : "You haven't created any dashboards yet"}
</div>
</div>
}
subtext={
<div>
A Dashboard is a collection of <Tooltip title={<div className="text-center">Utilize cards to visualize key user interactions or product performance metrics.</div>} className="text-center"><span className="underline decoration-dotted">Cards</span></Tooltip> that can be shared across teams.
</div>
}
>
<div className="mt-3 border-b">
<div className="grid grid-cols-12 py-2 font-medium px-6">
<div className="col-span-8">Title</div>
<div className="col-span-2">Visibility</div>
<div className="col-span-2 text-right">Last Modified</div>
</div>
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import {useHistory} from "react-router";
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map(
(dashboard: any) => (
<React.Fragment key={dashboard.dashboardId}>
<DashboardListItem dashboard={dashboard} />
</React.Fragment>
)
)}
</div>
function DashboardList({siteId}: { siteId: string }) {
const {dashboardStore} = useStore();
const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.filter.query;
const history = useHistory();
<div className="w-full flex items-center justify-between pt-4 px-6">
<div className="text-disabled-text">
Showing{' '}
<span className="font-semibold">{Math.min(list.length, dashboardStore.pageSize)}</span>{' '}
out of <span className="font-semibold">{list.length}</span> Dashboards
</div>
<Pagination
page={dashboardStore.page}
total={lenth}
onPageChange={(page) => dashboardStore.updateKey('page', page)}
limit={dashboardStore.pageSize}
debounceRequest={100}
/>
</div>
</NoContent>
);
const tableConfig: TableColumnsType<Dashboard> = [
{
title: 'Title',
dataIndex: 'name',
width: '25%',
render: (t) => <div className="link capitalize-first">{t}</div>,
},
{
title: 'Description',
ellipsis: {
showTitle: false,
},
width: '25%',
dataIndex: 'description',
},
{
title: 'Last Modified',
dataIndex: 'updatedAt',
width: '16.67%',
sorter: (a, b) => a.updatedAt.toMillis() - b.updatedAt.toMillis(),
sortDirections: ['ascend', 'descend'],
render: (date) => checkForRecent(date, 'LLL dd, yyyy, hh:mm a'),
},
{
title: 'Modified By',
dataIndex: 'updatedBy',
width: '16.67%',
sorter: (a, b) => a.updatedBy.localeCompare(b.updatedBy),
sortDirections: ['ascend', 'descend'],
},
{
title: (
<div className={'flex items-center justify-between'}>
<div>Visibility</div>
<Switch checked={!dashboardStore.filter.showMine} onChange={() =>
dashboardStore.updateKey('filter', {
...dashboardStore.filter,
showMine: !dashboardStore.filter.showMine,
})} checkedChildren={'Public'} unCheckedChildren={'Private'}/>
</div>
),
width: '16.67%',
dataIndex: 'isPublic',
render: (isPublic: boolean) => (
<Tag icon={isPublic ? <TeamOutlined/> : <LockOutlined/>}>
{isPublic ? 'Team' : 'Private'}
</Tag>
),
},
];
return (
list.length === 0 && !dashboardStore.filter.showMine ? (
<Empty
image={<AnimatedSVG name={dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS} size={600}/>}
imageStyle={{height: 300}}
description={(
<div className="text-center">
<div>
<Typography.Text className="my-2 text-xl font-medium">
Create your first dashboard.
</Typography.Text>
<div className="mb-2 text-lg text-gray-500 mt-2 leading-normal">
Organize your product and technical insights as cards in dashboards to see the bigger picture, <br/>take action and improve user experience.
</div>
<div className="my-4">
<CreateDashboardButton/>
</div>
</div>
</div>
)}
/>
) : (
<Table
dataSource={list}
columns={tableConfig}
pagination={{
showTotal: (total, range) =>
`Showing ${range[0]}-${range[1]} of ${total} items`,
size: 'small',
}}
onRow={(record) => ({
onClick: () => {
dashboardStore.selectDashboardById(record.dashboardId);
const path = withSiteId(
dashboardSelected(record.dashboardId),
siteId
);
history.push(path);
},
})}
/>)
);
}
export default observer(DashboardList);
export default connect((state: any) => ({
siteId: state.getIn(['site', 'siteId']),
}))(observer(DashboardList));

View file

@ -1,48 +0,0 @@
import React from 'react';
import { Icon } from 'UI';
import { connect } from 'react-redux';
import { IDashboard } from 'App/mstore/types/dashboard';
import { checkForRecent } from 'App/date';
import { withSiteId, dashboardSelected } from 'App/routes';
import { useStore } from 'App/mstore';
import { withRouter, RouteComponentProps } from 'react-router-dom';
interface Props extends RouteComponentProps {
dashboard: IDashboard;
siteId: string;
}
function DashboardListItem(props: Props) {
const { dashboard, siteId, history } = props;
const { dashboardStore } = useStore();
const onItemClick = () => {
dashboardStore.selectDashboardById(dashboard.dashboardId);
const path = withSiteId(dashboardSelected(dashboard.dashboardId), siteId);
history.push(path);
};
return (
<div className="hover:bg-active-blue cursor-pointer border-t px-6" onClick={onItemClick}>
<div className="grid grid-cols-12 py-4 select-none items-center">
<div className="col-span-8 flex items-start">
<div className="flex items-center capitalize-first">
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
<Icon name="columns-gap" size="16" color="tealx" />
</div>
<div className="link capitalize-first">{dashboard.name}</div>
</div>
</div>
<div className="col-span-2">
<div className="flex items-center">
<Icon name={dashboard.isPublic ? 'user-friends' : 'person-fill'} className="mr-2" />
<span>{dashboard.isPublic ? 'Team' : 'Private'}</span>
</div>
</div>
<div className="col-span-2 text-right">{checkForRecent(dashboard.createdAt, 'LLL dd, yyyy, hh:mm a')}</div>
</div>
{dashboard.description ? <div className="color-gray-medium px-2 pb-2">{dashboard.description}</div> : null}
</div>
);
}
// @ts-ignore
export default connect((state) => ({ siteId: state.getIn(['site', 'siteId']) }))(withRouter(DashboardListItem));

View file

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils';
import { Input } from 'antd';
let debounceUpdate: any = () => {};
@ -24,16 +24,15 @@ function DashboardSearch() {
};
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query}
name="dashboardsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title or description"
onChange={write}
/>
</div>
<Input.Search
value={query}
allowClear
name="dashboardsSearch"
className="w-full"
placeholder="Filter by title or description"
onChange={write}
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
/>
);
}

View file

@ -3,13 +3,13 @@ import withPageTitle from 'HOCs/withPageTitle';
import DashboardList from './DashboardList';
import Header from './Header';
function DashboardsView({ history, siteId }: { history: any; siteId: string }) {
return (
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded py-4 border">
<Header history={history} siteId={siteId} />
<DashboardList />
</div>
);
function DashboardsView({history, siteId}: { history: any; siteId: string }) {
return (
<div style={{maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded-lg py-4 border shadow-sm">
<Header />
<DashboardList/>
</div>
);
}
export default withPageTitle('Dashboards - OpenReplay')(DashboardsView);

View file

@ -1,65 +1,27 @@
import React from 'react';
import { Button, PageTitle, Toggler, Icon } from 'UI';
import Select from 'Shared/Select';
import {PageTitle} from 'UI';
import DashboardSearch from './DashboardSearch';
import { useStore } from 'App/mstore';
import { observer, useObserver } from 'mobx-react-lite';
import { withSiteId } from 'App/routes';
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
function Header({ history, siteId }: { history: any; siteId: string }) {
const { dashboardStore } = useStore();
const sort = useObserver(() => dashboardStore.sort);
const onAddDashboardClick = () => {
dashboardStore.initDashboard();
dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => {
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId));
});
};
return (
<>
<div className="flex items-center justify-between px-6">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" />
</div>
<div className="ml-auto flex items-center">
<Button variant="primary" onClick={onAddDashboardClick}>
New Dashboard
</Button>
<div className="mx-2"></div>
<div className="w-1/4" style={{ minWidth: 300 }}>
<DashboardSearch />
</div>
</div>
</div>
<div className="border-y px-3 py-1 mt-2 flex items-center w-full justify-end gap-4">
<Toggler
label="Private Dashboards"
checked={dashboardStore.filter.showMine}
name="test"
className="font-medium mr-2"
onChange={() =>
dashboardStore.updateKey('filter', {
...dashboardStore.filter,
showMine: !dashboardStore.filter.showMine,
})
}
/>
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' },
]}
defaultValue={sort.by}
plain
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
/>
</div>
</>
);
function Header() {
return (
<>
<div className="flex items-center justify-between px-4 pb-2">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards"/>
</div>
<div className="ml-auto flex items-center">
<CreateDashboardButton/>
<div className="mx-2"></div>
<div className="w-1/4" style={{minWidth: 300}}>
<DashboardSearch/>
</div>
</div>
</div>
</>
);
}
export default observer(Header);
export default Header;

View file

@ -0,0 +1,88 @@
import React, {useEffect, useMemo} from 'react';
import {useStore} from "App/mstore";
import WidgetWrapper from "Components/Dashboard/components/WidgetWrapper/WidgetWrapper";
import {observer} from "mobx-react-lite";
import {Loader} from "UI";
import WidgetChart from "Components/Dashboard/components/WidgetChart/WidgetChart";
import LazyLoad from 'react-lazyload';
import {Card} from "antd";
import {CARD_CATEGORIES} from "Components/Dashboard/components/DashboardList/NewDashModal/ExampleCards";
const CARD_TYPES_MAP = CARD_CATEGORIES.reduce((acc: any, category: any) => {
acc[category.key] = category.types;
return acc;
}, {});
interface Props {
category?: string;
selectedList: any;
onCard: (metricId: number) => void;
query?: string;
}
function CardsLibrary(props: Props) {
const {selectedList, query = ''} = props;
const {metricStore, dashboardStore} = useStore();
// const cards = useMemo(() => {
// return metricStore.filteredCards.filter((card: any) => {
// return CARD_TYPES_MAP[props.category || 'default'].includes(card.metricType);
// });
// }, [metricStore.filteredCards, props.category]);
const cards = useMemo(() => {
return metricStore.filteredCards.filter((card: any) => {
return card.name.toLowerCase().includes(query.toLowerCase());
});
}, [query, metricStore.filteredCards]);
useEffect(() => {
metricStore.fetchList();
}, []);
const onItemClick = (metricId: number) => {
props.onCard(metricId);
}
return (
<Loader loading={metricStore.isLoading}>
<div className="grid grid-cols-4 gap-4 items-start">
{cards.map((metric: any) => (
<React.Fragment key={metric.metricId}>
<div className={'col-span-' + metric.config.col}
onClick={() => onItemClick(metric.metricId)}>
<LazyLoad>
<Card hoverable
style={{
border: selectedList.includes(metric.metricId) ? '1px solid #1890ff' : '1px solid #f0f0f0',
}}
styles={{
header: {
padding: '4px 14px',
minHeight: '36px',
fontSize: '14px',
borderBottom: 'none'
},
body: {padding: '14px'},
cover: {
border: '2px solid #1890ff',
// border: selectedList.includes(metric.metricId) ? '2px solid #1890ff' : 'none',
}
}} title={metric.name}>
<WidgetChart
// isPreview={true}
metric={metric}
isTemplate={true}
isWidget={true}
/>
</Card>
</LazyLoad>
</div>
</React.Fragment>
))}
</div>
</Loader>
);
}
export default observer(CardsLibrary);

View file

@ -0,0 +1,102 @@
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: string = history.location.pathname.split('/')[1];
const dashboardId: string = history.location.pathname.split('/')[3];
const isItDashboard = history.location.pathname.includes('dashboard')
// 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 cardId = await createCard();
if (dashboardId) {
await addCardToDashboard(dashboardId, cardId);
dashboardStore.fetch(dashboardId);
} else if (isItDashboard) {
const dashboardId = await createNewDashboard();
await addCardToDashboard(dashboardId, cardId);
history.replace(`${history.location.pathname}/${dashboardId}`);
} else {
history.replace(`${history.location.pathname}/${cardId}`);
}
}
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-xl leading-4 font-medium">
{metric.name}
</div>
</Space>
<Button type="primary" onClick={createDashboardAndAddCard}>
<Space>
<ArrowRight size={14}/>Create
</Space>
</Button>
</div>
<CardBuilder siteId={siteId}/>
<WidgetPreview className="" name={metric.name} isEditing={true}/>
</div>
);
}
export default CreateCard;

View file

@ -0,0 +1,761 @@
import ExampleFunnel from "./Examples/Funnel";
import ExamplePath from "./Examples/Path";
import ExampleTrend from "./Examples/Trend";
import Trend from "./Examples/Trend";
import PerfBreakdown from "./Examples/PerfBreakdown";
import ByBrowser from "./Examples/SessionsBy/ByBrowser";
import BySystem from "./Examples/SessionsBy/BySystem";
import ByCountry from "./Examples/SessionsBy/ByCountry";
import ByUrl from "./Examples/SessionsBy/ByUrl";
import {ERRORS, FUNNEL, INSIGHTS, PERFORMANCE, TABLE, TIMESERIES, USER_PATH, WEB_VITALS} from "App/constants/card";
import {FilterKey} from "Types/filter/filterType";
import {Activity, BarChart, TableCellsMerge, TrendingUp} from "lucide-react";
import WebVital from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/WebVital";
import Bars from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/Bars";
import ByIssues from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByIssues";
import InsightsExample from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/InsightsExample";
import ByUser from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/ByUser";
import BarChartCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/BarChart";
import AreaChartCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard";
import CallsWithErrorsExample
from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/CallsWithErrorsExample";
export const CARD_CATEGORY = {
PRODUCT_ANALYTICS: 'product-analytics',
PERFORMANCE_MONITORING: 'performance-monitoring',
WEB_ANALYTICS: 'web-analytics',
ERROR_TRACKING: 'error-tracking',
WEB_VITALS: 'web-vitals',
}
export const CARD_CATEGORIES = [
{key: CARD_CATEGORY.PRODUCT_ANALYTICS, label: 'Product Analytics', icon: TrendingUp, types: [USER_PATH, ERRORS]},
{key: CARD_CATEGORY.PERFORMANCE_MONITORING, label: 'Performance Monitoring', icon: Activity, types: [TIMESERIES]},
{key: CARD_CATEGORY.WEB_ANALYTICS, label: 'Web Analytics', icon: BarChart, types: [TABLE]},
{key: CARD_CATEGORY.ERROR_TRACKING, label: 'Errors Tracking', icon: TableCellsMerge, types: [WEB_VITALS]},
{key: CARD_CATEGORY.WEB_VITALS, label: 'Web Vitals', icon: TableCellsMerge, types: [WEB_VITALS]}
];
export interface CardType {
title: string;
key: string;
cardType: string;
category: string;
example: any;
metricOf?: string;
width?: number;
data?: any;
height?: number;
}
export const CARD_LIST: CardType[] = [
{
title: 'Funnel',
key: FUNNEL,
cardType: FUNNEL,
category: CARD_CATEGORIES[0].key,
example: ExampleFunnel,
width: 4,
height: 356,
data: {
stages: [
{
"value": [
"/sessions"
],
"type": "location",
"operator": "contains",
"sessionsCount": 1586,
"dropPct": null,
"usersCount": 470,
"dropDueToIssues": 0
},
{
"value": [],
"type": "click",
"operator": "onAny",
"sessionsCount": 1292,
"dropPct": 18,
"usersCount": 450,
"dropDueToIssues": 294
}
],
totalDropDueToIssues: 294
}
},
{
title: 'Path Finder',
key: USER_PATH,
cardType: USER_PATH,
category: CARD_CATEGORIES[0].key,
example: ExamplePath,
},
{
title: 'Sessions Trend',
key: TIMESERIES,
cardType: TIMESERIES,
metricOf: 'sessionCount',
category: CARD_CATEGORIES[0].key,
data: {
chart: generateTimeSeriesData(),
label: "Number of Sessions",
namesMap: [
"Series 1"
]
},
example: ExampleTrend,
},
{
title: 'Sessions by Issues',
key: FilterKey.ISSUE,
cardType: TABLE,
metricOf: FilterKey.ISSUE,
category: CARD_CATEGORIES[0].key,
example: ByIssues,
},
{
title: 'Insights',
key: INSIGHTS,
cardType: INSIGHTS,
metricOf: 'issueCategories',
category: CARD_CATEGORIES[0].key,
width: 4,
example: InsightsExample,
},
// Performance Monitoring
{
title: 'CPU Load',
key: FilterKey.CPU,
cardType: PERFORMANCE,
metricOf: FilterKey.CPU,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "CPU Load (%)",
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Crashes',
key: FilterKey.CRASHES,
cardType: PERFORMANCE,
metricOf: FilterKey.CRASHES,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Framerate',
key: FilterKey.FPS,
cardType: PERFORMANCE,
metricOf: FilterKey.FPS,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "Frames Per Second",
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'DOM Building Time',
key: FilterKey.PAGES_DOM_BUILD_TIME,
cardType: PERFORMANCE,
metricOf: FilterKey.PAGES_DOM_BUILD_TIME,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "DOM Build Time (ms)",
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Memory Consumption',
key: FilterKey.MEMORY_CONSUMPTION,
cardType: PERFORMANCE,
metricOf: FilterKey.MEMORY_CONSUMPTION,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "JS Heap Size (MB)",
unit: 'mb',
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Page Response Time',
key: FilterKey.PAGES_RESPONSE_TIME,
cardType: PERFORMANCE,
metricOf: FilterKey.PAGES_RESPONSE_TIME,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "Page Response Time (ms)",
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Page Response Time Distribution',
key: FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION,
cardType: PERFORMANCE,
metricOf: FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
label: "Number of Calls",
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Resources vs Visually Completed',
key: FilterKey.RESOURCES_VS_VISUALLY_COMPLETE,
cardType: PERFORMANCE,
metricOf: FilterKey.RESOURCES_VS_VISUALLY_COMPLETE,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateBarChartDate(),
namesMap: [
"Series 1"
]
},
example: BarChartCard,
},
{
title: 'Sessions per Browser',
key: FilterKey.SESSIONS_PER_BROWSER,
cardType: PERFORMANCE,
metricOf: FilterKey.SESSIONS_PER_BROWSER,
category: CARD_CATEGORIES[1].key,
data: generateRandomBarsData(),
example: Bars,
},
{
title: 'Slowest Domains',
key: FilterKey.SLOWEST_DOMAINS,
cardType: PERFORMANCE,
metricOf: FilterKey.SLOWEST_DOMAINS,
category: CARD_CATEGORIES[1].key,
data: generateRandomBarsData(),
example: Bars,
},
{
title: 'Speed Index by Location',
key: FilterKey.SPEED_LOCATION,
cardType: PERFORMANCE,
metricOf: FilterKey.SPEED_LOCATION,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Time to Render',
key: FilterKey.TIME_TO_RENDER,
cardType: PERFORMANCE,
metricOf: FilterKey.TIME_TO_RENDER,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
{
title: 'Sessions Impacted by Slow Pages',
key: FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES,
cardType: PERFORMANCE,
metricOf: FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES,
category: CARD_CATEGORIES[1].key,
data: {
chart: generateAreaData(),
namesMap: [
"Series 1"
]
},
example: AreaChartCard,
},
// Web analytics
{
title: 'Sessions by Users',
key: FilterKey.USERID,
cardType: TABLE,
metricOf: FilterKey.USERID,
category: CARD_CATEGORIES[2].key,
example: ByUser,
},
{
title: 'Sessions by Browser',
key: FilterKey.USER_BROWSER,
cardType: TABLE,
metricOf: FilterKey.USER_BROWSER,
category: CARD_CATEGORIES[2].key,
example: ByBrowser,
},
// {
// title: 'Sessions by System',
// key: TYPE.SESSIONS_BY_SYSTEM,
// cardType: TABLE,
// metricOf: FilterKey.USER_OS,
// category: CARD_CATEGORIES[2].key,
// example: BySystem,
// },
{
title: 'Sessions by Country',
key: FilterKey.USER_COUNTRY,
cardType: TABLE,
metricOf: FilterKey.USER_COUNTRY,
category: CARD_CATEGORIES[2].key,
example: ByCountry,
},
{
title: 'Sessions by Device',
key: FilterKey.USER_DEVICE,
cardType: TABLE,
metricOf: FilterKey.USER_DEVICE,
category: CARD_CATEGORIES[2].key,
example: BySystem,
},
{
title: 'Sessions by URL',
key: FilterKey.LOCATION,
cardType: TABLE,
metricOf: FilterKey.LOCATION,
category: CARD_CATEGORIES[2].key,
example: ByUrl,
},
// Errors Tracking
{
title: 'JS Errors',
key: FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS,
cardType: ERRORS,
metricOf: FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS,
category: CARD_CATEGORIES[3].key,
data: {
chart: generateBarChartDate(),
},
example: BarChartCard,
},
{
title: 'Errors by Origin',
key: FilterKey.RESOURCES_BY_PARTY,
cardType: ERRORS,
metricOf: FilterKey.RESOURCES_BY_PARTY,
category: CARD_CATEGORIES[3].key,
data: {
chart: generateBarChartDate(),
},
example: BarChartCard,
},
{
title: 'Errors by Domain',
key: FilterKey.ERRORS_PER_DOMAINS,
cardType: ERRORS,
metricOf: FilterKey.ERRORS_PER_DOMAINS,
category: CARD_CATEGORIES[3].key,
example: Bars,
data: generateRandomBarsData(),
},
{
title: 'Errors by Type',
key: FilterKey.ERRORS_PER_TYPE,
cardType: ERRORS,
metricOf: FilterKey.ERRORS_PER_TYPE,
category: CARD_CATEGORIES[3].key,
data: {
chart: generateBarChartDate(),
},
example: BarChartCard,
},
{
title: 'Calls with Errors',
key: FilterKey.CALLS_ERRORS,
cardType: ERRORS,
metricOf: FilterKey.CALLS_ERRORS,
category: CARD_CATEGORIES[3].key,
width: 4,
data: {
chart: [
{
"method": "GET",
"urlHostpath": 'https://openreplay.com',
"allRequests": 1333,
"4xx": 1333,
"5xx": 0
},
{
"method": "POST",
"urlHostpath": 'https://company.domain.com',
"allRequests": 10,
"4xx": 10,
"5xx": 0
},
{
"method": "PUT",
"urlHostpath": 'https://example.com',
"allRequests": 3,
"4xx": 3,
"5xx": 0
}
],
},
example: CallsWithErrorsExample,
},
{
title: '4xx Domains',
key: FilterKey.DOMAINS_ERRORS_4XX,
cardType: ERRORS,
metricOf: FilterKey.DOMAINS_ERRORS_4XX,
category: CARD_CATEGORIES[3].key,
data: {
chart: generateTimeSeriesData(),
label: "Number of Errors",
namesMap: [
"Series 1"
]
},
example: ExampleTrend,
},
{
title: '5xx Domains',
key: FilterKey.DOMAINS_ERRORS_5XX,
cardType: ERRORS,
metricOf: FilterKey.DOMAINS_ERRORS_5XX,
category: CARD_CATEGORIES[3].key,
data: {
chart: generateTimeSeriesData(),
label: "Number of Errors",
namesMap: [
"Series 1"
]
},
example: ExampleTrend,
},
// Web vitals
{
title: 'CPU Load',
key: FilterKey.AVG_CPU,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_CPU,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'DOM Content Loaded',
key: FilterKey.AVG_DOM_CONTENT_LOADED,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_DOM_CONTENT_LOADED,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'DOM Content Loaded Start',
key: FilterKey.AVG_DOM_CONTENT_LOAD_START,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_DOM_CONTENT_LOAD_START,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'First Meaningful Paint',
key: FilterKey.AVG_FIRST_CONTENTFUL_PIXEL,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_FIRST_CONTENTFUL_PIXEL,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'First Paint',
key: FilterKey.AVG_FIRST_PAINT,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_FIRST_PAINT,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Frame Rate',
key: FilterKey.AVG_FPS,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_FPS,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Image Load Time',
key: FilterKey.AVG_IMAGE_LOAD_TIME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_IMAGE_LOAD_TIME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Page Load Time',
key: FilterKey.AVG_PAGE_LOAD_TIME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_PAGE_LOAD_TIME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'DOM Build Time',
key: FilterKey.AVG_PAGES_DOM_BUILD_TIME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_PAGES_DOM_BUILD_TIME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Page Response Time',
key: FilterKey.AVG_PAGES_RESPONSE_TIME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_PAGES_RESPONSE_TIME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Request Load Time',
key: FilterKey.AVG_REQUEST_LOADT_IME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_REQUEST_LOADT_IME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Response Time',
key: FilterKey.AVG_RESPONSE_TIME,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_RESPONSE_TIME,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Session Duration',
key: FilterKey.AVG_SESSION_DURATION,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_SESSION_DURATION,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Time Till First Byte',
key: FilterKey.AVG_TILL_FIRST_BYTE,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_TILL_FIRST_BYTE,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Time to be Interactive',
key: FilterKey.AVG_TIME_TO_INTERACTIVE,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_TIME_TO_INTERACTIVE,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'Time to Render',
key: FilterKey.AVG_TIME_TO_RENDER,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_TIME_TO_RENDER,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
{
title: 'JS Heap Size',
key: FilterKey.AVG_USED_JS_HEAP_SIZE,
cardType: WEB_VITALS,
metricOf: FilterKey.AVG_USED_JS_HEAP_SIZE,
category: CARD_CATEGORIES[4].key,
width: 1,
height: 148,
data: generateWebVitalData(),
example: WebVital,
},
]
function generateRandomBarsData(): { total: number, values: { label: string, value: number }[] } {
const labels = ["company.domain.com", "openreplay.com"];
const values = labels.map(label => ({
label,
value: Math.floor(Math.random() * 100)
}));
const total = values.reduce((acc, curr) => acc + curr.value, 0);
return {
total,
values: values.sort((a, b) => b.value - a.value)
};
}
function generateWebVitalData(): { value: number, chart: { timestamp: number, value: number }[], unit: string } {
const chart = Array.from({length: 7}, (_, i) => ({
timestamp: Date.now() + i * 86400000,
value: parseFloat((Math.random() * 10).toFixed(15))
}));
const value = chart.reduce((acc, curr) => acc + curr.value, 0) / chart.length;
return {
value,
chart,
unit: "%"
};
}
function generateTimeSeriesData(): any[] {
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
const pointsPerMonth = 3; // Number of points for each month
const data = months.flatMap((month, monthIndex) =>
Array.from({length: pointsPerMonth}, (_, pointIndex) => ({
time: month,
"Series 1": Math.floor(Math.random() * 90),
timestamp: Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000
}))
);
return data;
}
function generateAreaData(): any[] {
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"];
const pointsPerMonth = 3; // Number of points for each month
const data = months.flatMap((month, monthIndex) =>
Array.from({length: pointsPerMonth}, (_, pointIndex) => ({
time: month,
"value": Math.floor(Math.random() * 90),
timestamp: Date.now() + (monthIndex * pointsPerMonth + pointIndex) * 86400000
}))
);
return data;
}
function generateRandomValue(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateBarChartDate(): any[] {
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul'];
return months.map(month => ({
time: month,
value: generateRandomValue(1000, 5000),
}));
}

View file

@ -0,0 +1,82 @@
import React from 'react';
import {NoContent} from 'UI';
import {
AreaChart, Area,
CartesianGrid, Tooltip,
ResponsiveContainer,
XAxis, YAxis
} from 'recharts';
import {NO_METRIC_DATA} from 'App/constants/messages'
import {AvgLabel, Styles} from "Components/Dashboard/Widgets/common";
import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
onClick?: any;
data?: any,
}
// interface Props {
// data: any,
// label?: string
// }
function AreaChartCard(props: Props) {
const {data} = props;
const gradientDef = Styles.gradientDef();
return (
<ExCard
{...props}
title={
<div className={'flex items-center gap-2'}>
<div>{props.title}</div>
</div>
}
>
<NoContent
size="small"
title={NO_METRIC_DATA}
show={data?.chart.length === 0}
>
<>
{/*<div className="flex items-center justify-end mb-3">*/}
{/* <AvgLabel text="Avg" className="ml-3" count={data?.value}/>*/}
{/*</div>*/}
<ResponsiveContainer height={207} width="100%">
<AreaChart
data={data?.chart}
margin={Styles.chartMargins}
>
{gradientDef}
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis {...Styles.xaxis} dataKey="time" interval={3}/>
<YAxis
{...Styles.yaxis}
allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)}
label={{...Styles.axisLabelLeft, value: data?.label}}
/>
<Tooltip {...Styles.tooltip} />
<Area
name="Avg"
type="monotone"
dataKey="value"
stroke={Styles.strokeColor}
fillOpacity={1}
strokeWidth={2}
strokeOpacity={0.8}
fill={'url(#colorCount)'}
/>
</AreaChart>
</ResponsiveContainer>
</>
</NoContent>
</ExCard>
);
}
export default AreaChartCard;

View file

@ -0,0 +1,70 @@
import {GitCommitHorizontal} from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {PERFORMANCE} from "App/constants/card";
import {Bar, BarChart, CartesianGrid, Legend, Rectangle, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts";
import {Styles} from "Components/Dashboard/Widgets/common";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
onClick?: any;
data?: any,
}
function BarChartCard(props: Props) {
return (
<ExCard
{...props}
>
{/*<ResponsiveContainer width="100%" height="100%">*/}
{/* <BarChart*/}
{/* width={400}*/}
{/* height={280}*/}
{/* data={_data}*/}
{/* margin={Styles.chartMargins}*/}
{/* >*/}
{/* /!*<CartesianGrid strokeDasharray="3 3"/>*!/*/}
{/* <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>*/}
{/* <XAxis {...Styles.xaxis} dataKey="name"/>*/}
{/* <YAxis {...Styles.yaxis} />*/}
{/* <Tooltip/>*/}
{/* <Legend/>*/}
{/* <Bar dataKey="pv" fill="#8884d8" activeBar={<Rectangle fill="pink" stroke="blue"/>}/>*/}
{/* /!*<Bar dataKey="uv" fill="#82ca9d" activeBar={<Rectangle fill="gold" stroke="purple"/>}/>*!/*/}
{/* </BarChart>*/}
{/*</ResponsiveContainer>*/}
<ResponsiveContainer height={240} width="100%">
<BarChart
data={props.data?.chart}
margin={Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={21}
/>
<YAxis
{...Styles.yaxis}
tickFormatter={val => Styles.tickFormatter(val)}
label={{...Styles.axisLabelLeft, value: "Number of Errors"}}
allowDecimals={false}
/>
<Legend/>
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name={<span className="float">One</span>}
dataKey="value" stackId="a" fill={Styles.colors[0]}/>
{/*<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a"*/}
{/* fill={Styles.colors[2]}/>*/}
</BarChart>
</ResponsiveContainer>
</ExCard>
);
}
export default BarChartCard;

View file

@ -0,0 +1,61 @@
import React from 'react';
import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard";
import {List, Progress} from "antd";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
data?: any;
}
function Bars(props: Props) {
const _data = props.data || {
total: 90,
values: [
{
"label": "company.domain.com",
"value": 89
},
{
"label": "openreplay.com",
"value": 15
}
]
}
return (
<ExCard
{...props}
>
<List
itemLayout="horizontal"
dataSource={_data.values}
renderItem={(item: any) => (
<List.Item>
<List.Item.Meta
title={(
<div className="flex justify-between w-full">
<span>{item.label}</span>
<span>{item.value}</span>
</div>
)}
description={(
<Progress percent={Math.round((item.value * 100) / _data.total)}
showInfo={false}
strokeColor="#394EFF"
trailColor="#f0f0f0"
style={{width: '100%'}}
size={['small', 2]}
/>
)}
/>
</List.Item>
)}
/>
</ExCard>
);
}
export default Bars;

View file

@ -0,0 +1,23 @@
import React from 'react';
import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard";
import CallWithErrors from "Components/Dashboard/Widgets/PredefinedWidgets/CallWithErrors";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
onClick?: any;
data?: any,
}
function CallsWithErrorsExample(props: Props) {
return (
<ExCard
{...props}
>
<CallWithErrors data={props.data}/>
</ExCard>
);
}
export default CallsWithErrorsExample;

View file

@ -0,0 +1,266 @@
import { Segmented } from 'antd';
import {
Angry,
ArrowDownUp,
Mouse,
MousePointerClick,
Unlink,
} from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import { size } from '@floating-ui/react-dom-interactions';
const TYPES = {
Frustrations: 'frustrations',
Errors: 'errors',
Users: 'users',
};
function ExampleCount(props: any) {
const [type, setType] = React.useState(TYPES.Frustrations);
const el = {
[TYPES.Frustrations]: <Frustrations />,
[TYPES.Errors]: <Errors />,
[TYPES.Users]: <Users />,
};
return (
<ExCard
{...props}
title={
<div className={'flex items-center gap-2'}>
<div>{props.title}</div>
<div className={'font-normal'}>
<Segmented
options={[
{ label: 'Frustrations', value: '0' },
{ label: 'Errors', value: '1' },
{ label: 'Users', value: '2' },
]}
size='small'
onChange={(v) => setType(v)}
/>
</div>
</div>
}
>
{el[type]}
</ExCard>
);
}
export function Frustrations() {
const rows = [
{
label: 'Rage Clicks',
progress: 25,
value: 100,
icon: <Angry size={12} strokeWidth={1} />,
},
{
label: 'Dead Clicks',
progress: 75,
value: 75,
icon: <MousePointerClick size={12} strokeWidth={1} />,
},
{
label: '4XX Pages',
progress: 50,
value: 50,
icon: <Unlink size={12} strokeWidth={1} />,
},
{
label: 'Mouse Trashing',
progress: 10,
value: 25,
icon: <Mouse size={12} strokeWidth={1} />,
},
{
label: 'Excessive Scrolling',
progress: 10,
value: 10,
icon: <ArrowDownUp size={12} strokeWidth={1} />,
},
];
const lineWidth = 140;
return (
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted py-2 last:border-0 first:pt-0 last:pb-0'
}
>
<Circle badgeType={0}>{r.icon}</Circle>
<div>{r.label}</div>
<div style={{ marginLeft: 'auto', marginRight: 20, display: 'flex' }}>
<div
style={{
height: 2,
width: lineWidth * (0.01 * r.progress),
background: '#394EFF',
}}
className={'rounded-l'}
/>
<div
style={{
height: 2,
width: lineWidth - lineWidth * (0.01 * r.progress),
background: '#E2E4F6',
}}
className={'rounded-r'}
/>
</div>
<div className={'min-w-8'}>{r.value}</div>
</div>
))}
</div>
);
}
export function Errors() {
const rows = [
{
label: 'HTTP response status code (404 Not Found)',
value: 500,
progress: 90,
icon: <div className={'text-red text-xs'}>4XX</div>,
},
{
label: 'Cross-origin request blocked',
value: 300,
progress: 60,
icon: <div className={'text-red text-xs'}>CROS</div>,
},
{
label: 'Reference error',
value: 200,
progress: 40,
icon: <div className={'text-red text-xs'}>RE</div>,
},
{
label: 'Unhandled Promise Rejection',
value: 50,
progress: 20,
icon: <div className={'text-red text-xs'}>NULL</div>,
},
{
label: 'Failed Network Request',
value: 10,
progress: 5,
icon: <div className={'text-red text-xs'}>XHR</div>,
},
];
const lineWidth = 270;
return (
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted last:border-0 py-2 first:pt-0 last:pb-0'
}
>
<Circle badgeType={1}>{r.icon}</Circle>
<div className={'ml-2 flex flex-col gap-0'}>
<div>{r.label}</div>
<div style={{ display: 'flex' }}>
<div
style={{
height: 2,
width: lineWidth * (0.01 * r.progress),
background: '#394EFF',
}}
className={'rounded-l'}
/>
<div
style={{
height: 2,
width: lineWidth - lineWidth * (0.01 * r.progress),
background: '#E2E4F6',
}}
className={'rounded-r'}
/>
</div>
</div>
<div className={'min-w-8 ml-auto'}>{r.value}</div>
</div>
))}
</div>
);
}
export function Users() {
const rows = [
{
label: 'pedro@mycompany.com',
value: '9.5K',
},
{
label: 'mauricio@mycompany.com',
value: '2.5K',
},
{
label: 'alex@mycompany.com',
value: '405',
},
{
label: 'jose@mycompany.com',
value: '150',
},
{
label: 'maria@mycompany.com',
value: '123',
},
];
return (
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted py-2 last:border-0 first:pt-0 last:pb-0'
}
>
<Circle badgeType={2}>{r.label[0].toUpperCase()}</Circle>
<div className={'ml-2'}>
<div>{r.label}</div>
</div>
<div className={'min-w-8 ml-auto'}>{r.value}</div>
</div>
))}
</div>
);
}
export function Circle({
children,
badgeType,
}: {
children: React.ReactNode;
badgeType: 0 | 1 | 2 | 3;
}) {
const colors = {
// frustrations
0: '#FFFBE6',
// errors
1: '#FFF1F0',
// users and domains
2: '#EBF4F5',
// sessions by url
3: '#E2E4F6',
};
return (
<div
className={'w-8 h-8 flex items-center justify-center rounded-full'}
style={{ background: colors[badgeType] }}
>
{children}
</div>
);
}
export default ExampleCount;

View file

@ -0,0 +1,30 @@
import React from 'react'
function ExCard({
title,
children,
type,
onCard,
height,
}: {
title: React.ReactNode;
children: React.ReactNode;
type: string;
onCard: (card: string) => void;
height?: number;
}) {
return (
<div
className={'rounded-lg overflow-hidden border border-transparent p-4 bg-white hover:border-blue hover:shadow-sm relative'}
style={{width: '100%', height: height || 286}}
>
<div className="absolute inset-0 z-10 cursor-pointer" onClick={() => onCard(type)}></div>
<div className={'font-medium text-lg'}>{title}</div>
<div className={'flex flex-col gap-2 mt-2 cursor-pointer'}
style={{height: height ? height - 50 : 236}}
onClick={() => onCard(type)}>{children}</div>
</div>
);
}
export default ExCard

View file

@ -0,0 +1,67 @@
import {ArrowRight} from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {FUNNEL} from "App/constants/card";
import FunnelWidget from "Components/Funnels/FunnelWidget/FunnelWidget";
import Funnel from "App/mstore/types/funnel";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
data?: any,
}
function ExampleFunnel(props: Props) {
// const steps = [
// {
// progress: 500,
// },
// {
// progress: 250,
// },
// {
// progress: 100,
// },
// ];
const _data = {
funnel: new Funnel().fromJSON(props.data)
}
return (
<ExCard
{...props}
>
<FunnelWidget data={_data} isWidget={true}/>
{/*<>*/}
{/* {steps.map((step, index) => (*/}
{/* <div key={index}>*/}
{/* <div>Step {index + 1}</div>*/}
{/* <div className={'rounded flex items-center w-full overflow-hidden'}>*/}
{/* <div*/}
{/* style={{*/}
{/* backgroundColor: step.progress <= 100 ? '#394EFF' : '#E2E4F6',*/}
{/* width: `${(step.progress / 500) * 100}%`,*/}
{/* height: 30,*/}
{/* }}*/}
{/* />*/}
{/* <div*/}
{/* style={{*/}
{/* width: `${((500 - step.progress) / 500) * 100}%`,*/}
{/* height: 30,*/}
{/* background: '#FFF1F0',*/}
{/* }}*/}
{/* />*/}
{/* </div>*/}
{/* <div className={'flex items-center gap-2'}>*/}
{/* <ArrowRight size={14} color={'#8C8C8C'} strokeWidth={1}/>*/}
{/* <div className={'text-disabled-text'}>{step.progress}</div>*/}
{/* </div>*/}
{/* </div>*/}
{/* ))}*/}
{/*</>*/}
</ExCard>
);
}
export default ExampleFunnel;

View file

@ -0,0 +1,56 @@
import React from 'react';
import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard";
import InsightsCard from "Components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard";
import {InsightIssue} from "App/mstore/types/widget";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
}
function InsightsExample(props: Props) {
const data = {
issues: [
{
"category": "errors",
"name": "Error: Invalid unit value NaN",
"value": 562,
"oldValue": null,
"ratio": 7.472410583698976,
"change": 1,
"isNew": true
},
{
"category": "errors",
"name": "TypeError: e.node.getContext is not a function",
"value": 128,
"oldValue": 1,
"ratio": 1.7019013429065284,
"change": 12700.0,
"isNew": false
},
{
"category": "errors",
"name": "Unhandled Promise Rejection: {\"message\":\"! POST error on /client/members; 400\",\"response\":{}}",
"value": 26,
"oldValue": null,
"ratio": 0.34569871027788857,
"change": 1,
"isNew": true
}
].map(
(i: any) =>
new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew)
)
}
return (
<ExCard
{...props}
>
<InsightsCard data={data}/>
</ExCard>
);
}
export default InsightsExample;

View file

@ -0,0 +1,59 @@
import React from 'react';
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(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: 3, target: 4, value: 50},
{source: 3, target: 5, value: 50},
{source: 4, target: 5, value: 15},
],
};
return (
<ExCard
{...props}
>
<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

@ -0,0 +1,105 @@
import { GitCommitHorizontal } from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
import {PERFORMANCE} from "App/constants/card";
function PerfBreakdown(props: any) {
const rows = [
['5K', '1K'],
['4K', '750'],
['3K', '500'],
['2K', '250'],
['1K', '0'],
];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May'];
const values = [
[3, 1, 9],
[2, 4, 10],
[3, 6, 2],
[7, 4, 1],
[5, 3, 4],
];
const bgs = ['#E2E4F6', '#A7BFFF', '#394EFF'];
return (
<ExCard
{...props}
>
<div className={'relative'}>
<div className={'flex flex-col gap-4'}>
{rows.map((r) => (
<div className={'flex items-center gap-2'}>
<div className={'text-gray-dark'}>{r[0]}</div>
<div className="border-t border-dotted border-gray-lighter w-full"></div>
<div className={'text-gray-dark min-w-8'}>{r[1]}</div>
</div>
))}
</div>
<div className={'px-4 flex items-center justify-around w-full'}>
{months.map((m, i) => (
<div className={'text-gray-dark relative'}>
<span>{m}</span>
<div
className={'absolute flex flex-col'}
style={{ bottom: 30, left: 0, width: 24 }}
>
{values[i].map((v, bg) => (
<div
style={{
width: '100%',
height: v * 9 + 'px',
background: bgs[bg],
}}
/>
))}
</div>
</div>
))}
</div>
<div
style={{
position: 'absolute',
top: 30,
left: 30,
zIndex: 99,
width: 308,
overflow: 'hidden',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="332"
height="37"
viewBox="0 0 332 37"
fill="none"
>
<path
d="M1 30.8715L4.66667 26.964C8.33333 23.0566 15.6667 15.2417 23 9.74387C30.3333 4.24605 37.6667 1.06529 45 1.54812C52.3333 2.03094 59.6667 6.17735 67 10.8175C74.3333 15.4577 81.6667 20.5916 89 19.6024C96.3333 18.6133 103.667 11.5009 111 7.69717C118.333 3.89339 125.667 3.39814 133 8.24328C140.333 13.0884 147.667 23.274 155 28.5047C162.333 33.7354 169.667 34.0114 177 33.4739C184.333 32.9365 191.667 31.5856 199 28.7677C206.333 25.9499 213.667 21.665 221 18.723C228.333 15.781 235.667 14.182 243 10.7612C250.333 7.34035 257.667 2.09783 265 3.39238C272.333 4.68693 279.667 12.5186 287 14.2932C294.333 16.0679 301.667 11.7856 309 14.3106C316.333 16.8356 323.667 26.1678 327.333 30.8339L331 35.5"
stroke="#6A8CFF"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
</div>
<div className={'flex gap-4 justify-center'}>
<div className={'flex gap-2 items-center'}>
<div className={'w-4 h-4 rounded-full bg-[#E2E4F6]'} />
<div className={'text-disabled-text'}>XHR</div>
</div>
<div className={'flex gap-2 items-center'}>
<div className={'w-4 h-4 rounded-full bg-[#A7BFFF]'} />
<div className={'text-disabled-text'}>Other</div>
</div>
<div className={'flex gap-2 items-center'}>
<GitCommitHorizontal size={14} strokeWidth={1} color={'#6A8CFF'} />
<div className={'text-disabled-text'}>Response End</div>
</div>
</div>
</ExCard>
);
}
export default PerfBreakdown;

View file

@ -0,0 +1,52 @@
import React from 'react';
import { Icon } from 'UI';
import ExCard from '../ExCard';
import ByComponent from './Component';
function ByBrowser(props: any) {
const rows = [
{
label: 'Chrome',
progress: 85,
value: '2.5K',
icon: <Icon name={'color/chrome'} size={26} />,
},
{
label: 'Edge',
progress: 25,
value: '405',
icon: <Icon name={'color/edge'} size={26} />,
},
{
label: 'Safari',
progress: 5,
value: '302',
icon: <Icon name={'color/safari'} size={26} />,
},
{
label: 'Firefox',
progress: 3,
value: '194',
icon: <Icon name={'color/firefox'} size={26} />,
},
{
label: 'Opera',
progress: 1,
value: '57',
icon: <Icon name={'color/opera'} size={26} />,
},
];
const lineWidth = 200;
return (
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
);
}
export default ByBrowser;

View file

@ -0,0 +1,50 @@
import React from 'react';
import { Icon } from 'UI';
import ByComponent from './Component';
function ByCountry(props: any) {
const rows = [
{
label: 'United States',
progress: 70,
value: '165K',
icon: <Icon name={'color/us'} size={26} />,
},
{
label: 'India',
progress: 25,
value: '100K',
icon: <Icon name={'color/in'} size={26} />,
},
{
label: 'United Kingdom',
progress: 10,
value: '50K',
icon: <Icon name={'color/gb'} size={26} />,
},
{
label: 'France',
progress: 7,
value: '30K',
icon: <Icon name={'color/fr'} size={26} />,
},
{
label: 'Germany',
progress: 4,
value: '20K',
icon: <Icon name={'color/de'} size={26} />,
},
];
return (
<ByComponent
rows={rows}
lineWidth={180}
{...props}
/>
);
}
export default ByCountry;

View file

@ -0,0 +1,46 @@
import React from 'react';
import {Icon} from 'UI';
import ExCard from '../ExCard';
import ByComponent from './Component';
function ByIssues(props: any) {
const rows = [
{
label: 'Dead Click',
progress: 85,
value: '2.5K',
icon: <Icon name={'color/issues/dead_click'} size={26}/>,
},
{
label: 'Click Rage',
progress: 25,
value: '405',
icon: <Icon name={'color/issues/click_rage'} size={26}/>,
},
{
label: 'Bad Request',
progress: 5,
value: '302',
icon: <Icon name={'color/issues/bad_request'} size={26}/>,
},
{
label: 'Mouse Thrashing',
progress: 3,
value: '194',
icon: <Icon name={'color/issues/mouse_thrashing'} size={26}/>,
},
];
const lineWidth = 200;
return (
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
);
}
export default ByIssues;

View file

@ -0,0 +1,51 @@
import React from 'react';
import { Icon } from 'UI';
import ByComponent from './Component';
function BySystem(props: any) {
const rows = [
{
label: 'Windows',
progress: 75,
value: '2.5K',
icon: <Icon name={'color/microsoft'} size={26} />,
},
{
label: 'MacOS',
progress: 25,
value: '405',
icon: <Icon name={'color/apple'} size={26} />,
},
{
label: 'Ubuntu',
progress: 10,
value: '302',
icon: <Icon name={'color/ubuntu'} size={26} />,
},
{
label: 'Fedora',
progress: 7,
value: '302',
icon: <Icon name={'color/fedora'} size={26} />,
},
{
label: 'Unknown',
progress: 4,
value: '194',
icon: <Icon name={'question-circle'} size={26} />,
},
];
const lineWidth = 200;
return (
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
);
}
export default BySystem;

View file

@ -0,0 +1,104 @@
import { LinkOutlined } from '@ant-design/icons';
import { Segmented } from 'antd';
import React from 'react';
import { Circle } from '../Count';
import ExCard from '../ExCard';
function ByUrl(props: any) {
const [mode, setMode] = React.useState(0);
const rows = [
{
label: '/category/womens/dresses',
ptitle: 'Dresses',
value: '500',
progress: 75,
icon: <LinkOutlined size={12} />,
},
{
label: '/search?q=summer+dresses',
ptitle: 'Search: summer dresses',
value: '306',
progress: 60,
icon: <LinkOutlined size={12} />,
},
{
label: '/account/orders',
ptitle: 'Account: Orders',
value: '198',
progress: 30,
icon: <LinkOutlined size={12} />,
},
{
label: '/checkout/confirmation',
ptitle: 'Checkout: Confirmation',
value: '47',
progress: 15,
icon: <LinkOutlined size={12} />,
},
{
label: '/checkout/payment',
ptitle: 'Checkout: Payment',
value: '5',
progress: 5,
icon: <LinkOutlined size={12} />,
},
];
const lineWidth = 240;
return (
<ExCard
{...props}
title={
<div className={'flex gap-2 items-center'}>
<div>{props.title}</div>
<div className={'font-normal'}><Segmented
options={[
{ label: 'URL', value: '0' },
{ label: 'Page Title', value: '1' },
]}
onChange={(v) => setMode(Number(v))}
size='small'
/>
</div>
</div>
}
>
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted last:border-0 py-2 first:pt-0 last:pb-0'
}
>
<Circle badgeType={1}>{r.icon}</Circle>
<div className={'ml-2 flex flex-col gap-0'}>
<div>{mode === 0 ? r.label : r.ptitle}</div>
<div style={{ display: 'flex' }}>
<div
style={{
height: 2,
width: lineWidth * (0.01 * r.progress),
background: '#394EFF',
}}
className={'rounded-l'}
/>
<div
style={{
height: 2,
width: lineWidth - lineWidth * (0.01 * r.progress),
background: '#E2E4F6',
}}
className={'rounded-r'}
/>
</div>
</div>
<div className={'min-w-8 ml-auto'}>{r.value}</div>
</div>
))}
</div>
</ExCard>
);
}
export default ByUrl;

View file

@ -0,0 +1,53 @@
import React from 'react';
import {Avatar, Icon} from 'UI';
import ExCard from '../ExCard';
import ByComponent from './Component';
import {hashString} from "Types/session/session";
function ByUser(props: any) {
const rows = [
{
label: 'Demo User',
progress: 85,
value: '2.5K',
icon: <Avatar seed={hashString("a")}/>,
},
{
label: 'Admin User',
progress: 25,
value: '405',
icon: <Avatar seed={hashString("b")}/>,
},
{
label: 'Management User',
progress: 5,
value: '302',
icon: <Avatar seed={hashString("c")}/>,
},
{
label: 'Sales User',
progress: 3,
value: '194',
icon: <Avatar seed={hashString("d")}/>,
},
{
label: 'Marketing User',
progress: 1,
value: '57',
icon: <Avatar seed={hashString("e")}/>,
},
];
const lineWidth = 200;
return (
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
);
}
export default ByUser;

View file

@ -0,0 +1,67 @@
import ExCard from '../ExCard'
import React from 'react'
import CardSessionsByList from "Components/Dashboard/Widgets/CardSessionsByList";
function ByComponent({title, rows, lineWidth, onCard, type}: {
title: string
rows: {
label: string
progress: number
value: string
icon: React.ReactNode
}[]
onCard: (card: string) => void
type: string
lineWidth: number
}) {
const _rows = rows.map((r) => ({
...r,
name: r.label,
sessionCount: r.value,
})).slice(0, 4)
return (
<ExCard
title={title}
onCard={onCard}
type={type}
>
<div className={'flex gap-1 flex-col'}>
<CardSessionsByList list={_rows} selected={''} onClickHandler={() => null}/>
{/*{rows.map((r) => (*/}
{/* <div*/}
{/* className={*/}
{/* 'flex items-center gap-2 border-b border-dotted py-2 last:border-0 first:pt-0 last:pb-0'*/}
{/* }*/}
{/* >*/}
{/* <div>{r.icon}</div>*/}
{/* <div>{r.label}</div>*/}
{/* <div*/}
{/* style={{marginLeft: 'auto', marginRight: 20, display: 'flex'}}*/}
{/* >*/}
{/* <div*/}
{/* style={{*/}
{/* height: 2,*/}
{/* width: lineWidth * (0.01 * r.progress),*/}
{/* background: '#394EFF',*/}
{/* }}*/}
{/* className={'rounded-l'}*/}
{/* />*/}
{/* <div*/}
{/* style={{*/}
{/* height: 2,*/}
{/* width: lineWidth - lineWidth * (0.01 * r.progress),*/}
{/* background: '#E2E4F6',*/}
{/* }}*/}
{/* className={'rounded-r'}*/}
{/* />*/}
{/* </div>*/}
{/* <div className={'min-w-8'}>{r.value}</div>*/}
{/* </div>*/}
{/*))}*/}
</div>
</ExCard>
)
}
export default ByComponent

View file

@ -0,0 +1,15 @@
import React from 'react'
import ExCard from "./ExCard";
import { Errors } from "./Count";
function SessionsByErrors(props: any) {
return (
<ExCard
{...props}
>
<Errors />
</ExCard>
);
}
export default SessionsByErrors

View file

@ -0,0 +1,15 @@
import React from 'react'
import ExCard from "./ExCard";
import { Frustrations } from "./Count";
function SessionsByIssues(props: any) {
return (
<ExCard
{...props}
>
<Frustrations />
</ExCard>
);
}
export default SessionsByIssues

View file

@ -0,0 +1,86 @@
import { LinkOutlined } from '@ant-design/icons';
import React from 'react';
import { Circle } from './Count';
import ExCard from './ExCard';
function SlowestDomain(props: any) {
const rows = [
{
label: 'kroger.com',
value: '28,162 ms',
progress: 97,
icon: <LinkOutlined size={12} />,
},
{
label: 'instacart.com',
value: '3,165 ms',
progress: 60,
icon: <LinkOutlined size={12} />,
},
{
label: 'gifs.eco.br',
value: '1,503 ms',
progress: 40,
icon: <LinkOutlined size={12} />,
},
{
label: 'cdn.byintera.com',
value: '512 ms',
progress: 10,
icon: <LinkOutlined size={12} />,
},
{
label: 'analytics.twitter.com',
value: '110 ms',
progress: 5,
icon: <LinkOutlined size={12} />,
},
];
const lineWidth = 240;
return (
<ExCard
{...props}
>
<div className={'flex gap-1 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted last:border-0 py-2 first:pt-0 last:pb-0'
}
>
<Circle badgeType={2}>
{r.icon}
</Circle>
<div className={'ml-2 flex flex-col gap-0'}>
<div>{r.label}</div>
<div style={{ display: 'flex' }}>
<div
style={{
height: 2,
width: lineWidth * (0.01 * r.progress),
background: '#394EFF',
}}
className={'rounded-l'}
/>
<div
style={{
height: 2,
width: lineWidth - lineWidth * (0.01 * r.progress),
background: '#E2E4F6',
}}
className={'rounded-r'}
/>
</div>
</div>
<div className={'min-w-8 ml-auto'}>{r.value}</div>
</div>
))}
</div>
</ExCard>
);
}
export default SlowestDomain;

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

@ -0,0 +1,43 @@
import React from 'react';
import ExCard from './ExCard';
import AreaChartCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/AreaChartCard";
import CustomMetricLineChart from "Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart";
import {Styles} from "Components/Dashboard/Widgets/common";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
onClick?: any;
data?: any,
}
function ExampleTrend(props: Props) {
return (
<ExCard
{...props}
title={
<div className={'flex items-center gap-2'}>
<div>{props.title}</div>
</div>
}
>
{/*<AreaChartCard data={props.data} label={props.data?.label}/>*/}
<CustomMetricLineChart
data={props.data}
colors={Styles.customMetricColors}
params={{
density: 21,
}}
yaxis={
{...Styles.yaxis, domain: [0, 100]}
}
label={props.data?.label}
onClick={props.onClick}
/>
</ExCard>
);
}
export default ExampleTrend;

View file

@ -0,0 +1,56 @@
import React from 'react';
import CustomMetricOverviewChart from "Components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart";
import ExCard from "Components/Dashboard/components/DashboardList/NewDashModal/Examples/ExCard";
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
data?: any,
}
function WebVital(props: Props) {
const data = props.data || {
"value": 8.33316146432396,
"chart": [
{
"timestamp": 1718755200000,
"value": 9.37317620650954
},
{
"timestamp": 1718870399833,
"value": 6.294931643881294
},
{
"timestamp": 1718985599666,
"value": 7.103504928806133
},
{
"timestamp": 1719100799499,
"value": 7.946568201563857
},
{
"timestamp": 1719215999332,
"value": 8.941158674935712
},
{
"timestamp": 1719331199165,
"value": 10.180191693290734
},
{
"timestamp": 1719446398998,
"value": 0
}
],
"unit": "%"
}
return (
<ExCard
{...props}
>
<CustomMetricOverviewChart data={data}/>
</ExCard>
);
}
export default WebVital;

View file

@ -0,0 +1,60 @@
import React, {useEffect} from 'react';
import {Modal} from 'antd';
import SelectCard from './SelectCard';
import CreateCard from "Components/Dashboard/components/DashboardList/NewDashModal/CreateCard";
import colors from "tailwindcss/colors";
interface NewDashboardModalProps {
onClose: () => void;
open: boolean;
isAddingFromLibrary?: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose,
open,
isAddingFromLibrary = false,
}) => {
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
useEffect(() => {
return () => {
setStep(0);
}
}, [open]);
return (
<>
<Modal
open={open}
onCancel={onClose}
width={900}
destroyOnClose={true}
footer={null}
closeIcon={false}
styles={{
content: {
backgroundColor: colors.gray[100],
}
}}
>
<div className="flex flex-col gap-4" style={{
height: 700,
overflowY: 'auto',
overflowX: 'hidden',
}}>
{step === 0 && <SelectCard onClose={onClose}
selected={selectedCategory}
setSelectedCategory={setSelectedCategory}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}/>}
{step === 1 && <CreateCard onBack={() => setStep(0)}/>}
</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,137 @@
import React, {useMemo} from 'react';
import {Button, Input, Segmented, Space} from 'antd';
import {CARD_LIST, CARD_CATEGORIES, CardType} from './ExampleCards';
import {useStore} from 'App/mstore';
import Option from './Option';
import CardsLibrary from "Components/Dashboard/components/DashboardList/NewDashModal/CardsLibrary";
import {FUNNEL} from "App/constants/card";
interface SelectCardProps {
onClose: (refresh?: boolean) => void;
onCard: () => void;
isLibrary?: boolean;
selected?: string;
setSelectedCategory?: React.Dispatch<React.SetStateAction<string>>;
}
const SelectCard: React.FC<SelectCardProps> = (props: SelectCardProps) => {
const {onCard, isLibrary = false, selected, setSelectedCategory} = props;
const [selectedCards, setSelectedCards] = React.useState<number[]>([]);
const {metricStore, dashboardStore} = useStore();
const dashboardId = window.location.pathname.split('/')[3];
const [libraryQuery, setLibraryQuery] = React.useState<string>('');
const handleCardSelection = (card: string) => {
metricStore.init();
const selectedCard = CARD_LIST.find((c) => c.key === card) as CardType;
const cardData: any = {
metricType: selectedCard.cardType,
name: selectedCard.title,
metricOf: selectedCard.metricOf,
};
if (selectedCard.cardType === FUNNEL) {
cardData.series = []
cardData.series.filter = []
}
metricStore.merge(cardData);
metricStore.instance.resetDefaults();
onCard();
};
const cardItems = useMemo(() => {
return CARD_LIST.filter((card) => card.category === selected).map((card) => (
<div key={card.key} className={card.width ? `col-span-${card.width}` : 'col-span-2'}>
<card.example onCard={handleCardSelection}
type={card.key}
title={card.title}
data={card.data}
height={card.height}/>
</div>
));
}, [selected]);
const onCardClick = (cardId: number) => {
if (selectedCards.includes(cardId)) {
setSelectedCards(selectedCards.filter((id) => id !== cardId));
} else {
setSelectedCards([...selectedCards, cardId]);
}
}
const onAddSelected = () => {
const dashboard = dashboardStore.getDashboard(dashboardId);
dashboardStore.addWidgetToDashboard(dashboard!, selectedCards).finally(() => {
dashboardStore.fetch(dashboardId);
props.onClose(true);
});
}
return (
<>
<Space className="items-center justify-between">
<div className="text-xl leading-4 font-medium">
{dashboardId ? (isLibrary ? "Add Card" : "Create Card") : "Select a template to create a card"}
</div>
{isLibrary && (
<Space>
{selectedCards.length > 0 ? (
<Button type="primary" onClick={onAddSelected}>
Add {selectedCards.length} Selected
</Button>
) : ''}
<Input.Search
placeholder="Search"
onChange={(value) => setLibraryQuery(value.target.value)}
style={{width: 200}}
/>
</Space>
)}
</Space>
{!isLibrary && <CategorySelector setSelected={setSelectedCategory} selected={selected}/>}
{isLibrary ?
<CardsLibrary query={libraryQuery}
selectedList={selectedCards}
category={selected}
onCard={onCardClick}/> :
<ExampleCardsGrid items={cardItems}/>}
</>
);
};
interface CategorySelectorProps {
setSelected?: React.Dispatch<React.SetStateAction<string>>;
selected?: string;
}
const CategorySelector: React.FC<CategorySelectorProps> = ({setSelected, selected}) => (
<Segmented
options={CARD_CATEGORIES.map(({key, label, icon}) => ({
label: <Option key={key} label={label} Icon={icon}/>,
value: key,
}))}
value={selected}
onChange={setSelected}
className='w-fit'
/>
);
interface ExampleCardsGridProps {
items: JSX.Element[];
}
const ExampleCardsGrid: React.FC<ExampleCardsGridProps> = ({items}) => (
<div
className="w-full grid grid-cols-4 gap-4 overflow-scroll"
style={{maxHeight: 'calc(100vh - 100px)'}}
>
{items}
</div>
);
export default SelectCard;

View file

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

View file

@ -14,7 +14,7 @@ function DashboardOptions(props: Props) {
const { editHandler, deleteHandler, renderReport, isEnterprise, isTitlePresent } = props;
const menuItems = [
{ icon: 'pencil', text: 'Rename', onClick: () => editHandler(true) },
{ icon: 'text-paragraph', text: `${!isTitlePresent ? 'Add' : 'Edit'} Description`, onClick: () => editHandler(false) },
// { icon: 'text-paragraph', text: `${!isTitlePresent ? 'Add' : 'Edit'} Description`, onClick: () => editHandler(false) },
{ icon: 'users', text: 'Visibility & Access', onClick: editHandler },
{ icon: 'trash', text: 'Delete', onClick: deleteHandler },
{ icon: 'pdf-download', text: 'Download Report', onClick: renderReport, disabled: !isEnterprise, tooltipTitle: ENTERPRISE_REQUEIRED }
@ -23,7 +23,6 @@ function DashboardOptions(props: Props) {
return (
<ItemMenu
bold
label="More Options"
items={menuItems}
/>
);

View file

@ -1,7 +1,8 @@
import { useObserver } from 'mobx-react-lite';
import {useObserver} from 'mobx-react-lite';
import React from 'react';
import { Button, Modal, Form, Icon } from 'UI';
import { useStore } from 'App/mstore'
import {Button, Modal, Form, Icon} from 'UI';
import {useStore} from 'App/mstore'
import Select from 'Shared/Select';
interface Props {
@ -9,9 +10,10 @@ interface Props {
show: boolean;
closeHandler?: () => void;
}
function DashboardSelectionModal(props: Props) {
const { show, metricId, closeHandler } = props;
const { dashboardStore } = useStore();
const {show, metricId, closeHandler} = props;
const {dashboardStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
@ -41,16 +43,16 @@ function DashboardSelectionModal(props: Props) {
}, [])
return useObserver(() => (
<Modal size="small" open={ show } onClose={closeHandler}>
<Modal size="small" open={show} onClose={closeHandler}>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Add to selected dashboard' }</div>
<Icon
<div className='text-xl font-medium'>{'Add to selected dashboard'}</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="14"
name="close"
onClick={ closeHandler }
onClick={closeHandler}
/>
</Modal.Header>
@ -60,21 +62,21 @@ function DashboardSelectionModal(props: Props) {
<Select
options={dashboardOptions}
defaultValue={dashboardOptions[0].value}
onChange={({ value }: any) => setSelectedId(value.value)}
onChange={({value}: any) => setSelectedId(value.value)}
/>
</Form.Field>
</Modal.Content>
<Modal.Footer>
<Button
variant="primary"
onClick={ onSave }
className="float-left mr-2"
onClick={onSave}
className="float-left mr-2 "
>
Add
</Button>
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button>
<Button className="mr-2" onClick={closeHandler}>{'Cancel'}</Button>
</Modal.Footer>
</Modal>
</Modal>
));
}

View file

@ -2,4 +2,4 @@
& > tippy-popper > tippy-tooltip {
padding: 0!important;
}
}
}

View file

@ -1,111 +1,114 @@
import React, { useEffect } from 'react';
import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore';
import { Loader } from 'UI';
import { withSiteId } from 'App/routes';
import React, {useEffect} from 'react';
import {observer} from 'mobx-react-lite';
import {useStore} from 'App/mstore';
import {Loader} from 'UI';
import {withSiteId} from 'App/routes';
import withModal from 'App/components/Modal/withModal';
import DashboardWidgetGrid from '../DashboardWidgetGrid';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { useModal } from 'App/components/Modal';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {useModal} from 'App/components/Modal';
import DashboardModal from '../DashboardModal';
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
import withPageTitle from 'HOCs/withPageTitle';
import withReport from 'App/components/hocs/withReport';
import DashboardHeader from '../DashboardHeader';
import {useHistory} from "react-router";
interface IProps {
siteId: string;
dashboardId: any;
renderReport?: any;
siteId: string;
dashboardId: any;
renderReport?: any;
}
type Props = IProps & RouteComponentProps;
function DashboardView(props: Props) {
const { siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { showModal, hideModal } = useModal();
const {siteId, dashboardId} = props;
const {dashboardStore} = useStore();
const {showModal, hideModal} = useModal();
const history = useHistory();
const showAlertModal = dashboardStore.showAlertModal;
const loading = dashboardStore.fetchingDashboard;
const dashboard: any = dashboardStore.selectedDashboard;
const showAlertModal = dashboardStore.showAlertModal;
const loading = dashboardStore.fetchingDashboard;
const dashboard: any = dashboardStore.selectedDashboard;
const queryParams = new URLSearchParams(props.location.search);
const queryParams = new URLSearchParams(location.search);
const trimQuery = () => {
if (!queryParams.has('modal')) return;
queryParams.delete('modal');
props.history.replace({
search: queryParams.toString(),
});
};
const trimQuery = () => {
if (!queryParams.has('modal')) return;
queryParams.delete('modal');
history.replace({
search: queryParams.toString(),
});
};
useEffect(() => {
if (showAlertModal) {
showModal(
<AlertFormModal
showModal={showAlertModal}
onClose={() => {
hideModal();
dashboardStore.toggleAlertModal(false)
}}
/>,
{ right: false, width: 580 },
() => dashboardStore.toggleAlertModal(false)
)
}
}, [showAlertModal])
useEffect(() => {
if (showAlertModal) {
showModal(
<AlertFormModal
showModal={showAlertModal}
onClose={() => {
hideModal();
dashboardStore.toggleAlertModal(false)
}}
/>,
{right: false, width: 580},
() => dashboardStore.toggleAlertModal(false)
)
}
}, [showAlertModal])
const pushQuery = () => {
if (!queryParams.has('modal')) props.history.push('?modal=addMetric');
};
const pushQuery = () => {
if (!queryParams.has('modal')) history.push('?modal=addMetric');
};
useEffect(() => {
if (queryParams.has('modal')) {
onAddWidgets();
trimQuery();
}
}, []);
useEffect(() => {
if (queryParams.has('modal')) {
onAddWidgets();
trimQuery();
}
}, []);
useEffect(() => {
const isExists = dashboardStore.getDashboardById(dashboardId);
if (!isExists) {
props.history.push(withSiteId(`/dashboard`, siteId));
}
}, [dashboardId]);
useEffect(() => {
const isExists = dashboardStore.getDashboardById(dashboardId);
if (!isExists) {
history.push(withSiteId(`/dashboard`, siteId));
}
}, [dashboardId]);
useEffect(() => {
if (!dashboard || !dashboard.dashboardId) return;
dashboardStore.fetch(dashboard.dashboardId);
}, [dashboard]);
useEffect(() => {
if (!dashboard || !dashboard.dashboardId) return;
dashboardStore.fetch(dashboard.dashboardId);
}, [dashboard]);
const onAddWidgets = () => {
dashboardStore.initDashboard(dashboard);
showModal(
<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId} />,
{ right: true }
const onAddWidgets = () => {
dashboardStore.initDashboard(dashboard);
showModal(
<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId}/>,
{right: true}
);
};
if (!dashboard) return null;
return (
<Loader loading={loading}>
<div style={{maxWidth: '1360px', margin: 'auto'}}>
{/* @ts-ignore */}
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/>
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}
onEditHandler={onAddWidgets}
id="report"
/>
</div>
</Loader>
);
};
if (!dashboard) return null;
return (
<Loader loading={loading}>
<div style={{ maxWidth: '1360px', margin: 'auto' }}>
{/* @ts-ignore */}
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId} />
<DashboardWidgetGrid
siteId={siteId}
dashboardId={dashboardId}
onEditHandler={onAddWidgets}
id="report"
/>
</div>
</Loader>
);
}
// @ts-ignore
export default withPageTitle('Dashboards - OpenReplay')(
withReport(withRouter(withModal(observer(DashboardView))))
withReport(withModal(observer(DashboardView)))
);

View file

@ -1,112 +1,117 @@
import React from 'react';
import { useStore } from 'App/mstore';
import {useStore} from 'App/mstore';
import WidgetWrapper from '../WidgetWrapper';
import { NoContent, Loader, Icon } from 'UI';
import { useObserver } from 'mobx-react-lite';
import {NoContent, Loader, Icon} from 'UI';
import {useObserver} from 'mobx-react-lite';
import Widget from 'App/mstore/types/widget';
import MetricTypeList from '../MetricTypeList';
import WidgetWrapperNew from "Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew";
import {Empty} from "antd";
interface Props {
siteId: string;
dashboardId: string;
onEditHandler: () => void;
id?: string;
siteId: string;
dashboardId: string;
onEditHandler: () => void;
id?: string;
}
function DashboardWidgetGrid(props: Props) {
const { dashboardId, siteId } = props;
const { dashboardStore } = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
const dashboard = dashboardStore.selectedDashboard;
const list = useObserver(() => dashboard?.widgets);
const smallWidgets: Widget[] = [];
const regularWidgets: Widget[] = [];
const {dashboardId, siteId} = props;
const {dashboardStore} = useStore();
const loading = useObserver(() => dashboardStore.isLoading);
const dashboard = dashboardStore.selectedDashboard;
const list = useObserver(() => dashboard?.widgets);
const smallWidgets: Widget[] = [];
const regularWidgets: Widget[] = [];
list.forEach((item) => {
if (item.config.col === 1) {
smallWidgets.push(item);
} else {
regularWidgets.push(item);
}
});
const smallWidgetsLen = smallWidgets.length;
return useObserver(() => (
// @ts-ignore
<Loader loading={loading}>
<NoContent
show={list.length === 0}
icon="no-metrics-chart"
title={
<div className="bg-white rounded">
<div className="border-b p-5">
<div className="text-2xl font-normal">
There are no cards in this dashboard
</div>
<div className="text-base font-normal">
Create a card from any of the below types or pick an existing one from your library.
</div>
</div>
<div className="grid grid-cols-4 p-8 gap-2">
<MetricTypeList dashboardId={parseInt(dashboardId)} siteId={siteId} />
</div>
</div>
list?.forEach((item) => {
if (item.config.col === 1) {
smallWidgets.push(item);
} else {
regularWidgets.push(item);
}
>
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>{smallWidgets.length > 0 ? (
<>
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
<Icon name="grid-horizontal" size={26} />
Web Vitals
</div>
});
{smallWidgets &&
smallWidgets.map((item: any, index: any) => (
<React.Fragment key={item.widgetId}>
<WidgetWrapper
index={index}
widget={item}
moveListItem={(dragIndex: any, hoverIndex: any) =>
dashboard.swapWidgetPosition(dragIndex, hoverIndex)
const smallWidgetsLen = smallWidgets.length;
}dashboardId={dashboardId}
siteId={siteId}
isWidget={true}
grid="vitals"
/>
</React.Fragment>
))}
return useObserver(() => (
// @ts-ignore
list?.length === 0 ? <Empty description="No cards in this dashboard"/> : (
<Loader loading={loading}>
<NoContent
show={list?.length === 0}
icon="no-metrics-chart"
title={
<div className="bg-white rounded-lg">
<div className="border-b p-5">
<div className="text-2xl font-normal">
There are no cards in this dashboard
</div>
<div className="text-base font-normal">
Create a card from any of the below types or pick an existing one from your library.
</div>
</div>
{/*<div className="grid grid-cols-4 p-8 gap-2">*/}
{/* <MetricTypeList dashboardId={parseInt(dashboardId)} siteId={siteId}/>*/}
{/*</div>*/}
</div>
}
>
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>{smallWidgets.length > 0 ? (
<>
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
<Icon name="grid-horizontal" size={26}/>
Web Vitals
</div>
</>
) : null}
{smallWidgets &&
smallWidgets.map((item: any, index: any) => (
<React.Fragment key={item.widgetId}>
<WidgetWrapperNew
index={index}
widget={item}
moveListItem={(dragIndex: any, hoverIndex: any) =>
dashboard?.swapWidgetPosition(dragIndex, hoverIndex)
{smallWidgets.length > 0 && regularWidgets.length > 0 ? (
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
<Icon name="grid-horizontal" size={26} />
All Cards
</div>
) : null}
} dashboardId={dashboardId}
siteId={siteId}
isWidget={true}
grid="vitals"
/>
</React.Fragment>
))}
{regularWidgets &&
regularWidgets.map((item: any, index: any) => (
<React.Fragment key={item.widgetId}>
<WidgetWrapper
index={smallWidgetsLen + index}
widget={item}
moveListItem={(dragIndex: any, hoverIndex: any) =>
dashboard.swapWidgetPosition(dragIndex, hoverIndex)
}
dashboardId={dashboardId}
siteId={siteId}
isWidget={true}
grid="other"
/>
</React.Fragment>
))}
</div>
</NoContent>
</Loader>
));
</>
) : null}
{smallWidgets.length > 0 && regularWidgets.length > 0 ? (
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
<Icon name="grid-horizontal" size={26}/>
All Cards
</div>
) : null}
{regularWidgets &&
regularWidgets.map((item: any, index: any) => (
<React.Fragment key={item.widgetId}>
<WidgetWrapperNew
index={smallWidgetsLen + index}
widget={item}
moveListItem={(dragIndex: any, hoverIndex: any) =>
dashboard?.swapWidgetPosition(dragIndex, hoverIndex)
}
dashboardId={dashboardId}
siteId={siteId}
isWidget={true}
grid="other"
/>
</React.Fragment>
))}
</div>
</NoContent>
</Loader>
)
));
}
export default DashboardWidgetGrid;

View file

@ -0,0 +1,33 @@
import React from 'react';
import FilterSelection from "Shared/Filters/FilterSelection/FilterSelection";
import {PlusIcon} from "lucide-react";
import {Button} from "antd";
import {useStore} from "App/mstore";
interface Props {
series: any;
excludeFilterKeys: Array<string>;
}
function AddStepButton({series, excludeFilterKeys}: Props) {
const {metricStore} = useStore();
const metric: any = metricStore.instance;
const onAddFilter = (filter: any) => {
series.filter.addFilter(filter);
metric.updateKey('hasChanged', true)
}
return (
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
excludeFilterKeys={excludeFilterKeys}
>
<Button type="link" className='border-none hover:bg-blue-50' icon={<PlusIcon size={16}/>} size="small">
ADD STEP
</Button>
</FilterSelection>
);
}
export default AddStepButton;

View file

@ -1,119 +1,176 @@
import React, { useState } from 'react';
import React, {useState} from 'react';
import FilterList from 'Shared/Filters/FilterList';
import { Button, Icon } from 'UI';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SeriesName from './SeriesName';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import {observer} from 'mobx-react-lite';
import ExcludeFilters from './ExcludeFilters';
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
import {Button, Space} from "antd";
import {ChevronDown, ChevronUp, Trash} from "lucide-react";
const FilterCountLabels = observer((props: { filters: any, toggleExpand: any }) => {
const events = props.filters.filter((i: any) => i && i.isEvent).length;
const filters = props.filters.filter((i: any) => i && !i.isEvent).length;
return <div className="flex items-center">
<Space>
{events > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${events} Event${events > 1 ? 's' : ''}`}
</Button>
)}
{filters > 0 && (
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
{`${filters} Filter${filters > 1 ? 's' : ''}`}
</Button>
)}
</Space>
</div>;
});
const FilterSeriesHeader = observer((props: {
expanded: boolean,
hidden: boolean,
seriesIndex: number,
series: any,
onRemove: (seriesIndex: any) => void,
canDelete: boolean | undefined,
toggleExpand: () => void
}) => {
const onUpdate = (name: any) => {
props.series.update('name', name)
}
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
<Space className="mr-auto" size={30}>
<SeriesName
seriesIndex={props.seriesIndex}
name={props.series.name}
onUpdate={onUpdate}
/>
{!props.expanded &&
<FilterCountLabels filters={props.series.filter.filters} toggleExpand={props.toggleExpand}/>}
</Space>
<Space>
<Button onClick={props.onRemove}
size="small"
disabled={!props.canDelete}
icon={<Trash size={14}/>}/>
<Button onClick={props.toggleExpand}
size="small"
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
</div>;
})
interface Props {
seriesIndex: number;
series: any;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
supportsEmpty?: boolean;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
excludeFilterKeys?: Array<string>;
canExclude?: boolean;
seriesIndex: number;
series: any;
onRemoveSeries: (seriesIndex: any) => void;
canDelete?: boolean;
supportsEmpty?: boolean;
hideHeader?: boolean;
emptyMessage?: any;
observeChanges?: () => void;
excludeFilterKeys?: Array<string>;
canExclude?: boolean;
expandable?: boolean;
}
function FilterSeries(props: Props) {
const {
observeChanges = () => {},
canDelete,
hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,
} = props;
const [expanded, setExpanded] = useState(true);
const { series, seriesIndex } = props;
const {
observeChanges = () => {
},
canDelete,
hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,
expandable = false
} = props;
const [expanded, setExpanded] = useState(!expandable);
const {series, seriesIndex} = props;
const onAddFilter = (filter: any) => {
series.filter.addFilter(filter);
observeChanges();
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
series.filter.updateFilter(filterIndex, filter);
observeChanges();
};
const onUpdateFilter = (filterIndex: any, filter: any) => {
series.filter.updateFilter(filterIndex, filter);
observeChanges();
};
const onFilterMove = (newFilters: any) => {
series.filter.replaceFilters(newFilters.toArray())
observeChanges();
}
const onFilterMove = (newFilters: any) => {
series.filter.replaceFilters(newFilters.toArray())
observeChanges();
}
const onChangeEventsOrder = (_: any, {name, value}: any) => {
console.log(name, value)
series.filter.updateKey(name, value);
observeChanges();
};
const onChangeEventsOrder = (_: any, { name, value }: any) => {
series.filter.updateKey(name, value);
observeChanges();
};
const onRemoveFilter = (filterIndex: any) => {
series.filter.removeFilter(filterIndex);
observeChanges();
};
const onRemoveFilter = (filterIndex: any) => {
series.filter.removeFilter(filterIndex);
observeChanges();
};
return (
<div className="border rounded-lg shadow-sm bg-white">
{canExclude && <ExcludeFilters filter={series.filter}/>}
return (
<div className="border rounded bg-white">
{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)}
/>
</div>
<div className="flex items-center cursor-pointer">
<div onClick={props.onRemoveSeries} className={cn('ml-3', { disabled: !canDelete })}>
<Icon name="trash" size="16" />
</div>
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
{expanded && (
<>
<div className="p-5">
{series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>
{!hideHeader && (
<FilterSeriesHeader hidden={hideHeader}
seriesIndex={seriesIndex}
series={series}
onRemove={props.onRemoveSeries}
canDelete={canDelete}
expanded={expanded}
toggleExpand={() => setExpanded(!expanded)}/>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6">
<FilterSelection
filter={undefined}
onFilterClick={onAddFilter}
excludeFilterKeys={excludeFilterKeys}
>
<Button variant="text-primary" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div>
</div>
</>
)}
</div>
);
{expandable && !expanded && (
<Space className="justify-between w-full px-5 py-2">
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
)}
{expanded && (
<>
<div className="p-5">
{series.filter.filters.length > 0 ? (
<FilterList
filter={series.filter}
onUpdateFilter={onUpdateFilter}
onRemoveFilter={onRemoveFilter}
onChangeEventsOrder={onChangeEventsOrder}
supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys}
actions={[
expandable && (
<Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
)
]}
/>
) : (
<div className="color-gray-medium">{emptyMessage}</div>
)}
</div>
<div className="border-t h-12 flex items-center">
<div className="-mx-4 px-5">
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
</div>
</div>
</>
)}
</div>
);
}
export default observer(FilterSeries);

View file

@ -59,7 +59,7 @@ function FunnelIssues() {
return useObserver(() => (
<div className="my-8 bg-white rounded p-4 border">
<div className="flex">
<h1 className="font-medium text-2xl">Most significant issues <span className="font-normal">identified in this funnel</span></h1>
<h2 className="font-medium text-xl">Most significant issues <span className="font-normal">identified in this funnel</span></h2>
</div>
<div className="my-6 flex justify-between items-start">
<FunnelIssuesDropdown />

View file

@ -1,135 +1,146 @@
import React from 'react';
import { PageTitle, Button, Toggler, Icon } from "UI";
import { Segmented } from 'antd';
import {PageTitle, Button, Toggler, Icon} from "UI";
import {Segmented} from 'antd';
import MetricsSearch from '../MetricsSearch';
import Select from 'Shared/Select';
import { useStore } from 'App/mstore';
import { observer, useObserver } from 'mobx-react-lite';
import { DROPDOWN_OPTIONS } from 'App/constants/card';
import {useStore} from 'App/mstore';
import {observer, useObserver} from 'mobx-react-lite';
import {DROPDOWN_OPTIONS} from 'App/constants/card';
import AddCardModal from 'Components/Dashboard/components/AddCardModal';
import { useModal } from 'Components/Modal';
import {useModal} from 'Components/Modal';
import AddCardSelectionModal from "Components/Dashboard/components/AddCardSelectionModal";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
function MetricViewHeader({ siteId }: { siteId: string }) {
const { metricStore } = useStore();
const filter = metricStore.filter;
const { showModal } = useModal();
function MetricViewHeader({siteId}: { siteId: string }) {
const {metricStore} = useStore();
const filter = metricStore.filter;
const {showModal} = useModal();
const [showAddCardModal, setShowAddCardModal] = React.useState(false);
return (
<div>
<div className='flex items-center justify-between px-6'>
<div className='flex items-baseline mr-3'>
<PageTitle title='Cards' className='' />
return (
<div>
<div className='flex items-center justify-between px-6'>
<div className='flex items-baseline mr-3'>
<PageTitle title='Cards' className=''/>
</div>
<div className='ml-auto flex items-center'>
<Button variant='primary'
// onClick={() => showModal(<AddCardModal siteId={siteId}/>, {right: true})}
onClick={() => setShowAddCardModal(true)}
>New Card</Button>
<div className='ml-4 w-1/4' style={{minWidth: 300}}>
<MetricsSearch/>
</div>
</div>
</div>
<div className='border-y px-6 py-1 mt-2 flex items-center w-full justify-between'>
<div className='items-center flex gap-4'>
<Toggler
label='My Cards'
checked={filter.showMine}
name='test'
className='font-medium mr-2'
onChange={() =>
metricStore.updateKey('filter', {...filter, showMine: !filter.showMine})
}
/>
<Select
options={[{label: 'All Types', value: 'all'}, ...DROPDOWN_OPTIONS]}
name='type'
defaultValue={filter.type}
onChange={({value}) =>
metricStore.updateKey('filter', {...filter, type: value.value})
}
plain={true}
isSearchable={true}
/>
<DashboardDropdown
plain={true}
onChange={(value: any) =>
metricStore.updateKey('filter', {...filter, dashboard: value})
}
/>
</div>
<div className='flex items-center'>
<ListViewToggler/>
<Select
options={[
{label: 'Newest', value: 'desc'},
{label: 'Oldest', value: 'asc'}
]}
name='sort'
defaultValue={metricStore.sort.by}
onChange={({value}) => metricStore.updateKey('sort', {by: value.value})}
plain={true}
className='ml-4'
/>
</div>
{/*<AddCardSelectionModal open={showAddCardModal}/>*/}
<NewDashboardModal
onClose={() => setShowAddCardModal(false)}
open={showAddCardModal}
isCreatingNewCard={true}
/>
</div>
</div>
<div className='ml-auto flex items-center'>
<Button variant='primary'
onClick={() => showModal(<AddCardModal siteId={siteId} />, { right: true })}
>New Card</Button>
<div className='ml-4 w-1/4' style={{ minWidth: 300 }}>
<MetricsSearch />
</div>
</div>
</div>
<div className='border-y px-6 py-1 mt-2 flex items-center w-full justify-between'>
<div className='items-center flex gap-4'>
<Toggler
label='My Cards'
checked={filter.showMine}
name='test'
className='font-medium mr-2'
onChange={() =>
metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine })
}
/>
<Select
options={[{ label: 'All Types', value: 'all' }, ...DROPDOWN_OPTIONS]}
name='type'
defaultValue={filter.type}
onChange={({ value }) =>
metricStore.updateKey('filter', { ...filter, type: value.value })
}
plain={true}
isSearchable={true}
/>
<DashboardDropdown
plain={true}
onChange={(value: any) =>
metricStore.updateKey('filter', { ...filter, dashboard: value })
}
/>
</div>
<div className='flex items-center'>
<ListViewToggler />
<Select
options={[
{ label: 'Newest', value: 'desc' },
{ label: 'Oldest', value: 'asc' }
]}
name='sort'
defaultValue={metricStore.sort.by}
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })}
plain={true}
className='ml-4'
/>
</div>
</div>
</div>
);
);
}
export default observer(MetricViewHeader);
function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) {
const { dashboardStore, metricStore } = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
value: i.dashboardId
}));
function DashboardDropdown({onChange, plain = false}: { plain?: boolean; onChange: any }) {
const {dashboardStore, metricStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id,
label: i.name,
value: i.dashboardId
}));
return (
<Select
isSearchable={true}
placeholder='Filter by Dashboard'
plain={plain}
options={dashboardOptions}
value={metricStore.filter.dashboard}
onChange={({ value }: any) => onChange(value)}
isMulti={true}
/>
);
return (
<Select
isSearchable={true}
placeholder='Filter by Dashboard'
plain={plain}
options={dashboardOptions}
value={metricStore.filter.dashboard}
onChange={({value}: any) => onChange(value)}
isMulti={true}
/>
);
}
function ListViewToggler() {
const { metricStore } = useStore();
const listView = useObserver(() => metricStore.listView);
return (
<div className='flex items-center'>
<Segmented
options={[
{
label: <div className={'flex items-center gap-2'}>
<Icon name={'list-alt'} color={'inherit'} />
<div>List</div>
</div>,
value: 'list'
},
{
label: <div className={'flex items-center gap-2'}>
<Icon name={'grid'} color={'inherit'} />
<div>Grid</div>
</div>,
value: 'grid'
}
]}
onChange={(val) => {
metricStore.updateKey('listView', val === 'list')
}}
value={listView ? 'list' : 'grid'}
/>
</div>
);
const {metricStore} = useStore();
const listView = useObserver(() => metricStore.listView);
return (
<div className='flex items-center'>
<Segmented
options={[
{
label: <div className={'flex items-center gap-2'}>
<Icon name={'list-alt'} color={'inherit'}/>
<div>List</div>
</div>,
value: 'list'
},
{
label: <div className={'flex items-center gap-2'}>
<Icon name={'grid'} color={'inherit'}/>
<div>Grid</div>
</div>,
value: 'grid'
}
]}
onChange={(val) => {
metricStore.updateKey('listView', val === 'list')
}}
value={listView ? 'list' : 'grid'}
/>
</div>
);
}

View file

@ -9,7 +9,7 @@ interface Props {
}
function MetricsView({ siteId }: Props) {
return useObserver(() => (
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded pt-4 border">
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded-lg shadow-sm pt-4 border">
<MetricViewHeader siteId={siteId} />
<MetricsList siteId={siteId} />
</div>

View file

@ -1,13 +0,0 @@
import React from 'react';
import SessionListItem from '../SessionListItem';
function SessionList(props) {
return (
<div>
Session list
<SessionListItem session={{}} />
</div>
);
}
export default SessionList;

View file

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

View file

@ -1,14 +0,0 @@
import React from 'react';
interface Props {
session: any;
}
function SessionListItem(props: Props) {
return (
<div>
Session list item
</div>
);
}
export default SessionListItem;

View file

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

View file

@ -1,12 +0,0 @@
import React from 'react';
import SessionList from '../SessionList';
function SessionWidget(props) {
return (
<div>
<SessionList />
</div>
);
}
export default SessionWidget;

View file

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

View file

@ -1,5 +1,5 @@
import React, {useState, useRef, useEffect} from 'react';
import CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
import CustomMetricLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart';
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
@ -33,6 +33,7 @@ import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
import SankeyChart from 'Shared/Insights/SankeyChart';
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
interface Props {
metric: any;
@ -140,7 +141,7 @@ function WidgetChart(props: Props) {
if (metricType === TIMESERIES) {
if (viewType === 'lineChart') {
return (
<CustomMetriLineChart
<CustomMetricLineChart
data={data}
colors={colors}
params={params}
@ -181,11 +182,17 @@ function WidgetChart(props: Props) {
}
if (viewType === TABLE) {
return (
<CustomMetricTable
metric={metric} data={data[0]}
<SessionsBy
metric={metric}
data={data[0]}
onClick={onChartClick}
isTemplate={isTemplate}
/>
// <CustomMetricTable
// metric={metric} data={data[0]}
// onClick={onChartClick}
// isTemplate={isTemplate}
// />
);
} else if (viewType === 'pieChart') {
return (
@ -229,7 +236,7 @@ function WidgetChart(props: Props) {
if (metricType === RETENTION) {
if (viewType === 'trend') {
return (
<CustomMetriLineChart
<CustomMetricLineChart
data={data}
colors={colors}
params={params}

View file

@ -1,13 +1,13 @@
import React from 'react';
import SelectDateRange from 'Shared/SelectDateRange';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import {useStore} from 'App/mstore';
import {useObserver} from 'mobx-react-lite';
import {Space} from "antd";
interface Props {
}
function WidgetDateRange(props: Props) {
const { dashboardStore } = useStore();
function WidgetDateRange({
label = 'Time Range',
}: any) {
const {dashboardStore} = useStore();
const period = useObserver(() => dashboardStore.drillDownPeriod);
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
@ -21,16 +21,15 @@ function WidgetDateRange(props: Props) {
}
return (
<>
<span className="mr-1 color-gray-medium">Time Range</span>
<Space>
{label && <span className="mr-1 color-gray-medium">{label}</span>}
<SelectDateRange
period={period}
// onChange={(period: any) => metric.setPeriod(period)}
onChange={onChangePeriod}
right={true}
/>
</>
/>
</Space>
);
}
export default WidgetDateRange;
export default WidgetDateRange;

View file

@ -0,0 +1,330 @@
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, Segmented} 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 tableOptions = metricOf.filter((i) => i.type === 'table');
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 MetricTabs = ({metric, writeOption}: any) => {
if (![TABLE].includes(metric.metricType)) return null;
const onChange = (value: string) => {
console.log('value', value);
writeOption({
value: {
value
}, name: 'metricOf'
});
}
return (
<Segmented options={tableOptions} onChange={onChange} selected={metric.metricOf}/>
)
}
const MetricOptions = ({metric, writeOption}: any) => {
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 = observer(({metric}: any) => (
<Card styles={{
body: {padding: '4px 20px'},
header: {padding: '4px 20px', fontSize: '14px', fontWeight: 'bold', borderBottom: 'none'},
}}
title={metric.startType === 'start' ? 'Start Point' : 'End Point'}
>
<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>
</Card>
));
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'}}} className='rounded-full shadow-sm'>
<Button
type='link'
onClick={() => metric.addSeries()}
disabled={!canAddSeries}
size="small"
className='block w-full'
>
<Space>
<AudioWaveform size={16}/>
New Chart Series
</Space>
</Button>
</Card>
)}
</div>
);
});
interface RouteParams {
siteId: string;
dashboardId: string;
metricId: string;
}
interface CardBuilderProps {
siteId: string;
dashboardId?: string;
metricId?: string;
}
const CardBuilder = observer((props: CardBuilderProps) => {
const history = useHistory();
const {siteId, dashboardId, metricId} = props;
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';
console.log('metric', metric);
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 className="flex gap-6 flex-col">
{/*<MetricOptions*/}
{/* metric={metric}*/}
{/* writeOption={writeOption}*/}
{/*/>*/}
{/*<MetricTabs 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;

View file

@ -1,362 +1,367 @@
import React, { useEffect, useState } from 'react';
import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { Button, Icon, confirm, Tooltip } from 'UI';
import React, {useEffect, useState} from 'react';
import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions';
import {FilterKey} from 'Types/filter/filterType';
import {useStore} from 'App/mstore';
import {observer} from 'mobx-react-lite';
import {Button, Icon, confirm, Tooltip} from 'UI';
import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select';
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes';
import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes';
import MetricTypeDropdown from './components/MetricTypeDropdown';
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
import {
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
USER_PATH,
RETENTION
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
ERRORS,
RESOURCE_MONITORING,
PERFORMANCE,
WEB_VITALS,
INSIGHTS,
USER_PATH,
RETENTION
} from 'App/constants/card';
import { eventKeys } from 'App/types/filter/newFilter';
import { renderClickmapThumbnail } from './renderMap';
import {eventKeys} from 'App/types/filter/newFilter';
import {renderClickmapThumbnail} from './renderMap';
import Widget from 'App/mstore/types/widget';
import FilterItem from 'Shared/Filters/FilterItem';
import { Input } from 'antd'
import {Input} from 'antd'
interface Props {
history: any;
match: any;
onDelete: () => void;
history: any;
match: any;
onDelete: () => void;
expanded?: boolean;
}
function WidgetForm(props: Props) {
const {
history,
match: {
params: { siteId, dashboardId }
}
} = props;
const [aiQuery, setAiQuery] = useState('')
const [aiAskChart, setAiAskChart] = useState('')
const { metricStore, dashboardStore, aiFiltersStore } = useStore();
const isSaving = metricStore.isSaving;
const metric: any = metricStore.instance;
const [initialInstance, setInitialInstance] = useState();
const {
history,
match: {
params: {siteId, dashboardId}
}
} = props;
const [aiQuery, setAiQuery] = useState('')
const [aiAskChart, setAiAskChart] = useState('')
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const isSaving = metricStore.isSaving;
const metric: any = metricStore.instance;
const [initialInstance, setInitialInstance] = useState();
const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
const tableOptions = metricOf.filter((i) => i.type === 'table');
const isTable = metric.metricType === TABLE;
const 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 && i.isEvent).length;
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
const tableOptions = metricOf.filter((i) => i.type === 'table');
const isTable = metric.metricType === TABLE;
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 && i.isEvent).length;
const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(
metric.metricType
);
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(
metric.metricType
);
const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : [];
const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : [];
useEffect(() => {
if (!!metric && !initialInstance) {
setInitialInstance(metric.toJson());
}
}, [metric]);
useEffect(() => {
if (!!metric && !initialInstance) {
setInitialInstance(metric.toJson());
}
}, [metric]);
const writeOption = ({ value, name }: { value: any; name: any }) => {
value = Array.isArray(value) ? value : value.value;
const obj: any = { [name]: value };
const writeOption = ({value, name}: { value: any; name: any }) => {
value = Array.isArray(value) ? value : value.value;
const obj: any = {[name]: value};
if (name === 'metricType') {
switch (value) {
case TIMESERIES:
obj.metricOf = timeseriesOptions[0].value;
break;
case TABLE:
obj.metricOf = tableOptions[0].value;
break;
}
if (name === 'metricType') {
switch (value) {
case TIMESERIES:
obj.metricOf = timeseriesOptions[0].value;
break;
case TABLE:
obj.metricOf = tableOptions[0].value;
break;
}
}
metricStore.merge(obj);
};
const onSave = async () => {
const wasCreating = !metric.exists();
if (isClickmap) {
try {
metric.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(metric);
setInitialInstance(metric.toJson());
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
);
void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
}
}
};
const onDelete = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this card?`
})
) {
metricStore.delete(metric).then(props.onDelete);
}
};
const undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
const fetchResults = () => {
aiFiltersStore.getCardFilters(aiQuery, metric.metricType)
.then((f) => {
metric.createSeries(f.filters);
})
};
const fetchChartData = () => {
void aiFiltersStore.getCardData(aiAskChart, metric.toJson())
}
metricStore.merge(obj);
};
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchResults();
}
};
const handleChartKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchChartData();
}
};
const onSave = async () => {
const wasCreating = !metric.exists();
if (isClickmap) {
try {
metric.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(metric);
setInitialInstance(metric.toJson());
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
);
void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
}
}
};
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return (
<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}/>
const onDelete = async () => {
if (
await confirm({
header: 'Confirm',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this card?`
})
) {
metricStore.delete(metric).then(props.onDelete);
}
};
{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'
/>
const undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
<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'
/>
</>
)}
const fetchResults = () => {
aiFiltersStore.getCardFilters(aiQuery, metric.metricType)
.then((f) => {
metric.createSeries(f.filters);
})
};
{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={true}
placeholder='All Issues'
/>
</>
)}
const fetchChartData = () => {
void aiFiltersStore.getCardData(aiAskChart, metric.toJson())
}
{metric.metricType === INSIGHTS && (
<>
<span className='mx-3'>of</span>
<Select
name='metricValue'
options={issueCategories}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder='All Categories'
/>
</>
)}
const handleKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchResults();
}
};
const handleChartKeyDown = (event: any) => {
if (event.key === 'Enter') {
fetchChartData();
}
};
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return (
<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>
<Select
name='metricValue'
options={issueOptions}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder='All Issues'
/>
</>
)}
{metric.metricType === INSIGHTS && (
<>
<span className='mx-3'>of</span>
<Select
name='metricValue'
options={issueCategories}
value={metric.metricValue}
onChange={writeOption}
isMulti={true}
placeholder='All Categories'
/>
</>
)}
{metric.metricType === 'table' &&
!(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && (
<>
<span className='mx-3'>showing</span>
<Select
name='metricFormat'
options={[{ value: 'sessionCount', label: 'Session Count' }]}
defaultValue={metric.metricFormat}
onChange={writeOption}
/>
</>
)}
</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'>
Filtering and drill-downs will be supported soon for this card type.
</div>
</div>
)}
{testingKey ? <Input
placeholder="AI Query"
value={aiQuery}
onChange={(e: any) => setAiQuery(e.target.value)}
className="w-full mb-2"
onKeyDown={handleKeyDown}
/> : null}
{testingKey ? <Input
placeholder="AI Ask Chart"
value={aiAskChart}
onChange={(e: any) => setAiAskChart(e.target.value)}
className="w-full mb-2"
onKeyDown={handleChartKeyDown}
/> : null}
{aiFiltersStore.isLoading ? (
<div>
<div className='flex items-center font-medium py-2'>
Loading
</div>
</div>
) : null}
{!isPredefined && (
<div>
<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'
onClick={() => metric.addSeries()}
disabled={!canAddSeries}
>
ADD
</Button>
)}
</div>
{metric.series.length > 0 &&
metric.series
.slice(0, isTable || isFunnel || isClickmap || isInsights || isRetention ? 1 : metric.series.length)
.map((series: any, index: number) => (
<div className='mb-2' key={series.name}>
<FilterSeries
canExclude={isPathAnalysis}
supportsEmpty={!isClickmap && !isPathAnalysis}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={isTable || isClickmap || isInsights || isPathAnalysis || isFunnel}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={
isTable
? '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.'
}
/>
{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>
)}
</div>
<div className='form-groups flex items-center justify-between'>
<Tooltip
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}>
{metric.exists()
? 'Update'
: parseInt(dashboardId) > 0
? 'Create & Add to Dashboard'
: 'Create'}
</Button>
{metric.exists() && metric.hasChanged && (
<Button onClick={undoChanges} variant='text' icon='arrow-counterclockwise' className='ml-2'>
Undo
</Button>
*/}
{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>
)}
</div>
</Tooltip>
<div className='flex items-center'>
{metric.exists() && (
<Button variant='text-primary' onClick={onDelete}>
<Icon name='trash' size='14' className='mr-2' color='teal' />
Delete
</Button>
)}
{isPredefined && (
<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>
)}
{testingKey ? <Input
placeholder="AI Query"
value={aiQuery}
onChange={(e: any) => setAiQuery(e.target.value)}
className="w-full mb-2"
onKeyDown={handleKeyDown}
/> : null}
{testingKey ? <Input
placeholder="AI Ask Chart"
value={aiAskChart}
onChange={(e: any) => setAiAskChart(e.target.value)}
className="w-full mb-2"
onKeyDown={handleChartKeyDown}
/> : null}
{aiFiltersStore.isLoading ? (
<div>
<div className='flex items-center font-medium py-2'>
Loading
</div>
</div>
) : null}
{!isPredefined && (
<div>
<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'
onClick={() => metric.addSeries()}
disabled={!canAddSeries}
>
ADD
</Button>
)}
</div>
{metric.series.length > 0 &&
metric.series
.slice(0, isTable || isFunnel || isClickmap || isInsights || isRetention ? 1 : metric.series.length)
.map((series: any, index: number) => (
<div className='mb-2' key={series.name}>
<FilterSeries
canExclude={isPathAnalysis}
supportsEmpty={!isClickmap && !isPathAnalysis}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={isTable || isClickmap || isInsights || isPathAnalysis || isFunnel}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={
isTable
? '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>
))}
</div>
)}
<div className='form-groups flex items-center justify-between'>
<Tooltip
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}>
{metric.exists()
? 'Update'
: parseInt(dashboardId) > 0
? 'Create & Add to Dashboard'
: 'Create'}
</Button>
{metric.exists() && metric.hasChanged && (
<Button onClick={undoChanges} variant='text' icon='arrow-counterclockwise' className='ml-2'>
Undo
</Button>
)}
</div>
</Tooltip>
<div className='flex items-center'>
{metric.exists() && (
<Button variant='text-primary' onClick={onDelete}>
<Icon name='trash' size='14' className='mr-2' color='teal'/>
Delete
</Button>
)}
</div>
</div>
</div>
</div>
</div>
);
);
}
export default observer(WidgetForm);

View file

@ -0,0 +1,179 @@
import React from 'react';
import {Card, Space, Typography, Button} from "antd";
import {useStore} from "App/mstore";
import {eventKeys} from "Types/filter/newFilter";
import {
CLICKMAP,
ERRORS,
FUNNEL,
INSIGHTS,
PERFORMANCE,
RESOURCE_MONITORING,
RETENTION,
TABLE,
USER_PATH, WEB_VITALS
} from "App/constants/card";
import FilterSeries from "Components/Dashboard/components/FilterSeries/FilterSeries";
import {metricOf} from "App/constants/filterOptions";
import {AudioWaveform, ChevronDown, ChevronUp, PlusIcon} from "lucide-react";
import {observer} from "mobx-react-lite";
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
import {Icon} from "UI";
import FilterItem from "Shared/Filters/FilterItem";
import {FilterKey} from "Types/filter/filterType";
function WidgetFormNew() {
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric: any = metricStore.instance;
const eventsLength = metric.series[0].filter.filters.filter((i: any) => i && i.isEvent).length;
const filtersLength = metric.series[0].filter.filters.filter((i: any) => i && !i.isEvent).length;
const isClickMap = metric.metricType === CLICKMAP;
const isPathAnalysis = metric.metricType === USER_PATH;
const excludeFilterKeys = isClickMap || isPathAnalysis ? eventKeys : [];
const hasFilters = filtersLength > 0 || eventsLength > 0;
const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes(metric.metricType);
return isPredefined ? <PredefinedMessage/> : (
<Space direction="vertical" className="w-full">
<AdditionalFilters/>
<Card
styles={{
body: {padding: '0'},
cover: {}
}}
>
{!hasFilters && (
<DefineSteps metric={metric} excludeFilterKeys={excludeFilterKeys}/>
)}
</Card>
{hasFilters && (
<FilterSection metric={metric} excludeFilterKeys={excludeFilterKeys}/>
)}
</Space>
);
}
export default observer(WidgetFormNew);
function DefineSteps({metric, excludeFilterKeys}: any) {
return (
<Space className="px-4 py-2 rounded-lg shadow-sm">
<Typography.Text strong>Define Steps</Typography.Text>
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={metric.series[0]}/>
</Space>
);
}
const FilterSection = observer(({metric, excludeFilterKeys}: any) => {
// const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries');
// const tableOptions = metricOf.filter((i) => i.type === 'table');
const isTable = metric.metricType === TABLE;
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 && i.isEvent).length;
// const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1);
const isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention
// const onAddFilter = (filter: any) => {
// metric.series[0].filter.addFilter(filter);
// metric.updateKey('hasChanged', true)
// }
return (
<>
{
metric.series.length > 0 && metric.series
.slice(0, isSingleSeries ? 1 : metric.series.length)
.map((series: any, index: number) => (
<div className='mb-2' key={series.name}>
<FilterSeries
canExclude={isPathAnalysis}
supportsEmpty={!isClickMap && !isPathAnalysis}
excludeFilterKeys={excludeFilterKeys}
observeChanges={() => metric.updateKey('hasChanged', true)}
hideHeader={isTable || isClickMap || isInsights || isPathAnalysis || isFunnel}
seriesIndex={index}
series={series}
onRemoveSeries={() => metric.removeSeries(index)}
canDelete={metric.series.length > 1}
emptyMessage={
isTable
? '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.'
}
expandable={isSingleSeries}
/>
</div>
))
}
{!isSingleSeries && canAddSeries && (
<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>
)}
</>
);
})
const PathAnalysisFilter = observer(({metric}: any) => (
<Card styles={{
body: {padding: '4px 20px'},
header: {padding: '4px 20px', fontSize: '14px', fontWeight: 'bold', borderBottom: 'none'},
}}
title={metric.startType === 'start' ? 'Start Point' : 'End Point'}
>
<div className='form-group flex flex-col'>
<FilterItem
hideDelete
filter={metric.startPoint}
allowedFilterKeys={[FilterKey.LOCATION, FilterKey.CLICK, FilterKey.INPUT, FilterKey.CUSTOM]}
onUpdate={val => metric.updateStartPoint(val)}
onRemoveFilter={() => {
}}
/>
</div>
</Card>
));
const AdditionalFilters = observer(() => {
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const metric: any = metricStore.instance;
return (
<Space direction="vertical" className="w-full">
{metric.metricType === USER_PATH && <PathAnalysisFilter metric={metric}/>}
</Space>
)
});
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>
);

View file

@ -42,7 +42,7 @@ function WidgetPredefinedChart(props: Props) {
case FilterKey.ERRORS_PER_TYPE:
return <ErrorsByType data={data} metric={metric} />
case FilterKey.ERRORS_PER_DOMAINS:
return <ErrorsPerDomain data={data} metric={metric} />
return <ErrorsPerDomain data={metric.data} />
case FilterKey.RESOURCES_BY_PARTY:
return <ErrorsByOrigin data={data} metric={metric} />
case FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS:
@ -52,7 +52,7 @@ function WidgetPredefinedChart(props: Props) {
case FilterKey.DOMAINS_ERRORS_5XX:
return <CallsErrors5xx data={data} metric={metric} />
case FilterKey.CALLS_ERRORS:
return <CallWithErrors isTemplate={isTemplate} data={data} metric={metric} />
return <CallWithErrors isTemplate={isTemplate} data={data} />
// PERFORMANCE
case FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES:

View file

@ -1,148 +1,135 @@
import React from 'react';
import cn from 'classnames';
import WidgetWrapper from '../WidgetWrapper';
import { useStore } from 'App/mstore';
import { SegmentSelection, Button, Icon } from 'UI';
import { observer } from 'mobx-react-lite';
import { FilterKey } from 'Types/filter/filterType';
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
import {useStore} from 'App/mstore';
// import {SegmentSelection, Button, Icon} from 'UI';
import {observer} from 'mobx-react-lite';
// 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, RETENTION, USER_PATH } from 'App/constants/card';
import { Space, Switch } from 'antd';
// import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
import {CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH} from 'App/constants/card';
import {Space, Switch} from 'antd';
// import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
interface Props {
className?: string;
name: string;
isEditing?: boolean;
}
function WidgetPreview(props: Props) {
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = React.useState(false);
const { className = '' } = props;
const { metricStore, dashboardStore } = useStore();
const dashboards = dashboardStore.dashboards;
const {className = ''} = props;
const {metricStore, dashboardStore} = useStore();
// const dashboards = dashboardStore.dashboards;
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) => {
metric.update({ [ name ]: value });
}
const canAddToDashboard = metric.exists() && dashboards.length > 0;
// 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) => {
// metric.update({[name]: value});
// }
return (
<>
<div className={cn(className, 'bg-white rounded border')}>
<div className="flex items-center justify-between px-4 pt-2">
<h2 className="text-2xl">
{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>
<SegmentSelection
name="viewType"
className="my-3"
primary
size="small"
onSelect={ changeViewType }
value={{ value: metric.viewType }}
list={ [
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
{ value: 'progress', name: 'Progress', icon: 'hash' },
]}
/>
</>
)}
<div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
<div className="flex items-center justify-between px-4 pt-2">
<h2 className="text-xl">
{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>
)}
{!disableVisualization && isTable && (
<>
<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: 'table', name: 'Table', icon: 'table' },
{ value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },
]}
disabledMessage="Chart view is not supported"
/>
</>
)}
{/*{isTimeSeries && (*/}
{/* <>*/}
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
{/* <SegmentSelection*/}
{/* name="viewType"*/}
{/* className="my-3"*/}
{/* primary*/}
{/* size="small"*/}
{/* onSelect={ changeViewType }*/}
{/* value={{ value: metric.viewType }}*/}
{/* list={ [*/}
{/* { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },*/}
{/* { value: 'progress', name: 'Progress', icon: 'hash' },*/}
{/* ]}*/}
{/* />*/}
{/* </>*/}
{/*)}*/}
{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 />
) : null}
<WidgetDateRange />
{/* add to dashboard */}
{metric.exists() && (
<Button
variant="text-primary"
className="ml-2 p-0"
onClick={() => setShowDashboardSelectionModal(true)}
disabled={!canAddToDashboard}
>
<Icon name="columns-gap-filled" size="14" className="mr-2" color="teal"/>
Add to Dashboard
</Button>
)}
{/*{!disableVisualization && isTable && (*/}
{/* <>*/}
{/* <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: 'table', name: 'Table', icon: 'table' },*/}
{/* { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },*/}
{/* ]}*/}
{/* disabledMessage="Chart view is not supported"*/}
{/* />*/}
{/* </>*/}
{/*)}*/}
{/*{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/>
) : null}
{/* add to dashboard */}
{/*{metric.exists() && (*/}
{/* <AddToDashboardButton metricId={metric.metricId}/>*/}
{/*)}*/}
</div>
</div>
<div className="pt-0">
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName/>
</div>
</div>
<div className="p-4 pt-0">
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName />
</div>
</div>
{ canAddToDashboard && (
<DashboardSelectionModal
metricId={metric.metricId}
show={showDashboardSelectionModal}
closeHandler={() => setShowDashboardSelectionModal(false)}
/>
)}
</>
);
}

View file

@ -0,0 +1,99 @@
import {useHistory} from "react-router";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import {Button, Drawer, Dropdown, MenuProps, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, TrashIcon} from "lucide-react";
import {toast} from "react-toastify";
import React from "react";
import {useModal} from "Components/ModalContext";
import AlertFormModal from "Components/Alerts/AlertFormModal/AlertFormModal";
const CardViewMenu = () => {
const history = useHistory();
const {alertsStore, dashboardStore, metricStore} = useStore();
const widget = useObserver(() => metricStore.instance);
const {openModal, closeModal} = useModal();
const showAlertModal = () => {
const seriesId = widget.series[0] && widget.series[0].seriesId || '';
alertsStore.init({query: {left: seriesId}})
openModal(<AlertFormModal
onClose={closeModal}
/>, {
// title: 'Set Alerts',
placement: 'right',
width: 620,
});
}
const items: MenuProps['items'] = [
{
key: 'alert',
label: "Set Alerts",
icon: <BellIcon size={16}/>,
disabled: !widget.exists() || widget.metricType === 'predefined',
onClick: showAlertModal,
},
{
key: 'remove',
danger: true,
label: 'Remove',
icon: <TrashIcon size={16}/>,
onClick: () => {
Modal.confirm({
title: 'Are you sure you want to remove this card?',
icon: null,
// content: 'Bla bla ...',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
onOk: () => {
metricStore.delete(widget).then(r => {
history.goBack();
}).catch(() => {
toast.error('Failed to remove card');
});
},
})
}
},
];
const onClick: MenuProps['onClick'] = ({key}) => {
if (key === 'alert') {
message.info('Set Alerts');
} else if (key === 'remove') {
Modal.confirm({
title: 'Are you sure you want to remove this card?',
icon: null,
// content: 'Bla bla ...',
footer: (_, {OkBtn, CancelBtn}) => (
<>
<CancelBtn/>
<OkBtn/>
</>
),
onOk: () => {
metricStore.delete(widget).then(r => {
history.goBack();
}).catch(() => {
toast.error('Failed to remove card');
});
},
})
}
};
return (
<div className="flex items-center justify-between">
<Dropdown menu={{items}}>
<Button icon={<EllipsisVertical size={16}/>}/>
</Dropdown>
</div>
);
};
export default CardViewMenu;

View file

@ -1,18 +1,15 @@
import React, { useState } from 'react';
import { useStore } from 'App/mstore';
import cn from 'classnames';
import { Icon, Loader, NoContent } from 'UI';
import WidgetForm from '../WidgetForm';
import React, {useState} from 'react';
import {useStore} from 'App/mstore';
import {Icon, Loader, NoContent} from 'UI';
import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions';
import { useObserver } from 'mobx-react-lite';
import WidgetName from '../WidgetName';
import { withSiteId } from 'App/routes';
import {useObserver} from 'mobx-react-lite';
import {dashboardMetricDetails, metricDetails, withSiteId} from 'App/routes';
import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues';
import Breadcrumb from 'Shared/Breadcrumb';
import { FilterKey } from 'Types/filter/filterType';
import { Prompt } from 'react-router';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import {FilterKey} from 'Types/filter/filterType';
import {Prompt, useHistory} from 'react-router';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import {
TIMESERIES,
TABLE,
@ -21,36 +18,46 @@ import {
INSIGHTS,
USER_PATH,
RETENTION,
} from 'App/constants/card';
} from 'App/constants/card';
import CardIssues from '../CardIssues';
import CardUserList from '../CardUserList/CardUserList';
import WidgetViewHeader from "Components/Dashboard/components/WidgetView/WidgetViewHeader";
import WidgetFormNew from "Components/Dashboard/components/WidgetForm/WidgetFormNew";
import {Space} from "antd";
import {renderClickmapThumbnail} from "Components/Dashboard/components/WidgetForm/renderMap";
import Widget from "App/mstore/types/widget";
interface Props {
history: any;
match: any;
siteId: any;
}
function WidgetView(props: Props) {
const {
match: {
params: { siteId, dashboardId, metricId },
params: {siteId, dashboardId, metricId},
},
} = props;
const { metricStore, dashboardStore } = useStore();
// const siteId = location.pathname.split('/')[1];
// const dashboardId = location.pathname.split('/')[3];
const {metricStore, dashboardStore} = useStore();
const widget = useObserver(() => metricStore.instance);
const loading = useObserver(() => metricStore.isLoading);
const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
const hasChanged = useObserver(() => widget.hasChanged);
const dashboards = useObserver(() => dashboardStore.dashboards);
const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId));
const dashboardName = dashboard ? dashboard.name : null;
const [metricNotFound, setMetricNotFound] = useState(false);
const history = useHistory();
const [initialInstance, setInitialInstance] = useState();
const isClickMap = widget.metricType === CLICKMAP;
React.useEffect(() => {
if (metricId && metricId !== 'create') {
metricStore.fetch(metricId, dashboardStore.period).catch((e) => {
if (e.status === 404 || e.status === 422) {
if (e.response.status === 404 || e.response.status === 422) {
setMetricNotFound(true);
}
});
@ -59,13 +66,44 @@ function WidgetView(props: Props) {
}
}, []);
const onBackHandler = () => {
props.history.goBack();
// const onBackHandler = () => {
// props.history.goBack();
// };
//
// const openEdit = () => {
// if (expanded) return;
// setExpanded(true);
// };
const undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
const openEdit = () => {
if (expanded) return;
setExpanded(true);
const onSave = async () => {
const wasCreating = !widget.exists();
if (isClickMap) {
try {
widget.thumbnail = await renderClickmapThumbnail();
} catch (e) {
console.error(e);
}
}
const savedMetric = await metricStore.save(widget);
setInitialInstance(widget.toJson());
if (wasCreating) {
if (parseInt(dashboardId, 10) > 0) {
history.replace(
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
);
void dashboardStore.addWidgetToDashboard(
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
[savedMetric.metricId]
);
} else {
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
}
}
};
return useObserver(() => (
@ -80,57 +118,47 @@ function WidgetView(props: Props) {
}}
/>
<div style={{ maxWidth: '1360px', margin: 'auto'}}>
<div style={{maxWidth: '1360px', margin: 'auto'}}>
<Breadcrumb
items={[
{
label: dashboardName ? dashboardName : 'Cards',
to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId),
},
{ label: widget.name },
{label: widget.name},
]}
/>
<NoContent
show={metricNotFound}
title={
<div className="flex flex-col items-center justify-between">
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100} />
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100}/>
<div className="mt-4">Metric not found!</div>
</div>
}
>
<div className="bg-white rounded border">
<div
className={cn('px-6 py-4 flex justify-between items-center', {
'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded,
})}
onClick={openEdit}
>
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
<WidgetName name={widget.name} onUpdate={(name) => metricStore.merge({ name })} canEdit={expanded} />
</h1>
<div className="text-gray-600 w-full cursor-pointer" onClick={() => setExpanded(!expanded)}>
<div className="flex items-center select-none w-fit ml-auto">
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Edit'}</span>
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
</div>
</div>
</div>
<Space direction="vertical" size={20} className="w-full">
<WidgetViewHeader onSave={onSave} undoChanges={undoChanges}/>
{expanded && <WidgetForm onDelete={onBackHandler} {...props} />}
</div>
<WidgetFormNew/>
<WidgetPreview className="mt-8" name={widget.name} isEditing={expanded} />
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
<>
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) && <WidgetSessions className="mt-8" />}
{widget.metricType === FUNNEL && <FunnelIssues />}
</>
)}
{/*<div className="bg-white rounded border mt-3">*/}
{/* <WidgetForm expanded={expanded} onDelete={onBackHandler} {...props} />*/}
{/*</div>*/}
{widget.metricType === USER_PATH && <CardIssues />}
{widget.metricType === RETENTION && <CardUserList />}
<WidgetPreview name={widget.name} isEditing={expanded}/>
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
<>
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) &&
<WidgetSessions/>}
{widget.metricType === FUNNEL && <FunnelIssues/>}
</>
)}
{widget.metricType === USER_PATH && <CardIssues/>}
{widget.metricType === RETENTION && <CardUserList/>}
</Space>
</NoContent>
</div>
</Loader>

View file

@ -0,0 +1,48 @@
import React from 'react';
import cn from "classnames";
import WidgetName from "Components/Dashboard/components/WidgetName";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
import WidgetDateRange from "Components/Dashboard/components/WidgetDateRange/WidgetDateRange";
import {Button, Space} from "antd";
import CardViewMenu from "Components/Dashboard/components/WidgetView/CardViewMenu";
interface Props {
onClick?: () => void;
onSave: () => void;
undoChanges?: () => void;
}
function WidgetViewHeader({onClick, onSave, undoChanges}: Props) {
const {metricStore, dashboardStore} = useStore();
const widget = useObserver(() => metricStore.instance);
return (
<div
className={cn('flex justify-between items-center')}
onClick={onClick}
>
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
<WidgetName name={widget.name}
onUpdate={(name) => metricStore.merge({name})}
canEdit={true}/>
</h1>
<Space>
<WidgetDateRange label=""/>
<AddToDashboardButton metricId={widget.metricId}/>
<Button
type="primary"
onClick={onSave}
loading={metricStore.isSaving}
disabled={metricStore.isSaving || !widget.hasChanged}
>
Update
</Button>
<CardViewMenu/>
</Space>
</div>
);
}
export default WidgetViewHeader;

View file

@ -1,27 +1,34 @@
import React from 'react';
import WidgetIcon from './WidgetIcon';
import { useStore } from 'App/mstore';
import {useStore} from 'App/mstore';
import {Button} from "antd";
import {BellIcon} from "lucide-react";
import {useModal} from "Components/ModalContext";
import AlertFormModal from "Components/Alerts/AlertFormModal/AlertFormModal";
interface Props {
seriesId: string;
initAlert: Function;
initAlert?: Function;
}
function AlertButton(props: Props) {
const { seriesId } = props;
const { dashboardStore, alertsStore } = useStore();
const {seriesId} = props;
const {dashboardStore, alertsStore} = useStore();
const {openModal, closeModal} = useModal();
const onClick = () => {
dashboardStore.toggleAlertModal(true);
alertsStore.init({ query: { left: seriesId }})
// dashboardStore.toggleAlertModal(true);
alertsStore.init({query: {left: seriesId}})
openModal(<AlertFormModal
onClose={closeModal}
/>, {
// title: 'Set Alerts',
placement: 'right',
width: 620,
});
}
return (
<div onClick={onClick}>
<WidgetIcon
className="cursor-pointer"
icon="bell-plus"
tooltip="Set Alert"
/>
</div>
<Button onClick={onClick} type="text" icon={<BellIcon size={16}/>}/>
);
}
export default AlertButton;
export default AlertButton;

View file

@ -0,0 +1,48 @@
import React from 'react';
import {useHistory} from "react-router";
import {useStore} from "App/mstore";
import {useObserver} from "mobx-react-lite";
import {Button, Dropdown, MenuProps, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, EyeOffIcon, PencilIcon, TrashIcon} from "lucide-react";
import {toast} from "react-toastify";
import {dashboardMetricDetails, withSiteId} from "App/routes";
function CardMenu({card}: any) {
const siteId = location.pathname.split('/')[1];
const history = useHistory();
const {dashboardStore, metricStore} = useStore();
const dashboardId = dashboardStore.selectedDashboard?.dashboardId;
const items: MenuProps['items'] = [
{
key: 'edit',
label: "Edit",
icon: <PencilIcon size={16}/>,
},
{
key: 'hide',
label: 'Hide',
icon: <EyeOffIcon size={16}/>,
},
];
const onClick: MenuProps['onClick'] = ({key}) => {
if (key === 'edit') {
history.push(
withSiteId(dashboardMetricDetails(dashboardId, card.metricId), siteId)
)
} else if (key === 'hide') {
dashboardStore.deleteDashboardWidget(dashboardId!, card.widgetId).then(r => null);
}
};
return (
<div className="flex items-center justify-between">
<Dropdown menu={{items, onClick}} overlayStyle={{minWidth: '120px'}}>
<Button type="text" icon={<EllipsisVertical size={16}/>}/>
</Dropdown>
</div>
);
}
export default CardMenu;

View file

@ -97,7 +97,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
return (
<div
className={cn(
'relative rounded bg-white border group',
'relative rounded bg-white border group rounded-lg',
'col-span-' + widget.config.col,
{ 'hover:shadow-border-gray': !isTemplate && isWidget },
{ 'hover:shadow-border-main': isTemplate }

View file

@ -0,0 +1,159 @@
import React, {useRef} from 'react';
import cn from 'classnames';
import {Card, Tooltip, Button} from 'antd';
import {useDrag, useDrop} from 'react-dnd';
import WidgetChart from '../WidgetChart';
import {observer} from 'mobx-react-lite';
import {useStore} from 'App/mstore';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import {withSiteId, dashboardMetricDetails} from 'App/routes';
import TemplateOverlay from './TemplateOverlay';
import stl from './widgetWrapper.module.css';
import {FilterKey} from 'App/types/filter/filterType';
import LazyLoad from 'react-lazyload';
import {TIMESERIES} from "App/constants/card";
import CardMenu from "Components/Dashboard/components/WidgetWrapper/CardMenu";
import AlertButton from "Components/Dashboard/components/WidgetWrapper/AlertButton";
interface Props {
className?: string;
widget?: any;
index?: number;
moveListItem?: any;
isPreview?: boolean;
isTemplate?: boolean;
dashboardId?: string;
siteId?: string;
active?: boolean;
history?: any;
onClick?: () => void;
isWidget?: boolean;
hideName?: boolean;
grid?: string;
isGridView?: boolean;
}
function WidgetWrapperNew(props: Props & RouteComponentProps) {
const {dashboardStore} = useStore();
const {
isWidget = false,
active = false,
index = 0,
moveListItem = null,
isPreview = false,
isTemplate = false,
siteId,
grid = '',
isGridView = false,
} = props;
const widget: any = props.widget;
const isTimeSeries = widget.metricType === TIMESERIES;
const isPredefined = widget.metricType === 'predefined';
const dashboard = dashboardStore.selectedDashboard;
const [{isDragging}, dragRef] = useDrag({
type: 'item',
item: {index, grid},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [{isOver, canDrop}, dropRef] = useDrop({
accept: 'item',
drop: (item: any) => {
if (item.index === index || item.grid !== grid) return;
moveListItem(item.index, index);
},
canDrop(item) {
return item.grid === grid;
},
collect: (monitor: any) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
});
const onChartClick = () => {
if (!isWidget || isPredefined) return;
props.history.push(
withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId)
);
};
const ref: any = useRef(null);
const dragDropRef: any = dragRef(dropRef(ref));
const addOverlay =
isTemplate ||
(!isPredefined &&
isWidget &&
widget.metricOf !== FilterKey.ERRORS &&
widget.metricOf !== FilterKey.SESSIONS);
return (
<Card
className={cn(
'relative group',
'col-span-' + widget.config.col,
{'hover:shadow': !isTemplate && isWidget},
)}
style={{
userSelect: 'none',
opacity: isDragging ? 0.5 : 1,
borderColor:
(canDrop && isOver) || active ? '#394EFF' : isPreview ? 'transparent' : '#EEEEEE',
}}
ref={dragDropRef}
onClick={props.onClick ? props.onClick : () => null}
id={`widget-${widget.widgetId}`}
title={!props.hideName ? widget.name : null}
extra={isWidget ? [
<div className="flex items-center" id="no-print">
{!isPredefined && isTimeSeries && !isGridView && (
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId}/>
)}
{!isTemplate && !isGridView && (
<CardMenu card={widget} key="card-menu"/>
)}
</div>
] : []}
styles={{
header: {
padding: '0 14px',
borderBottom: 'none',
minHeight: 44,
fontWeight: 500,
fontSize: 14,
},
body: {
padding: 0,
},
}}
>
{!isTemplate && isWidget && isPredefined && (
<Tooltip title="Cannot drill down system provided metrics">
<div
className={cn(stl.drillDownMessage, 'disabled text-gray text-sm invisible group-hover:visible')}>
{'Cannot drill down system provided metrics'}
</div>
</Tooltip>
)}
{addOverlay && <TemplateOverlay onClick={onChartClick} isTemplate={isTemplate}/>}
<LazyLoad offset={!isTemplate ? 100 : 600}>
<div className="px-4" onClick={onChartClick}>
<WidgetChart
isPreview={isPreview}
metric={widget}
isTemplate={isTemplate}
isWidget={isWidget}
/>
</div>
</LazyLoad>
</Card>
);
}
export default withRouter(observer(WidgetWrapperNew));

View file

@ -13,6 +13,7 @@
left: 0;
right: 0;
}
.overlayDashboard {
top: 20%!important;
top: 40px !important;
}

Some files were not shown because too many files have changed in this diff Show more