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,24 +1,25 @@
import React, { useEffect, useRef } from 'react'; import React, {useEffect, useRef} from 'react';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import {withRouter, RouteComponentProps} from 'react-router-dom';
import { connect, ConnectedProps } from 'react-redux'; import {connect, ConnectedProps} from 'react-redux';
import { Loader } from 'UI'; import {Loader} from 'UI';
import { fetchUserInfo, setJwt } from 'Duck/user'; import {fetchUserInfo, setJwt} from 'Duck/user';
import { fetchList as fetchSiteList } from 'Duck/site'; import {fetchList as fetchSiteList} from 'Duck/site';
import { withStore } from 'App/mstore'; import {withStore} from 'App/mstore';
import { Map } from 'immutable'; import {Map} from 'immutable';
import * as routes from './routes'; import * as routes from './routes';
import { fetchTenants } from 'Duck/user'; import {fetchTenants} from 'Duck/user';
import { setSessionPath } from 'Duck/sessions'; import {setSessionPath} from 'Duck/sessions';
import { ModalProvider } from 'Components/Modal'; import {ModalProvider} from 'Components/Modal';
import { GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM } from 'App/constants/storageKeys'; import {GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM} from 'App/constants/storageKeys';
import PublicRoutes from 'App/PublicRoutes'; import PublicRoutes from 'App/PublicRoutes';
import Layout from 'App/layout/Layout'; import Layout from 'App/layout/Layout';
import { fetchListActive as fetchMetadata } from 'Duck/customField'; import {fetchListActive as fetchMetadata} from 'Duck/customField';
import { init as initSite } from 'Duck/site'; import {init as initSite} from 'Duck/site';
import PrivateRoutes from 'App/PrivateRoutes'; import PrivateRoutes from 'App/PrivateRoutes';
import { checkParam } from 'App/utils'; import {checkParam} from 'App/utils';
import IFrameRoutes from 'App/IFrameRoutes'; import IFrameRoutes from 'App/IFrameRoutes';
import {ModalProvider as NewModalProvider} from 'Components/ModalContext';
interface RouterProps extends RouteComponentProps, ConnectedProps<typeof connector> { interface RouterProps extends RouteComponentProps, ConnectedProps<typeof connector> {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -51,7 +52,7 @@ const Router: React.FC<RouterProps> = (props) => {
fetchUserInfo, fetchUserInfo,
fetchSiteList, fetchSiteList,
history, history,
match: { params: { siteId: siteIdFromPath } }, match: {params: {siteId: siteIdFromPath}},
setSessionPath, setSessionPath,
} = props; } = props;
const [isIframe, setIsIframe] = React.useState(false); const [isIframe, setIsIframe] = React.useState(false);
@ -142,18 +143,20 @@ const Router: React.FC<RouterProps> = (props) => {
location.pathname.includes('/assist/') || location.pathname.includes('multiview'); location.pathname.includes('/assist/') || location.pathname.includes('multiview');
if (isIframe) { if (isIframe) {
return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading} />; return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading}/>;
} }
return isLoggedIn ? ( return isLoggedIn ? (
<NewModalProvider>
<ModalProvider> <ModalProvider>
<Loader loading={loading || !siteId} className='flex-1'> <Loader loading={loading || !siteId} className='flex-1'>
<Layout hideHeader={hideHeader} siteId={siteId}> <Layout hideHeader={hideHeader} siteId={siteId}>
<PrivateRoutes /> <PrivateRoutes/>
</Layout> </Layout>
</Loader> </Loader>
</ModalProvider> </ModalProvider>
) : <PublicRoutes />; </NewModalProvider>
) : <PublicRoutes/>;
}; };
const mapStateToProps = (state: Map<string, any>) => { const mapStateToProps = (state: Map<string, any>) => {

View file

@ -1,38 +1,39 @@
import React, { useEffect } from 'react'; import React, {useEffect} from 'react';
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI'; import {Form, Input, SegmentSelection, Checkbox, Icon} from 'UI';
import { alertConditions as conditions } from 'App/constants'; import {alertConditions as conditions} from 'App/constants';
import stl from './alertForm.module.css'; import stl from './alertForm.module.css';
import DropdownChips from './DropdownChips'; import DropdownChips from './DropdownChips';
import { validateEmail } from 'App/validate'; import {validateEmail} from 'App/validate';
import cn from 'classnames'; import cn from 'classnames';
import { useStore } from 'App/mstore' import {useStore} from 'App/mstore'
import { observer } from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import Select from 'Shared/Select'; import Select from 'Shared/Select';
import {Button} from "antd";
const thresholdOptions = [ const thresholdOptions = [
{ label: '15 minutes', value: 15 }, {label: '15 minutes', value: 15},
{ label: '30 minutes', value: 30 }, {label: '30 minutes', value: 30},
{ label: '1 hour', value: 60 }, {label: '1 hour', value: 60},
{ label: '2 hours', value: 120 }, {label: '2 hours', value: 120},
{ label: '4 hours', value: 240 }, {label: '4 hours', value: 240},
{ label: '1 day', value: 1440 }, {label: '1 day', value: 1440},
]; ];
const changeOptions = [ const changeOptions = [
{ label: 'change', value: 'change' }, {label: 'change', value: 'change'},
{ label: '% change', value: 'percent' }, {label: '% change', value: 'percent'},
]; ];
const Circle = ({ text }) => ( const Circle = ({text}) => (
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center"> <div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
{text} {text}
</div> </div>
); );
const Section = ({ index, title, description, content }) => ( const Section = ({index, title, description, content}) => (
<div className="w-full"> <div className="w-full">
<div className="flex items-start"> <div className="flex items-start">
<Circle text={index} /> <Circle text={index}/>
<div> <div>
<span className="font-medium">{title}</span> <span className="font-medium">{title}</span>
{description && <div className="text-sm color-gray-medium">{description}</div>} {description && <div className="text-sm color-gray-medium">{description}</div>}
@ -49,9 +50,9 @@ function AlertForm(props) {
msTeamsChannels, msTeamsChannels,
webhooks, webhooks,
onDelete, onDelete,
style = { width: '580px', height: '100vh' }, style = {height: "calc('100vh - 40px')"},
} = props; } = props;
const { alertsStore } = useStore() const {alertsStore} = useStore()
const { const {
triggerOptions, triggerOptions,
loading, loading,
@ -59,22 +60,22 @@ function AlertForm(props) {
const instance = alertsStore.instance const instance = alertsStore.instance
const deleting = loading const deleting = loading
const write = ({ target: { value, name } }) => alertsStore.edit({ [name]: value }); const write = ({target: {value, name}}) => alertsStore.edit({[name]: value});
const writeOption = (e, { name, value }) => alertsStore.edit({ [name]: value.value }); const writeOption = (e, {name, value}) => alertsStore.edit({[name]: value.value});
const onChangeCheck = ({ target: { checked, name } }) => alertsStore.edit({ [name]: checked }); const onChangeCheck = ({target: {checked, name}}) => alertsStore.edit({[name]: checked});
useEffect(() => { useEffect(() => {
void alertsStore.fetchTriggerOptions(); void alertsStore.fetchTriggerOptions();
}, []); }, []);
const writeQueryOption = (e, { name, value }) => { const writeQueryOption = (e, {name, value}) => {
const { query } = instance; const {query} = instance;
alertsStore.edit({ query: { ...query, [name]: value } }); alertsStore.edit({query: {...query, [name]: value}});
}; };
const writeQuery = ({ target: { value, name } }) => { const writeQuery = ({target: {value, name}}) => {
const { query } = instance; const {query} = instance;
alertsStore.edit({ query: { ...query, [name]: value } }); alertsStore.edit({query: {...query, [name]: value}});
}; };
const metric = const metric =
@ -86,23 +87,23 @@ function AlertForm(props) {
return ( return (
<Form <Form
className={cn('p-6 pb-10', stl.wrapper)} className={cn('pb-10', stl.wrapper)}
style={style} style={style}
onSubmit={() => props.onSubmit(instance)} onSubmit={() => props.onSubmit(instance)}
id="alert-form" id="alert-form"
> >
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}> <div className={cn('-mx-6 px-6 pb-12')}>
<input <input
autoFocus={true} autoFocus={true}
className="text-lg border border-gray-light rounded w-full" className="text-lg border border-gray-light rounded w-full"
name="name" name="name"
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }} style={{fontSize: '18px', padding: '10px', fontWeight: '600'}}
value={instance && instance.name} value={instance && instance.name}
onChange={write} onChange={write}
placeholder="Untiltled Alert" placeholder="Untiltled Alert"
id="name-field" id="name-field"
/> />
<div className="mb-8" /> <div className="mb-8"/>
<Section <Section
index="1" index="1"
title={'What kind of alert do you want to set?'} title={'What kind of alert do you want to set?'}
@ -112,11 +113,11 @@ function AlertForm(props) {
primary primary
name="detectionMethod" name="detectionMethod"
className="my-3" className="my-3"
onSelect={(e, { name, value }) => alertsStore.edit({ [name]: value })} onSelect={(e, {name, value}) => alertsStore.edit({[name]: value})}
value={{ value: instance.detectionMethod }} value={{value: instance.detectionMethod}}
list={[ list={[
{ name: 'Threshold', value: 'threshold' }, {name: 'Threshold', value: 'threshold'},
{ name: 'Change', value: 'change' }, {name: 'Change', value: 'change'},
]} ]}
/> />
<div className="text-sm color-gray-medium"> <div className="text-sm color-gray-medium">
@ -125,12 +126,12 @@ function AlertForm(props) {
{!isThreshold && {!isThreshold &&
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'} '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>
<div className="my-4" /> <div className="my-4"/>
</div> </div>
} }
/> />
<hr className="my-8" /> <hr className="my-8"/>
<Section <Section
index="2" index="2"
@ -146,7 +147,7 @@ function AlertForm(props) {
options={changeOptions} options={changeOptions}
name="change" name="change"
defaultValue={instance.change} defaultValue={instance.change}
onChange={({ value }) => writeOption(null, { name: 'change', value })} onChange={({value}) => writeOption(null, {name: 'change', value})}
id="change-dropdown" id="change-dropdown"
/> />
</div> </div>
@ -164,8 +165,8 @@ function AlertForm(props) {
name="left" name="left"
value={triggerOptions.find((i) => i.value === instance.query.left)} value={triggerOptions.find((i) => i.value === instance.query.left)}
// onChange={ writeQueryOption } // onChange={ writeQueryOption }
onChange={({ value }) => onChange={({value}) =>
writeQueryOption(null, { name: 'left', value: value.value }) writeQueryOption(null, {name: 'left', value: value.value})
} }
/> />
</div> </div>
@ -179,15 +180,15 @@ function AlertForm(props) {
name="operator" name="operator"
defaultValue={instance.query.operator} defaultValue={instance.query.operator}
// onChange={ writeQueryOption } // onChange={ writeQueryOption }
onChange={({ value }) => onChange={({value}) =>
writeQueryOption(null, { name: 'operator', value: value.value }) writeQueryOption(null, {name: 'operator', value: value.value})
} }
/> />
{unit && ( {unit && (
<> <>
<Input <Input
className="px-4" className="px-4"
style={{ marginRight: '31px' }} style={{marginRight: '31px'}}
// label={{ basic: true, content: unit }} // label={{ basic: true, content: unit }}
// labelPosition='right' // labelPosition='right'
name="right" name="right"
@ -220,7 +221,7 @@ function AlertForm(props) {
name="currentPeriod" name="currentPeriod"
defaultValue={instance.currentPeriod} defaultValue={instance.currentPeriod}
// onChange={ writeOption } // onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })} onChange={({value}) => writeOption(null, {name: 'currentPeriod', value})}
/> />
</div> </div>
{!isThreshold && ( {!isThreshold && (
@ -235,7 +236,7 @@ function AlertForm(props) {
name="previousPeriod" name="previousPeriod"
defaultValue={instance.previousPeriod} defaultValue={instance.previousPeriod}
// onChange={ writeOption } // onChange={ writeOption }
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })} onChange={({value}) => writeOption(null, {name: 'previousPeriod', value})}
/> />
</div> </div>
)} )}
@ -243,7 +244,7 @@ function AlertForm(props) {
} }
/> />
<hr className="my-8" /> <hr className="my-8"/>
<Section <Section
index="3" index="3"
@ -294,7 +295,7 @@ function AlertForm(props) {
selected={instance.slackInput} selected={instance.slackInput}
options={slackChannels} options={slackChannels}
placeholder="Select Channel" placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({ slackInput: selected })} onChange={(selected) => alertsStore.edit({slackInput: selected})}
/> />
</div> </div>
</div> </div>
@ -308,7 +309,7 @@ function AlertForm(props) {
selected={instance.msteamsInput} selected={instance.msteamsInput}
options={msTeamsChannels} options={msTeamsChannels}
placeholder="Select Channel" placeholder="Select Channel"
onChange={(selected) => alertsStore.edit({ msteamsInput: selected })} onChange={(selected) => alertsStore.edit({msteamsInput: selected})}
/> />
</div> </div>
</div> </div>
@ -323,7 +324,7 @@ function AlertForm(props) {
validate={validateEmail} validate={validateEmail}
selected={instance.emailInput} selected={instance.emailInput}
placeholder="Type and press Enter key" placeholder="Type and press Enter key"
onChange={(selected) => alertsStore.edit({ emailInput: selected })} onChange={(selected) => alertsStore.edit({emailInput: selected})}
/> />
</div> </div>
</div> </div>
@ -337,7 +338,7 @@ function AlertForm(props) {
selected={instance.webhookInput} selected={instance.webhookInput}
options={webhooks} options={webhooks}
placeholder="Select Webhook" placeholder="Select Webhook"
onChange={(selected) => alertsStore.edit({ webhookInput: selected })} onChange={(selected) => alertsStore.edit({webhookInput: selected})}
/> />
</div> </div>
)} )}
@ -346,31 +347,32 @@ function AlertForm(props) {
/> />
</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 justify-between absolute bottom-0 left-0 right-0 p-6 border-t z-10 bg-white">
<div className="flex items-center"> <div className="flex items-center">
<Button <Button
loading={loading} loading={loading}
variant="primary" type="primary"
type="submit" htmlType="submit"
disabled={loading || !instance.validate()} disabled={loading || !instance.validate()}
id="submit-button" id="submit-button"
> >
{instance.exists() ? 'Update' : 'Create'} {instance.exists() ? 'Update' : 'Create'}
</Button> </Button>
<div className="mx-1" /> <div className="mx-1"/>
<Button onClick={props.onClose}>Cancel</Button> <Button onClick={props.onClose}>Cancel</Button>
</div> </div>
<div> <div>
{instance.exists() && ( {instance.exists() && (
<Button <Button
hover hover
variant="text" primary="text"
loading={deleting} loading={deleting}
type="button" type="button"
onClick={() => onDelete(instance)} onClick={() => onDelete(instance)}
id="trash-button" id="trash-button"
> >
<Icon name="trash" color="gray-medium" size="18" /> <Icon name="trash" color="gray-medium" size="18"/>
</Button> </Button>
)} )}
</div> </div>

View file

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

View file

@ -26,7 +26,7 @@ function Recordings(props: Props) {
}; };
return ( 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-center mb-4 justify-between px-6'>
<div className='flex items-baseline mr-3'> <div className='flex items-baseline mr-3'>
<PageTitle title='Training Videos' /> <PageTitle title='Training Videos' />

View file

@ -29,7 +29,7 @@ function AuditView() {
} }
return useObserver(() => ( 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"> <div className="flex items-center mb-4 px-5 pt-5">
<PageTitle title={ <PageTitle title={
<div className="flex items-center"> <div className="flex items-center">

View file

@ -68,7 +68,7 @@ function CustomFields(props) {
const { fields, loading } = props; const { fields, loading } = props;
return ( 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)}> <div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3> <h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
<div style={{ marginRight: '15px' }}> <div style={{ marginRight: '15px' }}>

View file

@ -108,7 +108,7 @@ function Integrations(props: Props) {
return ( 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>} />} {!hideHeader && <PageTitle title={<div>Integrations</div>} />}
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} /> <IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} />
@ -117,15 +117,7 @@ function Integrations(props: Props) {
<div className='mb-4' /> <div className='mb-4' />
<div className={cn(` <div className={cn(`
grid mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3
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
`)}> `)}>
{allIntegrations.map((integration: any) => ( {allIntegrations.map((integration: any) => (
<IntegrationItem <IntegrationItem

View file

@ -46,7 +46,7 @@ function Modules(props: Props) {
return ( return (
<div> <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> <h3 className='text-2xl'>Modules</h3>
<ul className='mt-3 ml-4 list-disc'> <ul className='mt-3 ml-4 list-disc'>
<li>OpenReplay's modules are a collection of advanced features that provide enhanced functionality.</li> <li>OpenReplay's modules are a collection of advanced features that provide enhanced functionality.</li>
@ -54,7 +54,7 @@ function Modules(props: Props) {
</ul> </ul>
</div> </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) => ( {modulesState.map((module) => (
<div key={module.key} className='flex flex-col h-full'> <div key={module.key} className='flex flex-col h-full'>
<ModuleCard module={module} onToggle={onToggle} /> <ModuleCard module={module} onToggle={onToggle} />

View file

@ -19,7 +19,7 @@ export default class ProfileSettings extends React.PureComponent {
render() { render() {
const { account, isEnterprise } = this.props; const { account, isEnterprise } = this.props;
return ( 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>} /> <PageTitle title={<div>Account</div>} />
<div className="flex items-center"> <div className="flex items-center">
<div className={styles.left}> <div className={styles.left}>

View file

@ -65,7 +65,7 @@ function Roles(props: Props) {
return ( return (
<React.Fragment> <React.Fragment>
<Loader loading={loading}> <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={cn(stl.tabHeader, 'flex items-center')}>
<div className="flex items-center mr-auto px-5 pt-5"> <div className="flex items-center mr-auto px-5 pt-5">
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3> <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) { function SessionsListingSettings(props: Props) {
return ( 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>} /> <PageTitle title={<div>Sessions Listing</div>} />
<div className='flex flex-col mt-4'> <div className='flex flex-col mt-4'>

View file

@ -111,7 +111,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
return ( return (
<Loader loading={loading}> <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')}> <div className={cn(stl.tabHeader, 'px-5 pt-5')}>
<PageTitle <PageTitle
title={<div className="mr-4">Projects</div>} title={<div className="mr-4">Projects</div>}

View file

@ -36,7 +36,7 @@ function UsersView(props: Props) {
}, []); }, []);
return ( 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"> <div className="flex items-center justify-between px-5 pt-5">
<PageTitle <PageTitle
title={ title={

View file

@ -44,7 +44,7 @@ function Webhooks() {
}; };
return ( 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)}> <div className={cn(styles.tabHeader)}>
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3> <h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button> <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 React from 'react'
import { Styles } from '../../common'; import {Styles} from '../../common';
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'; import {ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts';
import { LineChart, Line, Legend } from 'recharts'; import {LineChart, Line, Legend} from 'recharts';
interface Props { interface Props {
data: any; data: any;
@ -9,46 +9,56 @@ interface Props {
// seriesMap: any; // seriesMap: any;
colors: any; colors: any;
onClick?: (event, index) => void; 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 ( return (
<ResponsiveContainer height={ 240 } width="100%"> <ResponsiveContainer height={240} width="100%">
<LineChart <LineChart
data={ data.chart } data={data.chart}
margin={Styles.chartMargins} margin={Styles.chartMargins}
// syncId={ showSync ? "domainsErrors_4xx" : undefined } // syncId={ showSync ? "domainsErrors_4xx" : undefined }
onClick={onClick} onClick={onClick}
// isAnimationActive={ false } // isAnimationActive={ false }
> >
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis <XAxis
{...Styles.xaxis} {...Styles.xaxis}
dataKey="time" dataKey="time"
interval={params.density/7} interval={params.density / 7}
/> />
<YAxis <YAxis
{...Styles.yaxis} {...yaxis}
allowDecimals={false} allowDecimals={false}
tickFormatter={val => Styles.tickFormatter(val)} tickFormatter={val => Styles.tickFormatter(val)}
label={{ label={{
...Styles.axisLabelLeft, ...Styles.axisLabelLeft,
value: "Number of Sessions" value: label || "Number of Sessions"
}} }}
/> />
<Legend /> <Legend/>
<Tooltip {...Styles.tooltip} /> <Tooltip {...Styles.tooltip} />
{ Array.isArray(data.namesMap) && data.namesMap.map((key, index) => ( {Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
<Line <Line
key={key} key={key}
name={key} name={key}
type="monotone" type="monotone"
dataKey={key} dataKey={key}
stroke={colors[index]} stroke={colors[index]}
fillOpacity={ 1 } fillOpacity={1}
strokeWidth={ 2 } strokeWidth={2}
strokeOpacity={ 0.6 } strokeOpacity={0.6}
// fill="url(#colorCount)" // fill="url(#colorCount)"
dot={false} 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 React from 'react'
import { Styles } from '../../common'; import {Styles} from '../../common';
import { AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip } from 'recharts'; import {AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip} from 'recharts';
import CountBadge from '../../common/CountBadge'; import CountBadge from '../../common/CountBadge';
import { numberWithCommas } from 'App/utils'; import {numberWithCommas} from 'App/utils';
interface Props { interface Props {
data: any; data: any;
} }
function CustomMetricOverviewChart(props: Props) { function CustomMetricOverviewChart(props: Props) {
const { data } = props; const {data} = props;
const gradientDef = Styles.gradientDef(); const gradientDef = Styles.gradientDef();
return ( return (
<div className="relative -mx-4"> <div className="relative -mx-4">
<div className="absolute flex items-start flex-col justify-start inset-0 p-3"> <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>
<div className="flex items-center"> <div className="flex items-center">
<CountBadge <CountBadge
// title={subtext} // title={subtext}
count={ countView(Math.round(data.value), data.unit) } count={countView(Math.round(data.value), data.unit)}
change={ data.progress || 0 } change={data.progress || 0}
unit={ data.unit } unit={data.unit}
// className={textClass} // className={textClass}
/> />
</div> </div>
</div> </div>
<ResponsiveContainer height={ 100 } width="100%"> <ResponsiveContainer height={100} width="100%">
<AreaChart <AreaChart
data={ data.chart } data={data.chart}
margin={ { margin={{
top: 50, right: 0, left: 0, bottom: 0, top: 50, right: 0, left: 0, bottom: 0,
} } }}
> >
{gradientDef} {gradientDef}
<Tooltip {...Styles.tooltip} /> <Tooltip {...Styles.tooltip} />
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" /> <XAxis hide {...Styles.xaxis} interval={4} dataKey="time"/>
<YAxis hide interval={ 0 } /> <YAxis hide interval={0}/>
<Area <Area
name={''} name={''}
// unit={unit && ' ' + unit} // unit={unit && ' ' + unit}
type="monotone" type="monotone"
dataKey="value" dataKey="value"
stroke={Styles.strokeColor} stroke={Styles.strokeColor}
fillOpacity={ 1 } fillOpacity={1}
strokeWidth={ 2 } strokeWidth={2}
strokeOpacity={ 0.8 } strokeOpacity={0.8}
fill={'url(#colorCount)'} fill={'url(#colorCount)'}
/> />
</AreaChart> </AreaChart>
@ -66,7 +67,7 @@ const countView = (avg: any, unit: any) => {
if (unit === 'min') { if (unit === 'min') {
if (!avg) return 0; if (!avg) return 0;
const count = Math.trunc(avg); const count = Math.trunc(avg);
return numberWithCommas(count > 1000 ? count +'k' : count); return numberWithCommas(count > 1000 ? count + 'k' : count);
} }
return avg ? numberWithCommas(avg): 0; return avg ? numberWithCommas(avg) : 0;
} }

View file

@ -1,10 +1,10 @@
import React from 'react' import React from 'react'
import { Table } from '../../common'; import {Table} from '../../common';
import { List } from 'immutable'; import {List} from 'immutable';
import { filtersMap } from 'Types/filter/newFilter'; import {filtersMap} from 'Types/filter/newFilter';
import { NoContent, Icon } from 'UI'; import {NoContent, Icon} from 'UI';
import { tableColumnName } from 'App/constants/filterOptions'; import {tableColumnName} from 'App/constants/filterOptions';
import { numberWithCommas } from 'App/utils'; import {numberWithCommas} from 'App/utils';
const getColumns = (metric) => { const getColumns = (metric) => {
return [ return [
@ -13,6 +13,7 @@ const getColumns = (metric) => {
title: tableColumnName[metric.metricOf], title: tableColumnName[metric.metricOf],
toText: name => name || 'Unidentified', toText: name => name || 'Unidentified',
width: '70%', width: '70%',
icon: true,
}, },
{ {
key: 'sessionCount', key: 'sessionCount',
@ -29,13 +30,14 @@ interface Props {
onClick?: (filters: any) => void; onClick?: (filters: any) => void;
isTemplate?: boolean; isTemplate?: boolean;
} }
function CustomMetricTable(props: Props) { 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 rows = List(data.values);
const onClickHandler = (event: any, data: any) => { const onClickHandler = (event: any, data: any) => {
const filters = Array<any>(); const filters = Array<any>();
let filter = { ...filtersMap[metric.metricOf] } let filter = {...filtersMap[metric.metricOf]}
filter.value = [data.name] filter.value = [data.name]
filter.type = filter.key filter.type = filter.key
delete filter.key delete filter.key
@ -49,14 +51,14 @@ function CustomMetricTable(props: Props) {
onClick(filters); onClick(filters);
} }
return ( return (
<div className="" style={{ height: 240 }}> <div className="" style={{height: 240}}>
<NoContent <NoContent
style={{ minHeight: 220 }} style={{minHeight: 220}}
show={data.values && data.values.length === 0} show={data.values && data.values.length === 0}
size="small" size="small"
title={ title={
<div className="flex items-center"> <div className="flex items-center">
<Icon name="info-circle" className="mr-2" size="18" /> <Icon name="info-circle" className="mr-2" size="18"/>
No data for the selected time period No data for the selected time period
</div> </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 { interface Props {
data: any data: any
metric?: any
isTemplate?: boolean isTemplate?: boolean
} }
function CallWithErrors(props: Props) { function CallWithErrors(props: Props) {
const { data, metric } = props; const { data } = props;
const [search, setSearch] = React.useState('') const [search, setSearch] = React.useState('')
const test = (value = '', serach: any) => getRE(serach, 'i').test(value); 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) => { const write = ({ target: { name, value } }: any) => {
setSearch(value) setSearch(value)
}; };
return ( return (
<NoContent <NoContent
size="small" size="small"
title={NO_METRIC_DATA} title={NO_METRIC_DATA}
show={ metric.data.chart.length === 0 } show={ data.chart.length === 0 }
style={{ height: '240px'}} style={{ height: '240px'}}
> >
<div style={{ height: '240px'}}> <div style={{ height: '240px'}}>
<div className={ cn(stl.topActions, 'py-3 flex text-right')}> <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> </div>
<Table <Table
small small

View file

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

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { numberWithCommas } from 'App/utils'; import {numberWithCommas} from 'App/utils';
const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1']; const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1'];
const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72']; const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72'];
const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse(); const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
@ -23,31 +24,31 @@ export default {
compareColorsx, compareColorsx,
lineColor: '#2A7B7F', lineColor: '#2A7B7F',
lineColorCompare: '#394EFF', lineColorCompare: '#394EFF',
strokeColor: colors[2], strokeColor: compareColors[2],
xaxis: { xaxis: {
axisLine: { stroke: '#CCCCCC' }, axisLine: {stroke: '#CCCCCC'},
interval: 0, interval: 0,
dataKey: "time", dataKey: "time",
tick: { fill: '#999999', fontSize: 9 }, tick: {fill: '#999999', fontSize: 9},
tickLine: { stroke: '#CCCCCC' }, tickLine: {stroke: '#CCCCCC'},
strokeWidth: 0.5 strokeWidth: 0.5
}, },
yaxis: { yaxis: {
axisLine: { stroke: '#CCCCCC' }, axisLine: {stroke: '#CCCCCC'},
tick: { fill: '#999999', fontSize: 9 }, tick: {fill: '#999999', fontSize: 9},
tickLine: { stroke: '#CCCCCC' }, tickLine: {stroke: '#CCCCCC'},
}, },
axisLabelLeft: { axisLabelLeft: {
angle: -90, angle: -90,
fill: '#999999', fill: '#999999',
offset: 10, offset: 10,
style: { textAnchor: 'middle' }, style: {textAnchor: 'middle'},
position: 'insideLeft', position: 'insideLeft',
fontSize: 11 fontSize: 11
}, },
tickFormatter: val => `${countView(val)}`, tickFormatter: val => `${countView(val)}`,
tickFormatterBytes: val => Math.round(val / 1024 / 1024), tickFormatterBytes: val => Math.round(val / 1024 / 1024),
chartMargins: { left: 0, right: 20, top: 10, bottom: 5 }, chartMargins: {left: 0, right: 20, top: 10, bottom: 5},
tooltip: { tooltip: {
cursor: { cursor: {
fill: '#f6f6f6' fill: '#f6f6f6'
@ -62,7 +63,7 @@ export default {
fontSize: '10px' fontSize: '10px'
}, },
labelStyle: {}, labelStyle: {},
formatter: (value, name, { unit }) => { formatter: (value, name, {unit}) => {
if (unit && unit.trim() === 'mb') { if (unit && unit.trim() === 'mb') {
return numberWithCommas(Math.round(value / 1024 / 1024)) return numberWithCommas(Math.round(value / 1024 / 1024))
} }
@ -77,12 +78,12 @@ export default {
gradientDef: () => ( gradientDef: () => (
<defs> <defs>
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={colors[2]} stopOpacity={ 0.5 } /> <stop offset="5%" stopColor={compareColors[2]} stopOpacity={0.5}/>
<stop offset="95%" stopColor={colors[2]} stopOpacity={ 0.2 } /> <stop offset="95%" stopColor={compareColors[2]} stopOpacity={0.2}/>
</linearGradient> </linearGradient>
<linearGradient id="colorCountCompare" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="colorCountCompare" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={compareColors[4]} stopOpacity={ 0.9 } /> <stop offset="5%" stopColor={compareColors[4]} stopOpacity={0.9}/>
<stop offset="95%" stopColor={compareColors[4]} stopOpacity={ 0.2 } /> <stop offset="95%" stopColor={compareColors[4]} stopOpacity={0.2}/>
</linearGradient> </linearGradient>
</defs> </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; return unmount;
}, [history]); }, [history]);
return ( 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-center mb-4 justify-between px-6">
<div className="flex items-baseline mr-3"> <div className="flex items-baseline mr-3">
<PageTitle title="Alerts" /> <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> {/* <Form.Field>
<label>{'Description:'}</label> <label>{'Description:'}</label>
<Input <Input
className="" className=""
@ -69,7 +69,7 @@ function DashboardEditModal(props: Props) {
maxLength={300} maxLength={300}
autoFocus={!focusTitle} autoFocus={!focusTitle}
/> />
</Form.Field> </Form.Field> */}
<Form.Field> <Form.Field>
<div className="flex items-center"> <div className="flex items-center">

View file

@ -1,16 +1,18 @@
import React from 'react'; import React from 'react';
import Breadcrumb from 'Shared/Breadcrumb'; import Breadcrumb from 'Shared/Breadcrumb';
import { withSiteId } from 'App/routes'; import {withSiteId} from 'App/routes';
import { withRouter, RouteComponentProps } from 'react-router-dom'; import {withRouter, RouteComponentProps} from 'react-router-dom';
import { Button, PageTitle, confirm, Tooltip } from 'UI'; import {Button, PageTitle, confirm, Tooltip} from 'UI';
import SelectDateRange from 'Shared/SelectDateRange'; import SelectDateRange from 'Shared/SelectDateRange';
import { useStore } from 'App/mstore'; import {useStore} from 'App/mstore';
import { useModal } from 'App/components/Modal'; import {useModal} from 'App/components/Modal';
import DashboardOptions from '../DashboardOptions'; import DashboardOptions from '../DashboardOptions';
import withModal from 'App/components/Modal/withModal'; import withModal from 'App/components/Modal/withModal';
import { observer } from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import DashboardEditModal from '../DashboardEditModal'; 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 { interface IProps {
dashboardId: string; dashboardId: string;
@ -18,12 +20,14 @@ interface IProps {
renderReport?: any; renderReport?: any;
} }
type Props = IProps & RouteComponentProps; type Props = IProps & RouteComponentProps;
const MAX_CARDS = 29; const MAX_CARDS = 29;
function DashboardHeader(props: Props) { function DashboardHeader(props: Props) {
const { siteId, dashboardId } = props; const {siteId, dashboardId} = props;
const { dashboardStore } = useStore(); const {dashboardStore} = useStore();
const { showModal } = useModal(); const {showModal} = useModal();
const [focusTitle, setFocusedInput] = React.useState(true); const [focusTitle, setFocusedInput] = React.useState(true);
const [showEditModal, setShowEditModal] = React.useState(false); const [showEditModal, setShowEditModal] = React.useState(false);
const period = dashboardStore.period; const period = dashboardStore.period;
@ -40,7 +44,7 @@ function DashboardHeader(props: Props) {
const onDelete = async () => { const onDelete = async () => {
if ( if (
await confirm({ await confirm({
header: 'Confirm', header: 'Delete Dashboard',
confirmButton: 'Yes, delete', confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`, confirmation: `Are you sure you want to permanently delete this Dashboard?`,
}) })
@ -63,11 +67,11 @@ function DashboardHeader(props: Props) {
label: 'Dashboards', label: 'Dashboards',
to: withSiteId('/dashboard', siteId), to: withSiteId('/dashboard', siteId),
}, },
{ label: (dashboard && dashboard.name) || '' }, {label: (dashboard && dashboard.name) || ''},
]} ]}
/> />
<div className="flex items-center mb-2 justify-between"> <div className="flex items-center mb-2 justify-between">
<div className="flex items-center" style={{ flex: 3 }}> <div className="flex items-center" style={{flex: 3}}>
<PageTitle <PageTitle
title={ title={
// @ts-ignore // @ts-ignore
@ -79,33 +83,23 @@ function DashboardHeader(props: Props) {
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer" className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
/> />
</div> </div>
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}> <div className="flex items-center gap-2" style={{flex: 1, justifyContent: 'end'}}>
<Tooltip delay={0} disabled={canAddMore} title="The number of cards in one dashboard is limited to 30."> <CreateCardButton disabled={canAddMore} />
<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 <div
className="flex items-center flex-shrink-0 justify-end" className="flex items-center flex-shrink-0 justify-end dashboardDataPeriodSelector"
style={{ width: 'fit-content' }} style={{width: 'fit-content'}}
> >
<SelectDateRange <SelectDateRange
style={{ width: '300px' }} style={{width: '300px'}}
period={period} period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)} onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true} right={true}
isAnt={true}
useButtonStyle={true}
/> />
</div> </div>
<div className="mx-4" />
<div className="flex items-center flex-shrink-0"> <div className="flex items-center flex-shrink-0">
<DashboardOptions <DashboardOptions
editHandler={onEdit} editHandler={onEdit}
@ -123,7 +117,7 @@ function DashboardHeader(props: Props) {
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" 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)} onDoubleClick={() => onEdit(false)}
> >
{dashboard?.description || 'Describe the purpose of this dashboard'} {/* {dashboard?.description || 'Describe the purpose of this dashboard'} */}
</h2> </h2>
</Tooltip> </Tooltip>
</div> </div>

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 React from 'react';
import { NoContent, Pagination } from 'UI'; import {connect} from 'react-redux';
import { useStore } from 'App/mstore'; import {withRouter} from 'react-router-dom';
import { sliceListPerPage } from 'App/utils';
import DashboardListItem from './DashboardListItem';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { Tooltip } from 'antd';
function DashboardList() { import {checkForRecent} from 'App/date';
const { dashboardStore } = useStore(); import {useStore} from 'App/mstore';
import Dashboard from 'App/mstore/types/dashboard';
import {dashboardSelected, withSiteId} from 'App/routes';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import {useHistory} from "react-router";
function DashboardList({siteId}: { siteId: string }) {
const {dashboardStore} = useStore();
const list = dashboardStore.filteredList; const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.filter.query; const dashboardsSearch = dashboardStore.filter.query;
const lenth = list.length; const history = useHistory();
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 ( return (
<NoContent list.length === 0 && !dashboardStore.filter.showMine ? (
show={lenth === 0} <Empty
title={ image={<AnimatedSVG name={dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS} size={600}/>}
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} /> imageStyle={{height: 300}}
<div className="text-center mt-4"> description={(
{dashboardsSearch !== '' ? 'No matching results' : "You haven't created any dashboards yet"} <div className="text-center">
</div>
</div>
}
subtext={
<div> <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. <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>
}
>
<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> </div>
{sliceListPerPage(list, dashboardStore.page - 1, dashboardStore.pageSize).map(
(dashboard: any) => (
<React.Fragment key={dashboard.dashboardId}>
<DashboardListItem dashboard={dashboard} />
</React.Fragment>
)
)} )}
</div>
<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> <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 React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'App/mstore'; import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import { debounce } from 'App/utils'; import { debounce } from 'App/utils';
import { Input } from 'antd';
let debounceUpdate: any = () => {}; let debounceUpdate: any = () => {};
@ -24,16 +24,15 @@ function DashboardSearch() {
}; };
return ( return (
<div className="relative"> <Input.Search
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
value={query} value={query}
allowClear
name="dashboardsSearch" name="dashboardsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10" className="w-full"
placeholder="Filter by title or description" placeholder="Filter by title or description"
onChange={write} onChange={write}
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
/> />
</div>
); );
} }

View file

@ -3,11 +3,11 @@ import withPageTitle from 'HOCs/withPageTitle';
import DashboardList from './DashboardList'; import DashboardList from './DashboardList';
import Header from './Header'; import Header from './Header';
function DashboardsView({ history, siteId }: { history: any; siteId: string }) { function DashboardsView({history, siteId}: { history: any; siteId: string }) {
return ( 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 py-4 border shadow-sm">
<Header history={history} siteId={siteId} /> <Header />
<DashboardList /> <DashboardList/>
</div> </div>
); );
} }

View file

@ -1,65 +1,27 @@
import React from 'react'; 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 DashboardSearch from './DashboardSearch';
import { useStore } from 'App/mstore'; import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import { observer, useObserver } from 'mobx-react-lite';
import { withSiteId } from 'App/routes';
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));
});
};
function Header() {
return ( return (
<> <>
<div className="flex items-center justify-between px-6"> <div className="flex items-center justify-between px-4 pb-2">
<div className="flex items-baseline mr-3"> <div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" /> <PageTitle title="Dashboards"/>
</div> </div>
<div className="ml-auto flex items-center"> <div className="ml-auto flex items-center">
<Button variant="primary" onClick={onAddDashboardClick}> <CreateDashboardButton/>
New Dashboard
</Button>
<div className="mx-2"></div> <div className="mx-2"></div>
<div className="w-1/4" style={{ minWidth: 300 }}> <div className="w-1/4" style={{minWidth: 300}}>
<DashboardSearch /> <DashboardSearch/>
</div> </div>
</div> </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>
</> </>
); );
} }
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 { editHandler, deleteHandler, renderReport, isEnterprise, isTitlePresent } = props;
const menuItems = [ const menuItems = [
{ icon: 'pencil', text: 'Rename', onClick: () => editHandler(true) }, { 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: 'users', text: 'Visibility & Access', onClick: editHandler },
{ icon: 'trash', text: 'Delete', onClick: deleteHandler }, { icon: 'trash', text: 'Delete', onClick: deleteHandler },
{ icon: 'pdf-download', text: 'Download Report', onClick: renderReport, disabled: !isEnterprise, tooltipTitle: ENTERPRISE_REQUEIRED } { icon: 'pdf-download', text: 'Download Report', onClick: renderReport, disabled: !isEnterprise, tooltipTitle: ENTERPRISE_REQUEIRED }
@ -23,7 +23,6 @@ function DashboardOptions(props: Props) {
return ( return (
<ItemMenu <ItemMenu
bold bold
label="More Options"
items={menuItems} 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 React from 'react';
import { Button, Modal, Form, Icon } from 'UI'; import {Button, Modal, Form, Icon} from 'UI';
import { useStore } from 'App/mstore'
import {useStore} from 'App/mstore'
import Select from 'Shared/Select'; import Select from 'Shared/Select';
interface Props { interface Props {
@ -9,9 +10,10 @@ interface Props {
show: boolean; show: boolean;
closeHandler?: () => void; closeHandler?: () => void;
} }
function DashboardSelectionModal(props: Props) { function DashboardSelectionModal(props: Props) {
const { show, metricId, closeHandler } = props; const {show, metricId, closeHandler} = props;
const { dashboardStore } = useStore(); const {dashboardStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({ const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id, key: i.id,
label: i.name, label: i.name,
@ -41,16 +43,16 @@ function DashboardSelectionModal(props: Props) {
}, []) }, [])
return useObserver(() => ( return useObserver(() => (
<Modal size="small" open={ show } onClose={closeHandler}> <Modal size="small" open={show} onClose={closeHandler}>
<Modal.Header className="flex items-center justify-between"> <Modal.Header className="flex items-center justify-between">
<div>{ 'Add to selected dashboard' }</div> <div className='text-xl font-medium'>{'Add to selected dashboard'}</div>
<Icon <Icon
role="button" role="button"
tabIndex="-1" tabIndex="-1"
color="gray-dark" color="gray-dark"
size="14" size="14"
name="close" name="close"
onClick={ closeHandler } onClick={closeHandler}
/> />
</Modal.Header> </Modal.Header>
@ -60,19 +62,19 @@ function DashboardSelectionModal(props: Props) {
<Select <Select
options={dashboardOptions} options={dashboardOptions}
defaultValue={dashboardOptions[0].value} defaultValue={dashboardOptions[0].value}
onChange={({ value }: any) => setSelectedId(value.value)} onChange={({value}: any) => setSelectedId(value.value)}
/> />
</Form.Field> </Form.Field>
</Modal.Content> </Modal.Content>
<Modal.Footer> <Modal.Footer>
<Button <Button
variant="primary" variant="primary"
onClick={ onSave } onClick={onSave}
className="float-left mr-2" className="float-left mr-2 "
> >
Add Add
</Button> </Button>
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button> <Button className="mr-2" onClick={closeHandler}>{'Cancel'}</Button>
</Modal.Footer> </Modal.Footer>
</Modal> </Modal>
)); ));

View file

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

View file

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

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,11 +1,69 @@
import React, { useState } from 'react'; import React, {useState} from 'react';
import FilterList from 'Shared/Filters/FilterList'; import FilterList from 'Shared/Filters/FilterList';
import { Button, Icon } from 'UI';
import FilterSelection from 'Shared/Filters/FilterSelection';
import SeriesName from './SeriesName'; import SeriesName from './SeriesName';
import cn from 'classnames'; import cn from 'classnames';
import { observer } from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import ExcludeFilters from './ExcludeFilters'; 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 { interface Props {
seriesIndex: number; seriesIndex: number;
@ -18,25 +76,23 @@ interface Props {
observeChanges?: () => void; observeChanges?: () => void;
excludeFilterKeys?: Array<string>; excludeFilterKeys?: Array<string>;
canExclude?: boolean; canExclude?: boolean;
expandable?: boolean;
} }
function FilterSeries(props: Props) { function FilterSeries(props: Props) {
const { const {
observeChanges = () => {}, observeChanges = () => {
},
canDelete, canDelete,
hideHeader = false, hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.', emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
supportsEmpty = true, supportsEmpty = true,
excludeFilterKeys = [], excludeFilterKeys = [],
canExclude = false, canExclude = false,
expandable = false
} = props; } = props;
const [expanded, setExpanded] = useState(true); const [expanded, setExpanded] = useState(!expandable);
const { series, seriesIndex } = props; const {series, seriesIndex} = props;
const onAddFilter = (filter: any) => {
series.filter.addFilter(filter);
observeChanges();
};
const onUpdateFilter = (filterIndex: any, filter: any) => { const onUpdateFilter = (filterIndex: any, filter: any) => {
series.filter.updateFilter(filterIndex, filter); series.filter.updateFilter(filterIndex, filter);
@ -48,7 +104,8 @@ function FilterSeries(props: Props) {
observeChanges(); observeChanges();
} }
const onChangeEventsOrder = (_: any, { name, value }: any) => { const onChangeEventsOrder = (_: any, {name, value}: any) => {
console.log(name, value)
series.filter.updateKey(name, value); series.filter.updateKey(name, value);
observeChanges(); observeChanges();
}; };
@ -59,27 +116,28 @@ function FilterSeries(props: Props) {
}; };
return ( return (
<div className="border rounded bg-white"> <div className="border rounded-lg shadow-sm bg-white">
{canExclude && <ExcludeFilters filter={series.filter} />} {canExclude && <ExcludeFilters filter={series.filter}/>}
<div className={cn('border-b px-5 h-12 flex items-center relative', { hidden: hideHeader })}>
<div className="mr-auto"> {!hideHeader && (
<SeriesName <FilterSeriesHeader hidden={hideHeader}
seriesIndex={seriesIndex} seriesIndex={seriesIndex}
name={series.name} series={series}
onUpdate={(name) => series.update('name', name)} onRemove={props.onRemoveSeries}
/> canDelete={canDelete}
</div> expanded={expanded}
toggleExpand={() => setExpanded(!expanded)}/>
)}
<div className="flex items-center cursor-pointer"> {expandable && !expanded && (
<div onClick={props.onRemoveSeries} className={cn('ml-3', { disabled: !canDelete })}> <Space className="justify-between w-full px-5 py-2">
<Icon name="trash" size="16" /> <FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
</div> <Button onClick={() => setExpanded(!expanded)}
size="small"
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
</Space>
)}
<div onClick={() => setExpanded(!expanded)} className="ml-3">
<Icon name="chevron-down" size="16" />
</div>
</div>
</div>
{expanded && ( {expanded && (
<> <>
<div className="p-5"> <div className="p-5">
@ -92,22 +150,21 @@ function FilterSeries(props: Props) {
supportsEmpty={supportsEmpty} supportsEmpty={supportsEmpty}
onFilterMove={onFilterMove} onFilterMove={onFilterMove}
excludeFilterKeys={excludeFilterKeys} 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 className="color-gray-medium">{emptyMessage}</div>
)} )}
</div> </div>
<div className="border-t h-12 flex items-center"> <div className="border-t h-12 flex items-center">
<div className="-mx-4 px-6"> <div className="-mx-4 px-5">
<FilterSelection <AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
filter={undefined}
onFilterClick={onAddFilter}
excludeFilterKeys={excludeFilterKeys}
>
<Button variant="text-primary" icon="plus">
ADD STEP
</Button>
</FilterSelection>
</div> </div>
</div> </div>
</> </>

View file

@ -59,7 +59,7 @@ function FunnelIssues() {
return useObserver(() => ( return useObserver(() => (
<div className="my-8 bg-white rounded p-4 border"> <div className="my-8 bg-white rounded p-4 border">
<div className="flex"> <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>
<div className="my-6 flex justify-between items-start"> <div className="my-6 flex justify-between items-start">
<FunnelIssuesDropdown /> <FunnelIssuesDropdown />

View file

@ -1,31 +1,35 @@
import React from 'react'; import React from 'react';
import { PageTitle, Button, Toggler, Icon } from "UI"; import {PageTitle, Button, Toggler, Icon} from "UI";
import { Segmented } from 'antd'; import {Segmented} from 'antd';
import MetricsSearch from '../MetricsSearch'; import MetricsSearch from '../MetricsSearch';
import Select from 'Shared/Select'; import Select from 'Shared/Select';
import { useStore } from 'App/mstore'; import {useStore} from 'App/mstore';
import { observer, useObserver } from 'mobx-react-lite'; import {observer, useObserver} from 'mobx-react-lite';
import { DROPDOWN_OPTIONS } from 'App/constants/card'; import {DROPDOWN_OPTIONS} from 'App/constants/card';
import AddCardModal from 'Components/Dashboard/components/AddCardModal'; 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 }) { function MetricViewHeader({siteId}: { siteId: string }) {
const { metricStore } = useStore(); const {metricStore} = useStore();
const filter = metricStore.filter; const filter = metricStore.filter;
const { showModal } = useModal(); const {showModal} = useModal();
const [showAddCardModal, setShowAddCardModal] = React.useState(false);
return ( return (
<div> <div>
<div className='flex items-center justify-between px-6'> <div className='flex items-center justify-between px-6'>
<div className='flex items-baseline mr-3'> <div className='flex items-baseline mr-3'>
<PageTitle title='Cards' className='' /> <PageTitle title='Cards' className=''/>
</div> </div>
<div className='ml-auto flex items-center'> <div className='ml-auto flex items-center'>
<Button variant='primary' <Button variant='primary'
onClick={() => showModal(<AddCardModal siteId={siteId} />, { right: true })} // onClick={() => showModal(<AddCardModal siteId={siteId}/>, {right: true})}
onClick={() => setShowAddCardModal(true)}
>New Card</Button> >New Card</Button>
<div className='ml-4 w-1/4' style={{ minWidth: 300 }}> <div className='ml-4 w-1/4' style={{minWidth: 300}}>
<MetricsSearch /> <MetricsSearch/>
</div> </div>
</div> </div>
</div> </div>
@ -38,15 +42,15 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
name='test' name='test'
className='font-medium mr-2' className='font-medium mr-2'
onChange={() => onChange={() =>
metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine }) metricStore.updateKey('filter', {...filter, showMine: !filter.showMine})
} }
/> />
<Select <Select
options={[{ label: 'All Types', value: 'all' }, ...DROPDOWN_OPTIONS]} options={[{label: 'All Types', value: 'all'}, ...DROPDOWN_OPTIONS]}
name='type' name='type'
defaultValue={filter.type} defaultValue={filter.type}
onChange={({ value }) => onChange={({value}) =>
metricStore.updateKey('filter', { ...filter, type: value.value }) metricStore.updateKey('filter', {...filter, type: value.value})
} }
plain={true} plain={true}
isSearchable={true} isSearchable={true}
@ -55,26 +59,33 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
<DashboardDropdown <DashboardDropdown
plain={true} plain={true}
onChange={(value: any) => onChange={(value: any) =>
metricStore.updateKey('filter', { ...filter, dashboard: value }) metricStore.updateKey('filter', {...filter, dashboard: value})
} }
/> />
</div> </div>
<div className='flex items-center'> <div className='flex items-center'>
<ListViewToggler /> <ListViewToggler/>
<Select <Select
options={[ options={[
{ label: 'Newest', value: 'desc' }, {label: 'Newest', value: 'desc'},
{ label: 'Oldest', value: 'asc' } {label: 'Oldest', value: 'asc'}
]} ]}
name='sort' name='sort'
defaultValue={metricStore.sort.by} defaultValue={metricStore.sort.by}
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })} onChange={({value}) => metricStore.updateKey('sort', {by: value.value})}
plain={true} plain={true}
className='ml-4' className='ml-4'
/> />
</div> </div>
{/*<AddCardSelectionModal open={showAddCardModal}/>*/}
<NewDashboardModal
onClose={() => setShowAddCardModal(false)}
open={showAddCardModal}
isCreatingNewCard={true}
/>
</div> </div>
</div> </div>
); );
@ -82,8 +93,8 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
export default observer(MetricViewHeader); export default observer(MetricViewHeader);
function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) { function DashboardDropdown({onChange, plain = false}: { plain?: boolean; onChange: any }) {
const { dashboardStore, metricStore } = useStore(); const {dashboardStore, metricStore} = useStore();
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({ const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
key: i.id, key: i.id,
label: i.name, label: i.name,
@ -97,14 +108,14 @@ function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onCha
plain={plain} plain={plain}
options={dashboardOptions} options={dashboardOptions}
value={metricStore.filter.dashboard} value={metricStore.filter.dashboard}
onChange={({ value }: any) => onChange(value)} onChange={({value}: any) => onChange(value)}
isMulti={true} isMulti={true}
/> />
); );
} }
function ListViewToggler() { function ListViewToggler() {
const { metricStore } = useStore(); const {metricStore} = useStore();
const listView = useObserver(() => metricStore.listView); const listView = useObserver(() => metricStore.listView);
return ( return (
<div className='flex items-center'> <div className='flex items-center'>
@ -112,14 +123,14 @@ function ListViewToggler() {
options={[ options={[
{ {
label: <div className={'flex items-center gap-2'}> label: <div className={'flex items-center gap-2'}>
<Icon name={'list-alt'} color={'inherit'} /> <Icon name={'list-alt'} color={'inherit'}/>
<div>List</div> <div>List</div>
</div>, </div>,
value: 'list' value: 'list'
}, },
{ {
label: <div className={'flex items-center gap-2'}> label: <div className={'flex items-center gap-2'}>
<Icon name={'grid'} color={'inherit'} /> <Icon name={'grid'} color={'inherit'}/>
<div>Grid</div> <div>Grid</div>
</div>, </div>,
value: 'grid' value: 'grid'

View file

@ -9,7 +9,7 @@ interface Props {
} }
function MetricsView({ siteId }: Props) { function MetricsView({ siteId }: Props) {
return useObserver(() => ( 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} /> <MetricViewHeader siteId={siteId} />
<MetricsList siteId={siteId} /> <MetricsList siteId={siteId} />
</div> </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 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 CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable'; import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart'; 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 InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
import SankeyChart from 'Shared/Insights/SankeyChart'; import SankeyChart from 'Shared/Insights/SankeyChart';
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard'; import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
interface Props { interface Props {
metric: any; metric: any;
@ -140,7 +141,7 @@ function WidgetChart(props: Props) {
if (metricType === TIMESERIES) { if (metricType === TIMESERIES) {
if (viewType === 'lineChart') { if (viewType === 'lineChart') {
return ( return (
<CustomMetriLineChart <CustomMetricLineChart
data={data} data={data}
colors={colors} colors={colors}
params={params} params={params}
@ -181,11 +182,17 @@ function WidgetChart(props: Props) {
} }
if (viewType === TABLE) { if (viewType === TABLE) {
return ( return (
<CustomMetricTable <SessionsBy
metric={metric} data={data[0]} metric={metric}
data={data[0]}
onClick={onChartClick} onClick={onChartClick}
isTemplate={isTemplate} isTemplate={isTemplate}
/> />
// <CustomMetricTable
// metric={metric} data={data[0]}
// onClick={onChartClick}
// isTemplate={isTemplate}
// />
); );
} else if (viewType === 'pieChart') { } else if (viewType === 'pieChart') {
return ( return (
@ -229,7 +236,7 @@ function WidgetChart(props: Props) {
if (metricType === RETENTION) { if (metricType === RETENTION) {
if (viewType === 'trend') { if (viewType === 'trend') {
return ( return (
<CustomMetriLineChart <CustomMetricLineChart
data={data} data={data}
colors={colors} colors={colors}
params={params} params={params}

View file

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

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,12 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState} from 'react';
import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions'; import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions';
import { FilterKey } from 'Types/filter/filterType'; import {FilterKey} from 'Types/filter/filterType';
import { useStore } from 'App/mstore'; import {useStore} from 'App/mstore';
import { observer } from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import { Button, Icon, confirm, Tooltip } from 'UI'; import {Button, Icon, confirm, Tooltip} from 'UI';
import FilterSeries from '../FilterSeries'; import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select'; 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 MetricTypeDropdown from './components/MetricTypeDropdown';
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
import { import {
@ -22,28 +22,29 @@ import {
USER_PATH, USER_PATH,
RETENTION RETENTION
} from 'App/constants/card'; } from 'App/constants/card';
import { eventKeys } from 'App/types/filter/newFilter'; import {eventKeys} from 'App/types/filter/newFilter';
import { renderClickmapThumbnail } from './renderMap'; import {renderClickmapThumbnail} from './renderMap';
import Widget from 'App/mstore/types/widget'; import Widget from 'App/mstore/types/widget';
import FilterItem from 'Shared/Filters/FilterItem'; import FilterItem from 'Shared/Filters/FilterItem';
import { Input } from 'antd' import {Input} from 'antd'
interface Props { interface Props {
history: any; history: any;
match: any; match: any;
onDelete: () => void; onDelete: () => void;
expanded?: boolean;
} }
function WidgetForm(props: Props) { function WidgetForm(props: Props) {
const { const {
history, history,
match: { match: {
params: { siteId, dashboardId } params: {siteId, dashboardId}
} }
} = props; } = props;
const [aiQuery, setAiQuery] = useState('') const [aiQuery, setAiQuery] = useState('')
const [aiAskChart, setAiAskChart] = useState('') const [aiAskChart, setAiAskChart] = useState('')
const { metricStore, dashboardStore, aiFiltersStore } = useStore(); const {metricStore, dashboardStore, aiFiltersStore} = useStore();
const isSaving = metricStore.isSaving; const isSaving = metricStore.isSaving;
const metric: any = metricStore.instance; const metric: any = metricStore.instance;
const [initialInstance, setInitialInstance] = useState(); const [initialInstance, setInitialInstance] = useState();
@ -72,9 +73,9 @@ function WidgetForm(props: Props) {
} }
}, [metric]); }, [metric]);
const writeOption = ({ value, name }: { value: any; name: any }) => { const writeOption = ({value, name}: { value: any; name: any }) => {
value = Array.isArray(value) ? value : value.value; value = Array.isArray(value) ? value : value.value;
const obj: any = { [name]: value }; const obj: any = {[name]: value};
if (name === 'metricType') { if (name === 'metricType') {
switch (value) { switch (value) {
@ -158,11 +159,12 @@ function WidgetForm(props: Props) {
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true'; const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
return ( return (
<div className='p-6'> <div className='p-6'>
{/*
<div className='form-group'> <div className='form-group'>
<div className='flex items-center'> <div className='flex items-center'>
<span className='mr-2'>Card showing</span> <span className='mr-2'>Card showing</span>
<MetricTypeDropdown onSelect={writeOption} /> <MetricTypeDropdown onSelect={writeOption}/>
<MetricSubtypeDropdown onSelect={writeOption} /> <MetricSubtypeDropdown onSelect={writeOption}/>
{isPathAnalysis && ( {isPathAnalysis && (
<> <>
@ -170,8 +172,8 @@ function WidgetForm(props: Props) {
<Select <Select
name='startType' name='startType'
options={[ options={[
{ value: 'start', label: 'With Start Point' }, {value: 'start', label: 'With Start Point'},
{ value: 'end', label: 'With End Point' } {value: 'end', label: 'With End Point'}
]} ]}
defaultValue={metric.startType} defaultValue={metric.startType}
// value={metric.metricOf} // value={metric.metricOf}
@ -183,10 +185,10 @@ function WidgetForm(props: Props) {
<Select <Select
name='metricValue' name='metricValue'
options={[ options={[
{ value: 'location', label: 'Pages' }, {value: 'location', label: 'Pages'},
{ value: 'click', label: 'Clicks' }, {value: 'click', label: 'Clicks'},
{ value: 'input', label: 'Input' }, {value: 'input', label: 'Input'},
{ value: 'custom', label: 'Custom' }, {value: 'custom', label: 'Custom'},
]} ]}
defaultValue={metric.metricValue} defaultValue={metric.metricValue}
isMulti={true} isMulti={true}
@ -231,7 +233,7 @@ function WidgetForm(props: Props) {
<span className='mx-3'>showing</span> <span className='mx-3'>showing</span>
<Select <Select
name='metricFormat' name='metricFormat'
options={[{ value: 'sessionCount', label: 'Session Count' }]} options={[{value: 'sessionCount', label: 'Session Count'}]}
defaultValue={metric.metricFormat} defaultValue={metric.metricFormat}
onChange={writeOption} onChange={writeOption}
/> />
@ -240,6 +242,9 @@ function WidgetForm(props: Props) {
</div> </div>
</div> </div>
*/}
{isPathAnalysis && ( {isPathAnalysis && (
<div className='form-group flex flex-col'> <div className='form-group flex flex-col'>
{metric.startType === 'start' ? 'Start Point' : 'End Point'} {metric.startType === 'start' ? 'Start Point' : 'End Point'}
@ -251,13 +256,13 @@ function WidgetForm(props: Props) {
onUpdate={(val) => { onUpdate={(val) => {
metric.updateStartPoint(val); metric.updateStartPoint(val);
}} onRemoveFilter={() => { }} onRemoveFilter={() => {
}} /> }}/>
</div> </div>
)} )}
{isPredefined && ( {isPredefined && (
<div className='flex items-center my-6 justify-center'> <div className='flex items-center my-6 justify-center'>
<Icon name='info-circle' size='18' color='gray-medium' /> <Icon name='info-circle' size='18' color='gray-medium'/>
<div className='ml-2'> <div className='ml-2'>
Filtering and drill-downs will be supported soon for this card type. Filtering and drill-downs will be supported soon for this card type.
</div> </div>
@ -349,7 +354,7 @@ function WidgetForm(props: Props) {
<div className='flex items-center'> <div className='flex items-center'>
{metric.exists() && ( {metric.exists() && (
<Button variant='text-primary' onClick={onDelete}> <Button variant='text-primary' onClick={onDelete}>
<Icon name='trash' size='14' className='mr-2' color='teal' /> <Icon name='trash' size='14' className='mr-2' color='teal'/>
Delete Delete
</Button> </Button>
)} )}

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

View file

@ -1,43 +1,42 @@
import React from 'react'; import React from 'react';
import cn from 'classnames'; import cn from 'classnames';
import WidgetWrapper from '../WidgetWrapper'; import WidgetWrapper from '../WidgetWrapper';
import { useStore } from 'App/mstore'; import {useStore} from 'App/mstore';
import { SegmentSelection, Button, Icon } from 'UI'; // import {SegmentSelection, Button, Icon} from 'UI';
import { observer } from 'mobx-react-lite'; import {observer} from 'mobx-react-lite';
import { FilterKey } from 'Types/filter/filterType'; // import {FilterKey} from 'Types/filter/filterType';
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange'; // import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
import ClickMapRagePicker from "Components/Dashboard/components/ClickMapRagePicker"; import ClickMapRagePicker from "Components/Dashboard/components/ClickMapRagePicker";
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal'; // import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
import { CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH } from 'App/constants/card'; import {CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH} from 'App/constants/card';
import { Space, Switch } from 'antd'; import {Space, Switch} from 'antd';
// import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
interface Props { interface Props {
className?: string; className?: string;
name: string; name: string;
isEditing?: boolean; isEditing?: boolean;
} }
function WidgetPreview(props: Props) { function WidgetPreview(props: Props) {
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = React.useState(false); const {className = ''} = props;
const { className = '' } = props; const {metricStore, dashboardStore} = useStore();
const { metricStore, dashboardStore } = useStore(); // const dashboards = dashboardStore.dashboards;
const dashboards = dashboardStore.dashboards;
const metric: any = metricStore.instance; const metric: any = metricStore.instance;
const isTimeSeries = metric.metricType === TIMESERIES; // const isTimeSeries = metric.metricType === TIMESERIES;
const isTable = metric.metricType === TABLE; // const isTable = metric.metricType === TABLE;
const isRetention = metric.metricType === RETENTION; // const isRetention = metric.metricType === RETENTION;
const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS; // const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS;
//
const changeViewType = (_, { name, value }: any) => { // const changeViewType = (_, {name, value}: any) => {
metric.update({ [ name ]: value }); // metric.update({[name]: value});
} // }
const canAddToDashboard = metric.exists() && dashboards.length > 0;
return ( return (
<> <>
<div className={cn(className, 'bg-white rounded border')}> <div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
<div className="flex items-center justify-between px-4 pt-2"> <div className="flex items-center justify-between px-4 pt-2">
<h2 className="text-2xl"> <h2 className="text-xl">
{props.name} {props.name}
</h2> </h2>
<div className="flex items-center"> <div className="flex items-center">
@ -46,7 +45,7 @@ function WidgetPreview(props: Props) {
href="#" href="#"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
metric.update({ hideExcess: !metric.hideExcess }); metric.update({hideExcess: !metric.hideExcess});
}} }}
> >
<Space> <Space>
@ -58,91 +57,79 @@ function WidgetPreview(props: Props) {
</Space> </Space>
</a> </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' },
]}
/>
</>
)}
{!disableVisualization && isTable && ( {/*{isTimeSeries && (*/}
<> {/* <>*/}
<span className="mr-4 color-gray-medium">Visualization</span> {/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
<SegmentSelection {/* <SegmentSelection*/}
name="viewType" {/* name="viewType"*/}
className="my-3" {/* className="my-3"*/}
primary={true} {/* primary*/}
size="small" {/* size="small"*/}
onSelect={ changeViewType } {/* onSelect={ changeViewType }*/}
value={{ value: metric.viewType }} {/* value={{ value: metric.viewType }}*/}
list={[ {/* list={ [*/}
{ value: 'table', name: 'Table', icon: 'table' }, {/* { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },*/}
{ value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' }, {/* { value: 'progress', name: 'Progress', icon: 'hash' },*/}
]} {/* ]}*/}
disabledMessage="Chart view is not supported" {/* />*/}
/> {/* </>*/}
</> {/*)}*/}
)}
{isRetention && ( {/*{!disableVisualization && isTable && (*/}
<> {/* <>*/}
<span className="mr-4 color-gray-medium">Visualization</span> {/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
<SegmentSelection {/* <SegmentSelection*/}
name="viewType" {/* name="viewType"*/}
className="my-3" {/* className="my-3"*/}
primary={true} {/* primary={true}*/}
size="small" {/* size="small"*/}
onSelect={ changeViewType } {/* onSelect={ changeViewType }*/}
value={{ value: metric.viewType }} {/* value={{ value: metric.viewType }}*/}
list={[ {/* list={[*/}
{ value: 'trend', name: 'Trend', icon: 'graph-up-arrow' }, {/* { value: 'table', name: 'Table', icon: 'table' },*/}
{ value: 'cohort', name: 'Cohort', icon: 'dice-3' }, {/* { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },*/}
]} {/* ]}*/}
disabledMessage="Chart view is not supported" {/* disabledMessage="Chart view is not supported"*/}
/> {/* />*/}
</> {/* </>*/}
)} {/*)}*/}
<div className="mx-4" />
{/*{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 ? ( {metric.metricType === CLICKMAP ? (
<ClickMapRagePicker /> <ClickMapRagePicker/>
) : null} ) : null}
<WidgetDateRange />
{/* add to dashboard */} {/* add to dashboard */}
{metric.exists() && ( {/*{metric.exists() && (*/}
<Button {/* <AddToDashboardButton metricId={metric.metricId}/>*/}
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>
)}
</div> </div>
</div> </div>
<div className="p-4 pt-0"> <div className="pt-0">
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName /> <WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName/>
</div> </div>
</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 React, {useState} from 'react';
import { useStore } from 'App/mstore'; import {useStore} from 'App/mstore';
import cn from 'classnames'; import {Icon, Loader, NoContent} from 'UI';
import { Icon, Loader, NoContent } from 'UI';
import WidgetForm from '../WidgetForm';
import WidgetPreview from '../WidgetPreview'; import WidgetPreview from '../WidgetPreview';
import WidgetSessions from '../WidgetSessions'; import WidgetSessions from '../WidgetSessions';
import { useObserver } from 'mobx-react-lite'; import {useObserver} from 'mobx-react-lite';
import WidgetName from '../WidgetName'; import {dashboardMetricDetails, metricDetails, withSiteId} from 'App/routes';
import { withSiteId } from 'App/routes';
import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues'; import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues';
import Breadcrumb from 'Shared/Breadcrumb'; import Breadcrumb from 'Shared/Breadcrumb';
import { FilterKey } from 'Types/filter/filterType'; import {FilterKey} from 'Types/filter/filterType';
import { Prompt } from 'react-router'; import {Prompt, useHistory} from 'react-router';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import { import {
TIMESERIES, TIMESERIES,
TABLE, TABLE,
@ -21,36 +18,46 @@ import {
INSIGHTS, INSIGHTS,
USER_PATH, USER_PATH,
RETENTION, RETENTION,
} from 'App/constants/card'; } from 'App/constants/card';
import CardIssues from '../CardIssues'; import CardIssues from '../CardIssues';
import CardUserList from '../CardUserList/CardUserList'; 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 { interface Props {
history: any; history: any;
match: any; match: any;
siteId: any; siteId: any;
} }
function WidgetView(props: Props) { function WidgetView(props: Props) {
const { const {
match: { match: {
params: { siteId, dashboardId, metricId }, params: {siteId, dashboardId, metricId},
}, },
} = props; } = 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 widget = useObserver(() => metricStore.instance);
const loading = useObserver(() => metricStore.isLoading); const loading = useObserver(() => metricStore.isLoading);
const [expanded, setExpanded] = useState(!metricId || metricId === 'create'); const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
const hasChanged = useObserver(() => widget.hasChanged); const hasChanged = useObserver(() => widget.hasChanged);
const dashboards = useObserver(() => dashboardStore.dashboards); const dashboards = useObserver(() => dashboardStore.dashboards);
const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId)); const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId));
const dashboardName = dashboard ? dashboard.name : null; const dashboardName = dashboard ? dashboard.name : null;
const [metricNotFound, setMetricNotFound] = useState(false); const [metricNotFound, setMetricNotFound] = useState(false);
const history = useHistory();
const [initialInstance, setInitialInstance] = useState();
const isClickMap = widget.metricType === CLICKMAP;
React.useEffect(() => { React.useEffect(() => {
if (metricId && metricId !== 'create') { if (metricId && metricId !== 'create') {
metricStore.fetch(metricId, dashboardStore.period).catch((e) => { 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); setMetricNotFound(true);
} }
}); });
@ -59,13 +66,44 @@ function WidgetView(props: Props) {
} }
}, []); }, []);
const onBackHandler = () => { // const onBackHandler = () => {
props.history.goBack(); // props.history.goBack();
// };
//
// const openEdit = () => {
// if (expanded) return;
// setExpanded(true);
// };
const undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
}; };
const openEdit = () => { const onSave = async () => {
if (expanded) return; const wasCreating = !widget.exists();
setExpanded(true); 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(() => ( return useObserver(() => (
@ -80,57 +118,47 @@ function WidgetView(props: Props) {
}} }}
/> />
<div style={{ maxWidth: '1360px', margin: 'auto'}}> <div style={{maxWidth: '1360px', margin: 'auto'}}>
<Breadcrumb <Breadcrumb
items={[ items={[
{ {
label: dashboardName ? dashboardName : 'Cards', label: dashboardName ? dashboardName : 'Cards',
to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId), to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId),
}, },
{ label: widget.name }, {label: widget.name},
]} ]}
/> />
<NoContent <NoContent
show={metricNotFound} show={metricNotFound}
title={ title={
<div className="flex flex-col items-center justify-between"> <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 className="mt-4">Metric not found!</div>
</div> </div>
} }
> >
<div className="bg-white rounded border"> <Space direction="vertical" size={20} className="w-full">
<div <WidgetViewHeader onSave={onSave} undoChanges={undoChanges}/>
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>
{expanded && <WidgetForm onDelete={onBackHandler} {...props} />} <WidgetFormNew/>
</div>
<WidgetPreview className="mt-8" name={widget.name} isEditing={expanded} /> {/*<div className="bg-white rounded border mt-3">*/}
{/* <WidgetForm expanded={expanded} onDelete={onBackHandler} {...props} />*/}
{/*</div>*/}
<WidgetPreview name={widget.name} isEditing={expanded}/>
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && ( {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 === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) &&
{widget.metricType === FUNNEL && <FunnelIssues />} <WidgetSessions/>}
{widget.metricType === FUNNEL && <FunnelIssues/>}
</> </>
)} )}
{widget.metricType === USER_PATH && <CardIssues />} {widget.metricType === USER_PATH && <CardIssues/>}
{widget.metricType === RETENTION && <CardUserList />} {widget.metricType === RETENTION && <CardUserList/>}
</Space>
</NoContent> </NoContent>
</div> </div>
</Loader> </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,26 +1,33 @@
import React from 'react'; import React from 'react';
import WidgetIcon from './WidgetIcon'; 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 { interface Props {
seriesId: string; seriesId: string;
initAlert: Function; initAlert?: Function;
} }
function AlertButton(props: Props) { function AlertButton(props: Props) {
const { seriesId } = props; const {seriesId} = props;
const { dashboardStore, alertsStore } = useStore(); const {dashboardStore, alertsStore} = useStore();
const {openModal, closeModal} = useModal();
const onClick = () => { const onClick = () => {
dashboardStore.toggleAlertModal(true); // dashboardStore.toggleAlertModal(true);
alertsStore.init({ query: { left: seriesId }}) alertsStore.init({query: {left: seriesId}})
openModal(<AlertFormModal
onClose={closeModal}
/>, {
// title: 'Set Alerts',
placement: 'right',
width: 620,
});
} }
return ( return (
<div onClick={onClick}> <Button onClick={onClick} type="text" icon={<BellIcon size={16}/>}/>
<WidgetIcon
className="cursor-pointer"
icon="bell-plus"
tooltip="Set Alert"
/>
</div>
); );
} }

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 ( return (
<div <div
className={cn( className={cn(
'relative rounded bg-white border group', 'relative rounded bg-white border group rounded-lg',
'col-span-' + widget.config.col, 'col-span-' + widget.config.col,
{ 'hover:shadow-border-gray': !isTemplate && isWidget }, { 'hover:shadow-border-gray': !isTemplate && isWidget },
{ 'hover:shadow-border-main': isTemplate } { '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; left: 0;
right: 0; right: 0;
} }
.overlayDashboard { .overlayDashboard {
top: 20%!important; top: 40px !important;
} }

View file

@ -23,7 +23,7 @@ function ForgotPassword(props: Props) {
<div className="m-10 "> <div className="m-10 ">
<img src="/assets/logo.svg" width={200} /> <img src="/assets/logo.svg" width={200} />
</div> </div>
<div className="border rounded bg-white" style={{ width: '350px' }}> <div className="border rounded-lg bg-white shadow-sm" style={{ width: '350px' }}>
{creatingNewPassword ? ( {creatingNewPassword ? (
<h2 className="text-center text-lg font-medium mb-6 border-b p-5 w-full"> <h2 className="text-center text-lg font-medium mb-6 border-b p-5 w-full">
Welcome, join your organization by creating a new password Welcome, join your organization by creating a new password

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