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:
parent
d958549d64
commit
d604f9920b
346 changed files with 15161 additions and 2793 deletions
|
|
@ -1,24 +1,25 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { Loader } from 'UI';
|
||||
import { fetchUserInfo, setJwt } from 'Duck/user';
|
||||
import { fetchList as fetchSiteList } from 'Duck/site';
|
||||
import { withStore } from 'App/mstore';
|
||||
import { Map } from 'immutable';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {connect, ConnectedProps} from 'react-redux';
|
||||
import {Loader} from 'UI';
|
||||
import {fetchUserInfo, setJwt} from 'Duck/user';
|
||||
import {fetchList as fetchSiteList} from 'Duck/site';
|
||||
import {withStore} from 'App/mstore';
|
||||
import {Map} from 'immutable';
|
||||
|
||||
import * as routes from './routes';
|
||||
import { fetchTenants } from 'Duck/user';
|
||||
import { setSessionPath } from 'Duck/sessions';
|
||||
import { ModalProvider } from 'Components/Modal';
|
||||
import { GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM } from 'App/constants/storageKeys';
|
||||
import {fetchTenants} from 'Duck/user';
|
||||
import {setSessionPath} from 'Duck/sessions';
|
||||
import {ModalProvider} from 'Components/Modal';
|
||||
import {GLOBAL_DESTINATION_PATH, IFRAME, JWT_PARAM} from 'App/constants/storageKeys';
|
||||
import PublicRoutes from 'App/PublicRoutes';
|
||||
import Layout from 'App/layout/Layout';
|
||||
import { fetchListActive as fetchMetadata } from 'Duck/customField';
|
||||
import { init as initSite } from 'Duck/site';
|
||||
import {fetchListActive as fetchMetadata} from 'Duck/customField';
|
||||
import {init as initSite} from 'Duck/site';
|
||||
import PrivateRoutes from 'App/PrivateRoutes';
|
||||
import { checkParam } from 'App/utils';
|
||||
import {checkParam} from 'App/utils';
|
||||
import IFrameRoutes from 'App/IFrameRoutes';
|
||||
import {ModalProvider as NewModalProvider} from 'Components/ModalContext';
|
||||
|
||||
interface RouterProps extends RouteComponentProps, ConnectedProps<typeof connector> {
|
||||
isLoggedIn: boolean;
|
||||
|
|
@ -51,7 +52,7 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
fetchUserInfo,
|
||||
fetchSiteList,
|
||||
history,
|
||||
match: { params: { siteId: siteIdFromPath } },
|
||||
match: {params: {siteId: siteIdFromPath}},
|
||||
setSessionPath,
|
||||
} = props;
|
||||
const [isIframe, setIsIframe] = React.useState(false);
|
||||
|
|
@ -142,18 +143,20 @@ const Router: React.FC<RouterProps> = (props) => {
|
|||
location.pathname.includes('/assist/') || location.pathname.includes('multiview');
|
||||
|
||||
if (isIframe) {
|
||||
return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading} />;
|
||||
return <IFrameRoutes isJwt={isJwt} isLoggedIn={isLoggedIn} loading={loading}/>;
|
||||
}
|
||||
|
||||
return isLoggedIn ? (
|
||||
<NewModalProvider>
|
||||
<ModalProvider>
|
||||
<Loader loading={loading || !siteId} className='flex-1'>
|
||||
<Layout hideHeader={hideHeader} siteId={siteId}>
|
||||
<PrivateRoutes />
|
||||
<PrivateRoutes/>
|
||||
</Layout>
|
||||
</Loader>
|
||||
</ModalProvider>
|
||||
) : <PublicRoutes />;
|
||||
</NewModalProvider>
|
||||
) : <PublicRoutes/>;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: Map<string, any>) => {
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Button, Form, Input, SegmentSelection, Checkbox, Icon } from 'UI';
|
||||
import { alertConditions as conditions } from 'App/constants';
|
||||
import React, {useEffect} from 'react';
|
||||
import {Form, Input, SegmentSelection, Checkbox, Icon} from 'UI';
|
||||
import {alertConditions as conditions} from 'App/constants';
|
||||
import stl from './alertForm.module.css';
|
||||
import DropdownChips from './DropdownChips';
|
||||
import { validateEmail } from 'App/validate';
|
||||
import {validateEmail} from 'App/validate';
|
||||
import cn from 'classnames';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import {useStore} from 'App/mstore'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import Select from 'Shared/Select';
|
||||
import {Button} from "antd";
|
||||
|
||||
const thresholdOptions = [
|
||||
{ label: '15 minutes', value: 15 },
|
||||
{ label: '30 minutes', value: 30 },
|
||||
{ label: '1 hour', value: 60 },
|
||||
{ label: '2 hours', value: 120 },
|
||||
{ label: '4 hours', value: 240 },
|
||||
{ label: '1 day', value: 1440 },
|
||||
{label: '15 minutes', value: 15},
|
||||
{label: '30 minutes', value: 30},
|
||||
{label: '1 hour', value: 60},
|
||||
{label: '2 hours', value: 120},
|
||||
{label: '4 hours', value: 240},
|
||||
{label: '1 day', value: 1440},
|
||||
];
|
||||
|
||||
const changeOptions = [
|
||||
{ label: 'change', value: 'change' },
|
||||
{ label: '% change', value: 'percent' },
|
||||
{label: 'change', value: 'change'},
|
||||
{label: '% change', value: 'percent'},
|
||||
];
|
||||
|
||||
const Circle = ({ text }) => (
|
||||
const Circle = ({text}) => (
|
||||
<div className="circle mr-4 w-6 h-6 rounded-full bg-gray-light flex items-center justify-center">
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Section = ({ index, title, description, content }) => (
|
||||
const Section = ({index, title, description, content}) => (
|
||||
<div className="w-full">
|
||||
<div className="flex items-start">
|
||||
<Circle text={index} />
|
||||
<Circle text={index}/>
|
||||
<div>
|
||||
<span className="font-medium">{title}</span>
|
||||
{description && <div className="text-sm color-gray-medium">{description}</div>}
|
||||
|
|
@ -49,9 +50,9 @@ function AlertForm(props) {
|
|||
msTeamsChannels,
|
||||
webhooks,
|
||||
onDelete,
|
||||
style = { width: '580px', height: '100vh' },
|
||||
style = {height: "calc('100vh - 40px')"},
|
||||
} = props;
|
||||
const { alertsStore } = useStore()
|
||||
const {alertsStore} = useStore()
|
||||
const {
|
||||
triggerOptions,
|
||||
loading,
|
||||
|
|
@ -59,22 +60,22 @@ function AlertForm(props) {
|
|||
const instance = alertsStore.instance
|
||||
const deleting = loading
|
||||
|
||||
const write = ({ target: { value, name } }) => alertsStore.edit({ [name]: value });
|
||||
const writeOption = (e, { name, value }) => alertsStore.edit({ [name]: value.value });
|
||||
const onChangeCheck = ({ target: { checked, name } }) => alertsStore.edit({ [name]: checked });
|
||||
const write = ({target: {value, name}}) => alertsStore.edit({[name]: value});
|
||||
const writeOption = (e, {name, value}) => alertsStore.edit({[name]: value.value});
|
||||
const onChangeCheck = ({target: {checked, name}}) => alertsStore.edit({[name]: checked});
|
||||
|
||||
useEffect(() => {
|
||||
void alertsStore.fetchTriggerOptions();
|
||||
}, []);
|
||||
|
||||
const writeQueryOption = (e, { name, value }) => {
|
||||
const { query } = instance;
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
const writeQueryOption = (e, {name, value}) => {
|
||||
const {query} = instance;
|
||||
alertsStore.edit({query: {...query, [name]: value}});
|
||||
};
|
||||
|
||||
const writeQuery = ({ target: { value, name } }) => {
|
||||
const { query } = instance;
|
||||
alertsStore.edit({ query: { ...query, [name]: value } });
|
||||
const writeQuery = ({target: {value, name}}) => {
|
||||
const {query} = instance;
|
||||
alertsStore.edit({query: {...query, [name]: value}});
|
||||
};
|
||||
|
||||
const metric =
|
||||
|
|
@ -86,23 +87,23 @@ function AlertForm(props) {
|
|||
|
||||
return (
|
||||
<Form
|
||||
className={cn('p-6 pb-10', stl.wrapper)}
|
||||
className={cn('pb-10', stl.wrapper)}
|
||||
style={style}
|
||||
onSubmit={() => props.onSubmit(instance)}
|
||||
id="alert-form"
|
||||
>
|
||||
<div className={cn(stl.content, '-mx-6 px-6 pb-12')}>
|
||||
<div className={cn('-mx-6 px-6 pb-12')}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className="text-lg border border-gray-light rounded w-full"
|
||||
name="name"
|
||||
style={{ fontSize: '18px', padding: '10px', fontWeight: '600' }}
|
||||
style={{fontSize: '18px', padding: '10px', fontWeight: '600'}}
|
||||
value={instance && instance.name}
|
||||
onChange={write}
|
||||
placeholder="Untiltled Alert"
|
||||
id="name-field"
|
||||
/>
|
||||
<div className="mb-8" />
|
||||
<div className="mb-8"/>
|
||||
<Section
|
||||
index="1"
|
||||
title={'What kind of alert do you want to set?'}
|
||||
|
|
@ -112,11 +113,11 @@ function AlertForm(props) {
|
|||
primary
|
||||
name="detectionMethod"
|
||||
className="my-3"
|
||||
onSelect={(e, { name, value }) => alertsStore.edit({ [name]: value })}
|
||||
value={{ value: instance.detectionMethod }}
|
||||
onSelect={(e, {name, value}) => alertsStore.edit({[name]: value})}
|
||||
value={{value: instance.detectionMethod}}
|
||||
list={[
|
||||
{ name: 'Threshold', value: 'threshold' },
|
||||
{ name: 'Change', value: 'change' },
|
||||
{name: 'Threshold', value: 'threshold'},
|
||||
{name: 'Change', value: 'change'},
|
||||
]}
|
||||
/>
|
||||
<div className="text-sm color-gray-medium">
|
||||
|
|
@ -125,12 +126,12 @@ function AlertForm(props) {
|
|||
{!isThreshold &&
|
||||
'Eg. Alert me if % change of memory.avg is greater than 10% over the past 4 hours compared to the previous 4 hours.'}
|
||||
</div>
|
||||
<div className="my-4" />
|
||||
<div className="my-4"/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
<hr className="my-8"/>
|
||||
|
||||
<Section
|
||||
index="2"
|
||||
|
|
@ -146,7 +147,7 @@ function AlertForm(props) {
|
|||
options={changeOptions}
|
||||
name="change"
|
||||
defaultValue={instance.change}
|
||||
onChange={({ value }) => writeOption(null, { name: 'change', value })}
|
||||
onChange={({value}) => writeOption(null, {name: 'change', value})}
|
||||
id="change-dropdown"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -164,8 +165,8 @@ function AlertForm(props) {
|
|||
name="left"
|
||||
value={triggerOptions.find((i) => i.value === instance.query.left)}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({ value }) =>
|
||||
writeQueryOption(null, { name: 'left', value: value.value })
|
||||
onChange={({value}) =>
|
||||
writeQueryOption(null, {name: 'left', value: value.value})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -179,15 +180,15 @@ function AlertForm(props) {
|
|||
name="operator"
|
||||
defaultValue={instance.query.operator}
|
||||
// onChange={ writeQueryOption }
|
||||
onChange={({ value }) =>
|
||||
writeQueryOption(null, { name: 'operator', value: value.value })
|
||||
onChange={({value}) =>
|
||||
writeQueryOption(null, {name: 'operator', value: value.value})
|
||||
}
|
||||
/>
|
||||
{unit && (
|
||||
<>
|
||||
<Input
|
||||
className="px-4"
|
||||
style={{ marginRight: '31px' }}
|
||||
style={{marginRight: '31px'}}
|
||||
// label={{ basic: true, content: unit }}
|
||||
// labelPosition='right'
|
||||
name="right"
|
||||
|
|
@ -220,7 +221,7 @@ function AlertForm(props) {
|
|||
name="currentPeriod"
|
||||
defaultValue={instance.currentPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({ value }) => writeOption(null, { name: 'currentPeriod', value })}
|
||||
onChange={({value}) => writeOption(null, {name: 'currentPeriod', value})}
|
||||
/>
|
||||
</div>
|
||||
{!isThreshold && (
|
||||
|
|
@ -235,7 +236,7 @@ function AlertForm(props) {
|
|||
name="previousPeriod"
|
||||
defaultValue={instance.previousPeriod}
|
||||
// onChange={ writeOption }
|
||||
onChange={({ value }) => writeOption(null, { name: 'previousPeriod', value })}
|
||||
onChange={({value}) => writeOption(null, {name: 'previousPeriod', value})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -243,7 +244,7 @@ function AlertForm(props) {
|
|||
}
|
||||
/>
|
||||
|
||||
<hr className="my-8" />
|
||||
<hr className="my-8"/>
|
||||
|
||||
<Section
|
||||
index="3"
|
||||
|
|
@ -294,7 +295,7 @@ function AlertForm(props) {
|
|||
selected={instance.slackInput}
|
||||
options={slackChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({ slackInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({slackInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -308,7 +309,7 @@ function AlertForm(props) {
|
|||
selected={instance.msteamsInput}
|
||||
options={msTeamsChannels}
|
||||
placeholder="Select Channel"
|
||||
onChange={(selected) => alertsStore.edit({ msteamsInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({msteamsInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -323,7 +324,7 @@ function AlertForm(props) {
|
|||
validate={validateEmail}
|
||||
selected={instance.emailInput}
|
||||
placeholder="Type and press Enter key"
|
||||
onChange={(selected) => alertsStore.edit({ emailInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({emailInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -337,7 +338,7 @@ function AlertForm(props) {
|
|||
selected={instance.webhookInput}
|
||||
options={webhooks}
|
||||
placeholder="Select Webhook"
|
||||
onChange={(selected) => alertsStore.edit({ webhookInput: selected })}
|
||||
onChange={(selected) => alertsStore.edit({webhookInput: selected})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -346,31 +347,32 @@ function AlertForm(props) {
|
|||
/>
|
||||
</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">
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="primary"
|
||||
type="submit"
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={loading || !instance.validate()}
|
||||
id="submit-button"
|
||||
>
|
||||
{instance.exists() ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
<div className="mx-1" />
|
||||
<div className="mx-1"/>
|
||||
<Button onClick={props.onClose}>Cancel</Button>
|
||||
</div>
|
||||
<div>
|
||||
{instance.exists() && (
|
||||
<Button
|
||||
hover
|
||||
variant="text"
|
||||
primary="text"
|
||||
loading={deleting}
|
||||
type="button"
|
||||
onClick={() => onDelete(instance)}
|
||||
id="trash-button"
|
||||
>
|
||||
<Icon name="trash" color="gray-medium" size="18" />
|
||||
<Icon name="trash" color="gray-medium" size="18"/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { SlideModal } from 'UI';
|
||||
import { useStore } from 'App/mstore'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {SlideModal} from 'UI';
|
||||
import {useStore} from 'App/mstore'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import AlertForm from '../AlertForm';
|
||||
import { SLACK, TEAMS, WEBHOOK } from 'App/constants/schedule';
|
||||
import { confirm } from 'UI';
|
||||
import {SLACK, TEAMS, WEBHOOK} from 'App/constants/schedule';
|
||||
import {confirm} from 'UI';
|
||||
|
||||
interface Select {
|
||||
label: string;
|
||||
|
|
@ -17,9 +17,10 @@ interface Props {
|
|||
metricId?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
function AlertFormModal(props: Props) {
|
||||
const { alertsStore, settingsStore } = useStore()
|
||||
const { metricId = null, showModal = false } = props;
|
||||
const {alertsStore, settingsStore} = useStore()
|
||||
const {metricId = null, showModal = false} = props;
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const webhooks = settingsStore.webhooks
|
||||
useEffect(() => {
|
||||
|
|
@ -32,7 +33,7 @@ function AlertFormModal(props: Props) {
|
|||
const msTeamsChannels: Select[] = []
|
||||
|
||||
webhooks.forEach((hook) => {
|
||||
const option = { value: hook.webhookId, label: hook.name }
|
||||
const option = {value: hook.webhookId, label: hook.name}
|
||||
if (hook.type === SLACK) {
|
||||
slackChannels.push(option)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ function Recordings(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded py-4 border h-screen overflow-y-scroll'>
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className='bg-white rounded-lg py-4 border h-screen overflow-y-scroll'>
|
||||
<div className='flex items-center mb-4 justify-between px-6'>
|
||||
<div className='flex items-baseline mr-3'>
|
||||
<PageTitle title='Training Videos' />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ function AuditView() {
|
|||
}
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className="flex items-center mb-4 px-5 pt-5">
|
||||
<PageTitle title={
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ function CustomFields(props) {
|
|||
|
||||
const { fields, loading } = props;
|
||||
return (
|
||||
<div className="p-5 bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-5 ">
|
||||
<div className={cn(styles.tabHeader)}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Metadata'}</h3>
|
||||
<div style={{ marginRight: '15px' }}>
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ function Integrations(props: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className='mb-4 p-5 bg-white rounded-lg border'>
|
||||
<div className='bg-white rounded-lg border shadow-sm p-5 mb-4'>
|
||||
{!hideHeader && <PageTitle title={<div>Integrations</div>} />}
|
||||
|
||||
<IntegrationFilters onChange={onChange} activeItem={activeFilter} filters={filters} />
|
||||
|
|
@ -117,15 +117,7 @@ function Integrations(props: Props) {
|
|||
<div className='mb-4' />
|
||||
|
||||
<div className={cn(`
|
||||
grid
|
||||
gap-3
|
||||
auto-cols-max
|
||||
${allIntegrations.length > 0 ? 'p-2' : ''}
|
||||
grid-cols-1 // default to 1 column
|
||||
sm:grid-cols-1 // 1 column on small screens and up
|
||||
md:grid-cols-2 // 2 columns on medium screens and up
|
||||
lg:grid-cols-3 // 3 columns on large screens and up
|
||||
xl:grid-cols-3 // 3 columns on extra-large screens
|
||||
mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3
|
||||
`)}>
|
||||
{allIntegrations.map((integration: any) => (
|
||||
<IntegrationItem
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function Modules(props: Props) {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className='bg-white rounded-lg border p-4'>
|
||||
<div className='bg-white rounded-lg border shadow-sm p-4'>
|
||||
<h3 className='text-2xl'>Modules</h3>
|
||||
<ul className='mt-3 ml-4 list-disc'>
|
||||
<li>OpenReplay's modules are a collection of advanced features that provide enhanced functionality.</li>
|
||||
|
|
@ -54,7 +54,7 @@ function Modules(props: Props) {
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 grid grid-cols-3 gap-3'>
|
||||
<div className='mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3'>
|
||||
{modulesState.map((module) => (
|
||||
<div key={module.key} className='flex flex-col h-full'>
|
||||
<ModuleCard module={module} onToggle={onToggle} />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export default class ProfileSettings extends React.PureComponent {
|
|||
render() {
|
||||
const { account, isEnterprise } = this.props;
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-5">
|
||||
<div className="bg-white rounded-lg border shadow-sm p-5">
|
||||
<PageTitle title={<div>Account</div>} />
|
||||
<div className="flex items-center">
|
||||
<div className={styles.left}>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function Roles(props: Props) {
|
|||
return (
|
||||
<React.Fragment>
|
||||
<Loader loading={loading}>
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className={cn(stl.tabHeader, 'flex items-center')}>
|
||||
<div className="flex items-center mr-auto px-5 pt-5">
|
||||
<h3 className={cn(stl.tabTitle, 'text-2xl')}>Roles and Access</h3>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const connector = connect(mapStateToProps);
|
|||
|
||||
function SessionsListingSettings(props: Props) {
|
||||
return (
|
||||
<div className='bg-white rounded-lg p-5'>
|
||||
<div className='bg-white rounded-lg border shadow-sm p-5'>
|
||||
<PageTitle title={<div>Sessions Listing</div>} />
|
||||
|
||||
<div className='flex flex-col mt-4'>
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
|
|||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className={cn(stl.tabHeader, 'px-5 pt-5')}>
|
||||
<PageTitle
|
||||
title={<div className="mr-4">Projects</div>}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function UsersView(props: Props) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border">
|
||||
<div className="flex items-center justify-between px-5 pt-5">
|
||||
<PageTitle
|
||||
title={
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ function Webhooks() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="p-5 bg-white rounded-lg">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-5">
|
||||
<div className={cn(styles.tabHeader)}>
|
||||
<h3 className={cn(styles.tabTitle, 'text-2xl')}>{'Webhooks'}</h3>
|
||||
<Button className="ml-auto" variant="primary" onClick={() => init()}>Add Webhook</Button>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './CustomMetriLineChart';
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts';
|
||||
import { LineChart, Line, Legend } from 'recharts';
|
||||
import {Styles} from '../../common';
|
||||
import {ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip} from 'recharts';
|
||||
import {LineChart, Line, Legend} from 'recharts';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
|
|
@ -9,46 +9,56 @@ interface Props {
|
|||
// seriesMap: any;
|
||||
colors: any;
|
||||
onClick?: (event, index) => void;
|
||||
yaxis?: any;
|
||||
label?: string;
|
||||
}
|
||||
function CustomMetriLineChart(props: Props) {
|
||||
const { data = { chart: [], namesMap: [] }, params, colors, onClick = () => null } = props;
|
||||
|
||||
function CustomMetricLineChart(props: Props) {
|
||||
const {
|
||||
data = {chart: [], namesMap: []},
|
||||
params,
|
||||
colors,
|
||||
onClick = () => null,
|
||||
yaxis = {...Styles.yaxis},
|
||||
label = 'Number of Sessions'
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ResponsiveContainer height={ 240 } width="100%">
|
||||
<ResponsiveContainer height={240} width="100%">
|
||||
<LineChart
|
||||
data={ data.chart }
|
||||
data={data.chart}
|
||||
margin={Styles.chartMargins}
|
||||
// syncId={ showSync ? "domainsErrors_4xx" : undefined }
|
||||
onClick={onClick}
|
||||
// isAnimationActive={ false }
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={ false } stroke="#EEEEEE" />
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
|
||||
<XAxis
|
||||
{...Styles.xaxis}
|
||||
dataKey="time"
|
||||
interval={params.density/7}
|
||||
interval={params.density / 7}
|
||||
/>
|
||||
<YAxis
|
||||
{...Styles.yaxis}
|
||||
{...yaxis}
|
||||
allowDecimals={false}
|
||||
tickFormatter={val => Styles.tickFormatter(val)}
|
||||
label={{
|
||||
...Styles.axisLabelLeft,
|
||||
value: "Number of Sessions"
|
||||
value: label || "Number of Sessions"
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Legend/>
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
{ Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
|
||||
{Array.isArray(data.namesMap) && data.namesMap.map((key, index) => (
|
||||
<Line
|
||||
key={key}
|
||||
name={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={colors[index]}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.6 }
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={0.6}
|
||||
// fill="url(#colorCount)"
|
||||
dot={false}
|
||||
/>
|
||||
|
|
@ -58,4 +68,4 @@ function CustomMetriLineChart(props: Props) {
|
|||
)
|
||||
}
|
||||
|
||||
export default CustomMetriLineChart
|
||||
export default CustomMetricLineChart
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomMetricLineChart';
|
||||
|
|
@ -1,51 +1,52 @@
|
|||
import React from 'react'
|
||||
import { Styles } from '../../common';
|
||||
import { AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip } from 'recharts';
|
||||
import {Styles} from '../../common';
|
||||
import {AreaChart, ResponsiveContainer, XAxis, YAxis, Area, Tooltip} from 'recharts';
|
||||
import CountBadge from '../../common/CountBadge';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import {numberWithCommas} from 'App/utils';
|
||||
|
||||
interface Props {
|
||||
data: any;
|
||||
}
|
||||
|
||||
function CustomMetricOverviewChart(props: Props) {
|
||||
const { data } = props;
|
||||
const {data} = props;
|
||||
const gradientDef = Styles.gradientDef();
|
||||
|
||||
return (
|
||||
<div className="relative -mx-4">
|
||||
<div className="absolute flex items-start flex-col justify-start inset-0 p-3">
|
||||
<div className="mb-2 flex items-center" >
|
||||
<div className="mb-2 flex items-center">
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<CountBadge
|
||||
// title={subtext}
|
||||
count={ countView(Math.round(data.value), data.unit) }
|
||||
change={ data.progress || 0 }
|
||||
unit={ data.unit }
|
||||
count={countView(Math.round(data.value), data.unit)}
|
||||
change={data.progress || 0}
|
||||
unit={data.unit}
|
||||
// className={textClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ResponsiveContainer height={ 100 } width="100%">
|
||||
<ResponsiveContainer height={100} width="100%">
|
||||
<AreaChart
|
||||
data={ data.chart }
|
||||
margin={ {
|
||||
data={data.chart}
|
||||
margin={{
|
||||
top: 50, right: 0, left: 0, bottom: 0,
|
||||
} }
|
||||
}}
|
||||
>
|
||||
{gradientDef}
|
||||
<Tooltip {...Styles.tooltip} />
|
||||
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time" />
|
||||
<YAxis hide interval={ 0 } />
|
||||
<XAxis hide {...Styles.xaxis} interval={4} dataKey="time"/>
|
||||
<YAxis hide interval={0}/>
|
||||
<Area
|
||||
name={''}
|
||||
// unit={unit && ' ' + unit}
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={Styles.strokeColor}
|
||||
fillOpacity={ 1 }
|
||||
strokeWidth={ 2 }
|
||||
strokeOpacity={ 0.8 }
|
||||
fillOpacity={1}
|
||||
strokeWidth={2}
|
||||
strokeOpacity={0.8}
|
||||
fill={'url(#colorCount)'}
|
||||
/>
|
||||
</AreaChart>
|
||||
|
|
@ -66,7 +67,7 @@ const countView = (avg: any, unit: any) => {
|
|||
if (unit === 'min') {
|
||||
if (!avg) return 0;
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react'
|
||||
import { Table } from '../../common';
|
||||
import { List } from 'immutable';
|
||||
import { filtersMap } from 'Types/filter/newFilter';
|
||||
import { NoContent, Icon } from 'UI';
|
||||
import { tableColumnName } from 'App/constants/filterOptions';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import {Table} from '../../common';
|
||||
import {List} from 'immutable';
|
||||
import {filtersMap} from 'Types/filter/newFilter';
|
||||
import {NoContent, Icon} from 'UI';
|
||||
import {tableColumnName} from 'App/constants/filterOptions';
|
||||
import {numberWithCommas} from 'App/utils';
|
||||
|
||||
const getColumns = (metric) => {
|
||||
return [
|
||||
|
|
@ -13,6 +13,7 @@ const getColumns = (metric) => {
|
|||
title: tableColumnName[metric.metricOf],
|
||||
toText: name => name || 'Unidentified',
|
||||
width: '70%',
|
||||
icon: true,
|
||||
},
|
||||
{
|
||||
key: 'sessionCount',
|
||||
|
|
@ -29,13 +30,14 @@ interface Props {
|
|||
onClick?: (filters: any) => void;
|
||||
isTemplate?: boolean;
|
||||
}
|
||||
|
||||
function CustomMetricTable(props: Props) {
|
||||
const { metric = {}, data = { values: [] }, onClick = () => null, isTemplate } = props;
|
||||
const {metric = {}, data = {values: []}, onClick = () => null, isTemplate} = props;
|
||||
const rows = List(data.values);
|
||||
|
||||
const onClickHandler = (event: any, data: any) => {
|
||||
const filters = Array<any>();
|
||||
let filter = { ...filtersMap[metric.metricOf] }
|
||||
let filter = {...filtersMap[metric.metricOf]}
|
||||
filter.value = [data.name]
|
||||
filter.type = filter.key
|
||||
delete filter.key
|
||||
|
|
@ -49,14 +51,14 @@ function CustomMetricTable(props: Props) {
|
|||
onClick(filters);
|
||||
}
|
||||
return (
|
||||
<div className="" style={{ height: 240 }}>
|
||||
<div className="" style={{height: 240}}>
|
||||
<NoContent
|
||||
style={{ minHeight: 220 }}
|
||||
style={{minHeight: 220}}
|
||||
show={data.values && data.values.length === 0}
|
||||
size="small"
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<Icon name="info-circle" className="mr-2" size="18" />
|
||||
<Icon name="info-circle" className="mr-2" size="18"/>
|
||||
No data for the selected time period
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -46,30 +46,28 @@ const cols = [
|
|||
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
isTemplate?: boolean
|
||||
}
|
||||
function CallWithErrors(props: Props) {
|
||||
const { data, metric } = props;
|
||||
const { data } = props;
|
||||
const [search, setSearch] = React.useState('')
|
||||
const test = (value = '', serach: any) => getRE(serach, 'i').test(value);
|
||||
const _data = search ? metric.data.chart.filter((i: any) => test(i.urlHostpath, search)) : metric.data.chart;
|
||||
const _data = search ? data.chart.filter((i: any) => test(i.urlHostpath, search)) : data.chart;
|
||||
|
||||
const write = ({ target: { name, value } }: any) => {
|
||||
setSearch(value)
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<NoContent
|
||||
size="small"
|
||||
title={NO_METRIC_DATA}
|
||||
show={ metric.data.chart.length === 0 }
|
||||
show={ data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
>
|
||||
<div style={{ height: '240px'}}>
|
||||
<div className={ cn(stl.topActions, 'py-3 flex text-right')}>
|
||||
<input disabled={metric.data.chart.length === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={write} />
|
||||
<input disabled={data.chart.length === 0} className={stl.searchField} name="search" placeholder="Filter by Path" onChange={write} />
|
||||
</div>
|
||||
<Table
|
||||
small
|
||||
|
|
|
|||
|
|
@ -7,21 +7,20 @@ import { NO_METRIC_DATA } from 'App/constants/messages'
|
|||
|
||||
interface Props {
|
||||
data: any
|
||||
metric?: any
|
||||
}
|
||||
function ErrorsPerDomain(props: Props) {
|
||||
const { data, metric } = props;
|
||||
const { data } = props;
|
||||
// const firstAvg = 10;
|
||||
const firstAvg = metric.data.chart[0] && metric.data.chart[0].errorsCount;
|
||||
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
|
||||
return (
|
||||
<NoContent
|
||||
size="small"
|
||||
show={ metric.data.chart.length === 0 }
|
||||
show={ data.chart.length === 0 }
|
||||
style={{ height: '240px'}}
|
||||
title={NO_METRIC_DATA}
|
||||
>
|
||||
<div className="w-full" style={{ height: '240px' }}>
|
||||
{metric.data.chart.map((item, i) =>
|
||||
{data.chart.map((item, i) =>
|
||||
<Bar
|
||||
key={i}
|
||||
className="mb-2"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import {numberWithCommas} from 'App/utils';
|
||||
|
||||
const colorsTeal = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1'];
|
||||
const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72'];
|
||||
const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
|
||||
|
|
@ -23,31 +24,31 @@ export default {
|
|||
compareColorsx,
|
||||
lineColor: '#2A7B7F',
|
||||
lineColorCompare: '#394EFF',
|
||||
strokeColor: colors[2],
|
||||
strokeColor: compareColors[2],
|
||||
xaxis: {
|
||||
axisLine: { stroke: '#CCCCCC' },
|
||||
axisLine: {stroke: '#CCCCCC'},
|
||||
interval: 0,
|
||||
dataKey: "time",
|
||||
tick: { fill: '#999999', fontSize: 9 },
|
||||
tickLine: { stroke: '#CCCCCC' },
|
||||
tick: {fill: '#999999', fontSize: 9},
|
||||
tickLine: {stroke: '#CCCCCC'},
|
||||
strokeWidth: 0.5
|
||||
},
|
||||
yaxis: {
|
||||
axisLine: { stroke: '#CCCCCC' },
|
||||
tick: { fill: '#999999', fontSize: 9 },
|
||||
tickLine: { stroke: '#CCCCCC' },
|
||||
axisLine: {stroke: '#CCCCCC'},
|
||||
tick: {fill: '#999999', fontSize: 9},
|
||||
tickLine: {stroke: '#CCCCCC'},
|
||||
},
|
||||
axisLabelLeft: {
|
||||
angle: -90,
|
||||
fill: '#999999',
|
||||
offset: 10,
|
||||
style: { textAnchor: 'middle' },
|
||||
style: {textAnchor: 'middle'},
|
||||
position: 'insideLeft',
|
||||
fontSize: 11
|
||||
},
|
||||
tickFormatter: val => `${countView(val)}`,
|
||||
tickFormatterBytes: val => Math.round(val / 1024 / 1024),
|
||||
chartMargins: { left: 0, right: 20, top: 10, bottom: 5 },
|
||||
chartMargins: {left: 0, right: 20, top: 10, bottom: 5},
|
||||
tooltip: {
|
||||
cursor: {
|
||||
fill: '#f6f6f6'
|
||||
|
|
@ -62,7 +63,7 @@ export default {
|
|||
fontSize: '10px'
|
||||
},
|
||||
labelStyle: {},
|
||||
formatter: (value, name, { unit }) => {
|
||||
formatter: (value, name, {unit}) => {
|
||||
if (unit && unit.trim() === 'mb') {
|
||||
return numberWithCommas(Math.round(value / 1024 / 1024))
|
||||
}
|
||||
|
|
@ -77,12 +78,12 @@ export default {
|
|||
gradientDef: () => (
|
||||
<defs>
|
||||
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors[2]} stopOpacity={ 0.5 } />
|
||||
<stop offset="95%" stopColor={colors[2]} stopOpacity={ 0.2 } />
|
||||
<stop offset="5%" stopColor={compareColors[2]} stopOpacity={0.5}/>
|
||||
<stop offset="95%" stopColor={compareColors[2]} stopOpacity={0.2}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorCountCompare" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={compareColors[4]} stopOpacity={ 0.9 } />
|
||||
<stop offset="95%" stopColor={compareColors[4]} stopOpacity={ 0.2 } />
|
||||
<stop offset="5%" stopColor={compareColors[4]} stopOpacity={0.9}/>
|
||||
<stop offset="95%" stopColor={compareColors[4]} stopOpacity={0.2}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -26,7 +26,7 @@ function AlertsView({ siteId }: IAlertsView) {
|
|||
return unmount;
|
||||
}, [history]);
|
||||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded py-4 border">
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded-lg shadow-sm py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Alerts" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -57,7 +57,7 @@ function DashboardEditModal(props: Props) {
|
|||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
{/* <Form.Field>
|
||||
<label>{'Description:'}</label>
|
||||
<Input
|
||||
className=""
|
||||
|
|
@ -69,7 +69,7 @@ function DashboardEditModal(props: Props) {
|
|||
maxLength={300}
|
||||
autoFocus={!focusTitle}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Field> */}
|
||||
|
||||
<Form.Field>
|
||||
<div className="flex items-center">
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
import React from 'react';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { Button, PageTitle, confirm, Tooltip } from 'UI';
|
||||
import {withSiteId} from 'App/routes';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {Button, PageTitle, confirm, Tooltip} from 'UI';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {useModal} from 'App/components/Modal';
|
||||
import DashboardOptions from '../DashboardOptions';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import AddCardModal from '../AddCardModal';
|
||||
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
|
||||
import CreateCard from "Components/Dashboard/components/DashboardList/NewDashModal/CreateCard";
|
||||
import CreateCardButton from "Components/Dashboard/components/CreateCardButton";
|
||||
|
||||
interface IProps {
|
||||
dashboardId: string;
|
||||
|
|
@ -18,12 +20,14 @@ interface IProps {
|
|||
renderReport?: any;
|
||||
}
|
||||
|
||||
|
||||
type Props = IProps & RouteComponentProps;
|
||||
const MAX_CARDS = 29;
|
||||
|
||||
function DashboardHeader(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { showModal } = useModal();
|
||||
const {siteId, dashboardId} = props;
|
||||
const {dashboardStore} = useStore();
|
||||
const {showModal} = useModal();
|
||||
const [focusTitle, setFocusedInput] = React.useState(true);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
const period = dashboardStore.period;
|
||||
|
|
@ -40,7 +44,7 @@ function DashboardHeader(props: Props) {
|
|||
const onDelete = async () => {
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Confirm',
|
||||
header: 'Delete Dashboard',
|
||||
confirmButton: 'Yes, delete',
|
||||
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
|
||||
})
|
||||
|
|
@ -63,11 +67,11 @@ function DashboardHeader(props: Props) {
|
|||
label: 'Dashboards',
|
||||
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" style={{ flex: 3 }}>
|
||||
<div className="flex items-center" style={{flex: 3}}>
|
||||
<PageTitle
|
||||
title={
|
||||
// @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"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
|
||||
<Tooltip delay={0} disabled={canAddMore} title="The number of cards in one dashboard is limited to 30.">
|
||||
<Button
|
||||
disabled={!canAddMore}
|
||||
variant="primary"
|
||||
onClick={() =>
|
||||
showModal(<AddCardModal dashboardId={dashboardId} siteId={siteId} />, { right: true })
|
||||
}
|
||||
icon="plus"
|
||||
iconSize={24}
|
||||
>
|
||||
Add Card
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div className="mx-4"></div>
|
||||
<div className="flex items-center gap-2" style={{flex: 1, justifyContent: 'end'}}>
|
||||
<CreateCardButton disabled={canAddMore} />
|
||||
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 justify-end"
|
||||
style={{ width: 'fit-content' }}
|
||||
className="flex items-center flex-shrink-0 justify-end dashboardDataPeriodSelector"
|
||||
style={{width: 'fit-content'}}
|
||||
>
|
||||
<SelectDateRange
|
||||
style={{ width: '300px' }}
|
||||
style={{width: '300px'}}
|
||||
period={period}
|
||||
onChange={(period: any) => dashboardStore.setPeriod(period)}
|
||||
right={true}
|
||||
isAnt={true}
|
||||
useButtonStyle={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<DashboardOptions
|
||||
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"
|
||||
onDoubleClick={() => onEdit(false)}
|
||||
>
|
||||
{dashboard?.description || 'Describe the purpose of this dashboard'}
|
||||
{/* {dashboard?.description || 'Describe the purpose of this dashboard'} */}
|
||||
</h2>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,67 +1,122 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import {LockOutlined, TeamOutlined} from '@ant-design/icons';
|
||||
import {Empty, Switch, Table, TableColumnsType, Tag, Tooltip, Typography} from 'antd';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { NoContent, Pagination } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import DashboardListItem from './DashboardListItem';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import { Tooltip } from 'antd';
|
||||
import {connect} from 'react-redux';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
|
||||
function DashboardList() {
|
||||
const { dashboardStore } = useStore();
|
||||
import {checkForRecent} from 'App/date';
|
||||
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 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 (
|
||||
<NoContent
|
||||
show={lenth === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} />
|
||||
<div className="text-center mt-4">
|
||||
{dashboardsSearch !== '' ? 'No matching results' : "You haven't created any dashboards yet"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
subtext={
|
||||
list.length === 0 && !dashboardStore.filter.showMine ? (
|
||||
<Empty
|
||||
image={<AnimatedSVG name={dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS} size={600}/>}
|
||||
|
||||
imageStyle={{height: 300}}
|
||||
description={(
|
||||
<div className="text-center">
|
||||
<div>
|
||||
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 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>
|
||||
|
||||
{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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
import { Input } from 'antd';
|
||||
|
||||
let debounceUpdate: any = () => {};
|
||||
|
||||
|
|
@ -24,16 +24,15 @@ function DashboardSearch() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
<Input.Search
|
||||
value={query}
|
||||
allowClear
|
||||
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"
|
||||
onChange={write}
|
||||
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import withPageTitle from 'HOCs/withPageTitle';
|
|||
import DashboardList from './DashboardList';
|
||||
import Header from './Header';
|
||||
|
||||
function DashboardsView({ history, siteId }: { history: any; siteId: string }) {
|
||||
function DashboardsView({history, siteId}: { history: any; siteId: string }) {
|
||||
return (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded py-4 border">
|
||||
<Header history={history} siteId={siteId} />
|
||||
<DashboardList />
|
||||
<div style={{maxWidth: '1360px', margin: 'auto'}} className="bg-white rounded-lg py-4 border shadow-sm">
|
||||
<Header />
|
||||
<DashboardList/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,65 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Toggler, Icon } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
import {PageTitle} from 'UI';
|
||||
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
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));
|
||||
});
|
||||
};
|
||||
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
|
||||
|
||||
function Header() {
|
||||
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">
|
||||
<PageTitle title="Dashboards" />
|
||||
<PageTitle title="Dashboards"/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>
|
||||
New Dashboard
|
||||
</Button>
|
||||
<CreateDashboardButton/>
|
||||
<div className="mx-2"></div>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
<div className="w-1/4" style={{minWidth: 300}}>
|
||||
<DashboardSearch/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-y px-3 py-1 mt-2 flex items-center w-full justify-end gap-4">
|
||||
<Toggler
|
||||
label="Private Dashboards"
|
||||
checked={dashboardStore.filter.showMine}
|
||||
name="test"
|
||||
className="font-medium mr-2"
|
||||
onChange={() =>
|
||||
dashboardStore.updateKey('filter', {
|
||||
...dashboardStore.filter,
|
||||
showMine: !dashboardStore.filter.showMine,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Header);
|
||||
export default Header;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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),
|
||||
}));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export {default} from './NewDashboardModal';
|
||||
|
|
@ -14,7 +14,7 @@ function DashboardOptions(props: Props) {
|
|||
const { editHandler, deleteHandler, renderReport, isEnterprise, isTitlePresent } = props;
|
||||
const menuItems = [
|
||||
{ icon: 'pencil', text: 'Rename', onClick: () => editHandler(true) },
|
||||
{ icon: 'text-paragraph', text: `${!isTitlePresent ? 'Add' : 'Edit'} Description`, onClick: () => editHandler(false) },
|
||||
// { icon: 'text-paragraph', text: `${!isTitlePresent ? 'Add' : 'Edit'} Description`, onClick: () => editHandler(false) },
|
||||
{ icon: 'users', text: 'Visibility & Access', onClick: editHandler },
|
||||
{ icon: 'trash', text: 'Delete', onClick: deleteHandler },
|
||||
{ icon: 'pdf-download', text: 'Download Report', onClick: renderReport, disabled: !isEnterprise, tooltipTitle: ENTERPRISE_REQUEIRED }
|
||||
|
|
@ -23,7 +23,6 @@ function DashboardOptions(props: Props) {
|
|||
return (
|
||||
<ItemMenu
|
||||
bold
|
||||
label="More Options"
|
||||
items={menuItems}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { useObserver } from 'mobx-react-lite';
|
||||
import {useObserver} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { Button, Modal, Form, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore'
|
||||
import {Button, Modal, Form, Icon} from 'UI';
|
||||
|
||||
import {useStore} from 'App/mstore'
|
||||
import Select from 'Shared/Select';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -9,9 +10,10 @@ interface Props {
|
|||
show: boolean;
|
||||
closeHandler?: () => void;
|
||||
}
|
||||
|
||||
function DashboardSelectionModal(props: Props) {
|
||||
const { show, metricId, closeHandler } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const {show, metricId, closeHandler} = props;
|
||||
const {dashboardStore} = useStore();
|
||||
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
|
||||
key: i.id,
|
||||
label: i.name,
|
||||
|
|
@ -41,16 +43,16 @@ function DashboardSelectionModal(props: Props) {
|
|||
}, [])
|
||||
|
||||
return useObserver(() => (
|
||||
<Modal size="small" open={ show } onClose={closeHandler}>
|
||||
<Modal size="small" open={show} onClose={closeHandler}>
|
||||
<Modal.Header className="flex items-center justify-between">
|
||||
<div>{ 'Add to selected dashboard' }</div>
|
||||
<div className='text-xl font-medium'>{'Add to selected dashboard'}</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={ closeHandler }
|
||||
onClick={closeHandler}
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
|
|
@ -60,19 +62,19 @@ function DashboardSelectionModal(props: Props) {
|
|||
<Select
|
||||
options={dashboardOptions}
|
||||
defaultValue={dashboardOptions[0].value}
|
||||
onChange={({ value }: any) => setSelectedId(value.value)}
|
||||
onChange={({value}: any) => setSelectedId(value.value)}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Modal.Content>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={ onSave }
|
||||
className="float-left mr-2"
|
||||
onClick={onSave}
|
||||
className="float-left mr-2 "
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button>
|
||||
<Button className="mr-2" onClick={closeHandler}>{'Cancel'}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Loader } from 'UI';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import React, {useEffect} from 'react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {Loader} from 'UI';
|
||||
import {withSiteId} from 'App/routes';
|
||||
import withModal from 'App/components/Modal/withModal';
|
||||
import DashboardWidgetGrid from '../DashboardWidgetGrid';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import {withRouter, RouteComponentProps} from 'react-router-dom';
|
||||
import {useModal} from 'App/components/Modal';
|
||||
import DashboardModal from '../DashboardModal';
|
||||
import AlertFormModal from 'App/components/Alerts/AlertFormModal';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withReport from 'App/components/hocs/withReport';
|
||||
import DashboardHeader from '../DashboardHeader';
|
||||
import {useHistory} from "react-router";
|
||||
|
||||
interface IProps {
|
||||
siteId: string;
|
||||
|
|
@ -22,20 +23,21 @@ interface IProps {
|
|||
type Props = IProps & RouteComponentProps;
|
||||
|
||||
function DashboardView(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { showModal, hideModal } = useModal();
|
||||
const {siteId, dashboardId} = props;
|
||||
const {dashboardStore} = useStore();
|
||||
const {showModal, hideModal} = useModal();
|
||||
const history = useHistory();
|
||||
|
||||
const showAlertModal = dashboardStore.showAlertModal;
|
||||
const loading = dashboardStore.fetchingDashboard;
|
||||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
|
||||
const queryParams = new URLSearchParams(props.location.search);
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
const trimQuery = () => {
|
||||
if (!queryParams.has('modal')) return;
|
||||
queryParams.delete('modal');
|
||||
props.history.replace({
|
||||
history.replace({
|
||||
search: queryParams.toString(),
|
||||
});
|
||||
};
|
||||
|
|
@ -50,14 +52,14 @@ function DashboardView(props: Props) {
|
|||
dashboardStore.toggleAlertModal(false)
|
||||
}}
|
||||
/>,
|
||||
{ right: false, width: 580 },
|
||||
{right: false, width: 580},
|
||||
() => dashboardStore.toggleAlertModal(false)
|
||||
)
|
||||
}
|
||||
}, [showAlertModal])
|
||||
|
||||
const pushQuery = () => {
|
||||
if (!queryParams.has('modal')) props.history.push('?modal=addMetric');
|
||||
if (!queryParams.has('modal')) history.push('?modal=addMetric');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -70,7 +72,7 @@ function DashboardView(props: Props) {
|
|||
useEffect(() => {
|
||||
const isExists = dashboardStore.getDashboardById(dashboardId);
|
||||
if (!isExists) {
|
||||
props.history.push(withSiteId(`/dashboard`, siteId));
|
||||
history.push(withSiteId(`/dashboard`, siteId));
|
||||
}
|
||||
}, [dashboardId]);
|
||||
|
||||
|
|
@ -82,8 +84,8 @@ function DashboardView(props: Props) {
|
|||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
showModal(
|
||||
<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId} />,
|
||||
{ right: true }
|
||||
<DashboardModal siteId={siteId} onMetricAdd={pushQuery} dashboardId={dashboardId}/>,
|
||||
{right: true}
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -91,9 +93,9 @@ function DashboardView(props: Props) {
|
|||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }}>
|
||||
<div style={{maxWidth: '1360px', margin: 'auto'}}>
|
||||
{/* @ts-ignore */}
|
||||
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId} />
|
||||
<DashboardHeader renderReport={props.renderReport} siteId={siteId} dashboardId={dashboardId}/>
|
||||
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
|
|
@ -105,7 +107,8 @@ function DashboardView(props: Props) {
|
|||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export default withPageTitle('Dashboards - OpenReplay')(
|
||||
withReport(withRouter(withModal(observer(DashboardView))))
|
||||
withReport(withModal(observer(DashboardView)))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import {useStore} from 'App/mstore';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Loader, Icon } from 'UI';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import {NoContent, Loader, Icon} from 'UI';
|
||||
import {useObserver} from 'mobx-react-lite';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import MetricTypeList from '../MetricTypeList';
|
||||
import WidgetWrapperNew from "Components/Dashboard/components/WidgetWrapper/WidgetWrapperNew";
|
||||
import {Empty} from "antd";
|
||||
|
||||
interface Props {
|
||||
siteId: string;
|
||||
|
|
@ -12,16 +14,17 @@ interface Props {
|
|||
onEditHandler: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
function DashboardWidgetGrid(props: Props) {
|
||||
const { dashboardId, siteId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const {dashboardId, siteId} = props;
|
||||
const {dashboardStore} = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const list = useObserver(() => dashboard?.widgets);
|
||||
const smallWidgets: Widget[] = [];
|
||||
const regularWidgets: Widget[] = [];
|
||||
|
||||
list.forEach((item) => {
|
||||
list?.forEach((item) => {
|
||||
if (item.config.col === 1) {
|
||||
smallWidgets.push(item);
|
||||
} else {
|
||||
|
|
@ -33,12 +36,13 @@ function DashboardWidgetGrid(props: Props) {
|
|||
|
||||
return useObserver(() => (
|
||||
// @ts-ignore
|
||||
list?.length === 0 ? <Empty description="No cards in this dashboard"/> : (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
show={list?.length === 0}
|
||||
icon="no-metrics-chart"
|
||||
title={
|
||||
<div className="bg-white rounded">
|
||||
<div className="bg-white rounded-lg">
|
||||
<div className="border-b p-5">
|
||||
<div className="text-2xl font-normal">
|
||||
There are no cards in this dashboard
|
||||
|
|
@ -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.
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 p-8 gap-2">
|
||||
<MetricTypeList dashboardId={parseInt(dashboardId)} siteId={siteId} />
|
||||
</div>
|
||||
{/*<div className="grid grid-cols-4 p-8 gap-2">*/}
|
||||
{/* <MetricTypeList dashboardId={parseInt(dashboardId)} siteId={siteId}/>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>{smallWidgets.length > 0 ? (
|
||||
<>
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
<Icon name="grid-horizontal" size={26}/>
|
||||
Web Vitals
|
||||
</div>
|
||||
|
||||
{smallWidgets &&
|
||||
smallWidgets.map((item: any, index: any) => (
|
||||
<React.Fragment key={item.widgetId}>
|
||||
<WidgetWrapper
|
||||
<WidgetWrapperNew
|
||||
index={index}
|
||||
widget={item}
|
||||
moveListItem={(dragIndex: any, hoverIndex: any) =>
|
||||
dashboard.swapWidgetPosition(dragIndex, hoverIndex)
|
||||
dashboard?.swapWidgetPosition(dragIndex, hoverIndex)
|
||||
|
||||
}dashboardId={dashboardId}
|
||||
} dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
isWidget={true}
|
||||
grid="vitals"
|
||||
|
|
@ -82,7 +86,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
|
||||
{smallWidgets.length > 0 && regularWidgets.length > 0 ? (
|
||||
<div className="font-semibold text-xl py-4 flex items-center gap-2 col-span-4">
|
||||
<Icon name="grid-horizontal" size={26} />
|
||||
<Icon name="grid-horizontal" size={26}/>
|
||||
All Cards
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -90,11 +94,11 @@ function DashboardWidgetGrid(props: Props) {
|
|||
{regularWidgets &&
|
||||
regularWidgets.map((item: any, index: any) => (
|
||||
<React.Fragment key={item.widgetId}>
|
||||
<WidgetWrapper
|
||||
<WidgetWrapperNew
|
||||
index={smallWidgetsLen + index}
|
||||
widget={item}
|
||||
moveListItem={(dragIndex: any, hoverIndex: any) =>
|
||||
dashboard.swapWidgetPosition(dragIndex, hoverIndex)
|
||||
dashboard?.swapWidgetPosition(dragIndex, hoverIndex)
|
||||
}
|
||||
dashboardId={dashboardId}
|
||||
siteId={siteId}
|
||||
|
|
@ -106,6 +110,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
</div>
|
||||
</NoContent>
|
||||
</Loader>
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,11 +1,69 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, {useState} from 'react';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import { Button, Icon } from 'UI';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import SeriesName from './SeriesName';
|
||||
import cn from 'classnames';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import ExcludeFilters from './ExcludeFilters';
|
||||
import AddStepButton from "Components/Dashboard/components/FilterSeries/AddStepButton";
|
||||
import {Button, Space} from "antd";
|
||||
import {ChevronDown, ChevronUp, Trash} from "lucide-react";
|
||||
|
||||
|
||||
const FilterCountLabels = observer((props: { filters: any, toggleExpand: any }) => {
|
||||
const events = props.filters.filter((i: any) => i && i.isEvent).length;
|
||||
const filters = props.filters.filter((i: any) => i && !i.isEvent).length;
|
||||
return <div className="flex items-center">
|
||||
<Space>
|
||||
{events > 0 && (
|
||||
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
|
||||
{`${events} Event${events > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{filters > 0 && (
|
||||
<Button type="primary" ghost size="small" onClick={props.toggleExpand}>
|
||||
{`${filters} Filter${filters > 1 ? 's' : ''}`}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>;
|
||||
});
|
||||
|
||||
const FilterSeriesHeader = observer((props: {
|
||||
expanded: boolean,
|
||||
hidden: boolean,
|
||||
seriesIndex: number,
|
||||
series: any,
|
||||
onRemove: (seriesIndex: any) => void,
|
||||
canDelete: boolean | undefined,
|
||||
toggleExpand: () => void
|
||||
}) => {
|
||||
|
||||
const onUpdate = (name: any) => {
|
||||
props.series.update('name', name)
|
||||
}
|
||||
return <div className={cn("border-b px-5 h-12 flex items-center relative", {hidden: props.hidden})}>
|
||||
<Space className="mr-auto" size={30}>
|
||||
<SeriesName
|
||||
seriesIndex={props.seriesIndex}
|
||||
name={props.series.name}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
{!props.expanded &&
|
||||
<FilterCountLabels filters={props.series.filter.filters} toggleExpand={props.toggleExpand}/>}
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Button onClick={props.onRemove}
|
||||
size="small"
|
||||
disabled={!props.canDelete}
|
||||
icon={<Trash size={14}/>}/>
|
||||
<Button onClick={props.toggleExpand}
|
||||
size="small"
|
||||
icon={props.expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
</Space>
|
||||
</div>;
|
||||
})
|
||||
|
||||
interface Props {
|
||||
seriesIndex: number;
|
||||
|
|
@ -18,25 +76,23 @@ interface Props {
|
|||
observeChanges?: () => void;
|
||||
excludeFilterKeys?: Array<string>;
|
||||
canExclude?: boolean;
|
||||
expandable?: boolean;
|
||||
}
|
||||
|
||||
function FilterSeries(props: Props) {
|
||||
const {
|
||||
observeChanges = () => {},
|
||||
observeChanges = () => {
|
||||
},
|
||||
canDelete,
|
||||
hideHeader = false,
|
||||
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
|
||||
supportsEmpty = true,
|
||||
excludeFilterKeys = [],
|
||||
canExclude = false,
|
||||
expandable = false
|
||||
} = props;
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const { series, seriesIndex } = props;
|
||||
|
||||
const onAddFilter = (filter: any) => {
|
||||
series.filter.addFilter(filter);
|
||||
observeChanges();
|
||||
};
|
||||
const [expanded, setExpanded] = useState(!expandable);
|
||||
const {series, seriesIndex} = props;
|
||||
|
||||
const onUpdateFilter = (filterIndex: any, filter: any) => {
|
||||
series.filter.updateFilter(filterIndex, filter);
|
||||
|
|
@ -48,7 +104,8 @@ function FilterSeries(props: Props) {
|
|||
observeChanges();
|
||||
}
|
||||
|
||||
const onChangeEventsOrder = (_: any, { name, value }: any) => {
|
||||
const onChangeEventsOrder = (_: any, {name, value}: any) => {
|
||||
console.log(name, value)
|
||||
series.filter.updateKey(name, value);
|
||||
observeChanges();
|
||||
};
|
||||
|
|
@ -59,27 +116,28 @@ function FilterSeries(props: Props) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="border rounded bg-white">
|
||||
{canExclude && <ExcludeFilters filter={series.filter} />}
|
||||
<div className={cn('border-b px-5 h-12 flex items-center relative', { hidden: hideHeader })}>
|
||||
<div className="mr-auto">
|
||||
<SeriesName
|
||||
<div className="border rounded-lg shadow-sm bg-white">
|
||||
{canExclude && <ExcludeFilters filter={series.filter}/>}
|
||||
|
||||
{!hideHeader && (
|
||||
<FilterSeriesHeader hidden={hideHeader}
|
||||
seriesIndex={seriesIndex}
|
||||
name={series.name}
|
||||
onUpdate={(name) => series.update('name', name)}
|
||||
/>
|
||||
</div>
|
||||
series={series}
|
||||
onRemove={props.onRemoveSeries}
|
||||
canDelete={canDelete}
|
||||
expanded={expanded}
|
||||
toggleExpand={() => setExpanded(!expanded)}/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<div onClick={props.onRemoveSeries} className={cn('ml-3', { disabled: !canDelete })}>
|
||||
<Icon name="trash" size="16" />
|
||||
</div>
|
||||
{expandable && !expanded && (
|
||||
<Space className="justify-between w-full px-5 py-2">
|
||||
<FilterCountLabels filters={series.filter.filters} toggleExpand={() => setExpanded(!expanded)}/>
|
||||
<Button onClick={() => setExpanded(!expanded)}
|
||||
size="small"
|
||||
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<div onClick={() => setExpanded(!expanded)} className="ml-3">
|
||||
<Icon name="chevron-down" size="16" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{expanded && (
|
||||
<>
|
||||
<div className="p-5">
|
||||
|
|
@ -92,22 +150,21 @@ function FilterSeries(props: Props) {
|
|||
supportsEmpty={supportsEmpty}
|
||||
onFilterMove={onFilterMove}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
actions={[
|
||||
expandable && (
|
||||
<Button onClick={() => setExpanded(!expanded)}
|
||||
size="small"
|
||||
icon={expanded ? <ChevronUp size={16}/> : <ChevronDown size={16}/>}/>
|
||||
)
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<div className="color-gray-medium">{emptyMessage}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="border-t h-12 flex items-center">
|
||||
<div className="-mx-4 px-6">
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
excludeFilterKeys={excludeFilterKeys}
|
||||
>
|
||||
<Button variant="text-primary" icon="plus">
|
||||
ADD STEP
|
||||
</Button>
|
||||
</FilterSelection>
|
||||
<div className="-mx-4 px-5">
|
||||
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={series}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ function FunnelIssues() {
|
|||
return useObserver(() => (
|
||||
<div className="my-8 bg-white rounded p-4 border">
|
||||
<div className="flex">
|
||||
<h1 className="font-medium text-2xl">Most significant issues <span className="font-normal">identified in this funnel</span></h1>
|
||||
<h2 className="font-medium text-xl">Most significant issues <span className="font-normal">identified in this funnel</span></h2>
|
||||
</div>
|
||||
<div className="my-6 flex justify-between items-start">
|
||||
<FunnelIssuesDropdown />
|
||||
|
|
|
|||
|
|
@ -1,31 +1,35 @@
|
|||
import React from 'react';
|
||||
import { PageTitle, Button, Toggler, Icon } from "UI";
|
||||
import { Segmented } from 'antd';
|
||||
import {PageTitle, Button, Toggler, Icon} from "UI";
|
||||
import {Segmented} from 'antd';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import Select from 'Shared/Select';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer, useObserver } from 'mobx-react-lite';
|
||||
import { DROPDOWN_OPTIONS } from 'App/constants/card';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {observer, useObserver} from 'mobx-react-lite';
|
||||
import {DROPDOWN_OPTIONS} from 'App/constants/card';
|
||||
import AddCardModal from 'Components/Dashboard/components/AddCardModal';
|
||||
import { useModal } from 'Components/Modal';
|
||||
import {useModal} from 'Components/Modal';
|
||||
import AddCardSelectionModal from "Components/Dashboard/components/AddCardSelectionModal";
|
||||
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
|
||||
|
||||
function MetricViewHeader({ siteId }: { siteId: string }) {
|
||||
const { metricStore } = useStore();
|
||||
function MetricViewHeader({siteId}: { siteId: string }) {
|
||||
const {metricStore} = useStore();
|
||||
const filter = metricStore.filter;
|
||||
const { showModal } = useModal();
|
||||
const {showModal} = useModal();
|
||||
const [showAddCardModal, setShowAddCardModal] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center justify-between px-6'>
|
||||
<div className='flex items-baseline mr-3'>
|
||||
<PageTitle title='Cards' className='' />
|
||||
<PageTitle title='Cards' className=''/>
|
||||
</div>
|
||||
<div className='ml-auto flex items-center'>
|
||||
<Button variant='primary'
|
||||
onClick={() => showModal(<AddCardModal siteId={siteId} />, { right: true })}
|
||||
// onClick={() => showModal(<AddCardModal siteId={siteId}/>, {right: true})}
|
||||
onClick={() => setShowAddCardModal(true)}
|
||||
>New Card</Button>
|
||||
<div className='ml-4 w-1/4' style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
<div className='ml-4 w-1/4' style={{minWidth: 300}}>
|
||||
<MetricsSearch/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,15 +42,15 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
|
|||
name='test'
|
||||
className='font-medium mr-2'
|
||||
onChange={() =>
|
||||
metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine })
|
||||
metricStore.updateKey('filter', {...filter, showMine: !filter.showMine})
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
options={[{ label: 'All Types', value: 'all' }, ...DROPDOWN_OPTIONS]}
|
||||
options={[{label: 'All Types', value: 'all'}, ...DROPDOWN_OPTIONS]}
|
||||
name='type'
|
||||
defaultValue={filter.type}
|
||||
onChange={({ value }) =>
|
||||
metricStore.updateKey('filter', { ...filter, type: value.value })
|
||||
onChange={({value}) =>
|
||||
metricStore.updateKey('filter', {...filter, type: value.value})
|
||||
}
|
||||
plain={true}
|
||||
isSearchable={true}
|
||||
|
|
@ -55,26 +59,33 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
|
|||
<DashboardDropdown
|
||||
plain={true}
|
||||
onChange={(value: any) =>
|
||||
metricStore.updateKey('filter', { ...filter, dashboard: value })
|
||||
metricStore.updateKey('filter', {...filter, dashboard: value})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<ListViewToggler />
|
||||
<ListViewToggler/>
|
||||
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' }
|
||||
{label: 'Newest', value: 'desc'},
|
||||
{label: 'Oldest', value: 'asc'}
|
||||
]}
|
||||
name='sort'
|
||||
defaultValue={metricStore.sort.by}
|
||||
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })}
|
||||
onChange={({value}) => metricStore.updateKey('sort', {by: value.value})}
|
||||
plain={true}
|
||||
className='ml-4'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/*<AddCardSelectionModal open={showAddCardModal}/>*/}
|
||||
<NewDashboardModal
|
||||
onClose={() => setShowAddCardModal(false)}
|
||||
open={showAddCardModal}
|
||||
isCreatingNewCard={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -82,8 +93,8 @@ function MetricViewHeader({ siteId }: { siteId: string }) {
|
|||
|
||||
export default observer(MetricViewHeader);
|
||||
|
||||
function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) {
|
||||
const { dashboardStore, metricStore } = useStore();
|
||||
function DashboardDropdown({onChange, plain = false}: { plain?: boolean; onChange: any }) {
|
||||
const {dashboardStore, metricStore} = useStore();
|
||||
const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({
|
||||
key: i.id,
|
||||
label: i.name,
|
||||
|
|
@ -97,14 +108,14 @@ function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onCha
|
|||
plain={plain}
|
||||
options={dashboardOptions}
|
||||
value={metricStore.filter.dashboard}
|
||||
onChange={({ value }: any) => onChange(value)}
|
||||
onChange={({value}: any) => onChange(value)}
|
||||
isMulti={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ListViewToggler() {
|
||||
const { metricStore } = useStore();
|
||||
const {metricStore} = useStore();
|
||||
const listView = useObserver(() => metricStore.listView);
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
|
|
@ -112,14 +123,14 @@ function ListViewToggler() {
|
|||
options={[
|
||||
{
|
||||
label: <div className={'flex items-center gap-2'}>
|
||||
<Icon name={'list-alt'} color={'inherit'} />
|
||||
<Icon name={'list-alt'} color={'inherit'}/>
|
||||
<div>List</div>
|
||||
</div>,
|
||||
value: 'list'
|
||||
},
|
||||
{
|
||||
label: <div className={'flex items-center gap-2'}>
|
||||
<Icon name={'grid'} color={'inherit'} />
|
||||
<Icon name={'grid'} color={'inherit'}/>
|
||||
<div>Grid</div>
|
||||
</div>,
|
||||
value: 'grid'
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ interface Props {
|
|||
}
|
||||
function MetricsView({ siteId }: Props) {
|
||||
return useObserver(() => (
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded pt-4 border">
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto' }} className="bg-white rounded-lg shadow-sm pt-4 border">
|
||||
<MetricViewHeader siteId={siteId} />
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionList';
|
||||
|
|
@ -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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionListItem';
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import React from 'react';
|
||||
import SessionList from '../SessionList';
|
||||
|
||||
function SessionWidget(props) {
|
||||
return (
|
||||
<div>
|
||||
<SessionList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SessionWidget;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SessionWidget';
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, {useState, useRef, useEffect} from 'react';
|
||||
import CustomMetriLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetriLineChart';
|
||||
import CustomMetricLineChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricLineChart';
|
||||
import CustomMetricPercentage from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPercentage';
|
||||
import CustomMetricTable from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTable';
|
||||
import CustomMetricPieChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart';
|
||||
|
|
@ -33,6 +33,7 @@ import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/
|
|||
import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard';
|
||||
import SankeyChart from 'Shared/Insights/SankeyChart';
|
||||
import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard';
|
||||
import SessionsBy from "Components/Dashboard/Widgets/CustomMetricsWidgets/SessionsBy";
|
||||
|
||||
interface Props {
|
||||
metric: any;
|
||||
|
|
@ -140,7 +141,7 @@ function WidgetChart(props: Props) {
|
|||
if (metricType === TIMESERIES) {
|
||||
if (viewType === 'lineChart') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
<CustomMetricLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
|
|
@ -181,11 +182,17 @@ function WidgetChart(props: Props) {
|
|||
}
|
||||
if (viewType === TABLE) {
|
||||
return (
|
||||
<CustomMetricTable
|
||||
metric={metric} data={data[0]}
|
||||
<SessionsBy
|
||||
metric={metric}
|
||||
data={data[0]}
|
||||
onClick={onChartClick}
|
||||
isTemplate={isTemplate}
|
||||
/>
|
||||
// <CustomMetricTable
|
||||
// metric={metric} data={data[0]}
|
||||
// onClick={onChartClick}
|
||||
// isTemplate={isTemplate}
|
||||
// />
|
||||
);
|
||||
} else if (viewType === 'pieChart') {
|
||||
return (
|
||||
|
|
@ -229,7 +236,7 @@ function WidgetChart(props: Props) {
|
|||
if (metricType === RETENTION) {
|
||||
if (viewType === 'trend') {
|
||||
return (
|
||||
<CustomMetriLineChart
|
||||
<CustomMetricLineChart
|
||||
data={data}
|
||||
colors={colors}
|
||||
params={params}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react';
|
||||
import SelectDateRange from 'Shared/SelectDateRange';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {useObserver} from 'mobx-react-lite';
|
||||
import {Space} from "antd";
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
function WidgetDateRange(props: Props) {
|
||||
const { dashboardStore } = useStore();
|
||||
function WidgetDateRange({
|
||||
label = 'Time Range',
|
||||
}: any) {
|
||||
const {dashboardStore} = useStore();
|
||||
const period = useObserver(() => dashboardStore.drillDownPeriod);
|
||||
const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter);
|
||||
|
||||
|
|
@ -21,15 +21,14 @@ function WidgetDateRange(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="mr-1 color-gray-medium">Time Range</span>
|
||||
<Space>
|
||||
{label && <span className="mr-1 color-gray-medium">{label}</span>}
|
||||
<SelectDateRange
|
||||
period={period}
|
||||
// onChange={(period: any) => metric.setPeriod(period)}
|
||||
onChange={onChangePeriod}
|
||||
right={true}
|
||||
/>
|
||||
</>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button, Icon, confirm, Tooltip } from 'UI';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptions';
|
||||
import {FilterKey} from 'Types/filter/filterType';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {Button, Icon, confirm, Tooltip} from 'UI';
|
||||
import FilterSeries from '../FilterSeries';
|
||||
import Select from 'Shared/Select';
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes';
|
||||
import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes';
|
||||
import MetricTypeDropdown from './components/MetricTypeDropdown';
|
||||
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
|
||||
import {
|
||||
|
|
@ -22,28 +22,29 @@ import {
|
|||
USER_PATH,
|
||||
RETENTION
|
||||
} from 'App/constants/card';
|
||||
import { eventKeys } from 'App/types/filter/newFilter';
|
||||
import { renderClickmapThumbnail } from './renderMap';
|
||||
import {eventKeys} from 'App/types/filter/newFilter';
|
||||
import {renderClickmapThumbnail} from './renderMap';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
import FilterItem from 'Shared/Filters/FilterItem';
|
||||
import { Input } from 'antd'
|
||||
import {Input} from 'antd'
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
onDelete: () => void;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
function WidgetForm(props: Props) {
|
||||
const {
|
||||
history,
|
||||
match: {
|
||||
params: { siteId, dashboardId }
|
||||
params: {siteId, dashboardId}
|
||||
}
|
||||
} = props;
|
||||
const [aiQuery, setAiQuery] = useState('')
|
||||
const [aiAskChart, setAiAskChart] = useState('')
|
||||
const { metricStore, dashboardStore, aiFiltersStore } = useStore();
|
||||
const {metricStore, dashboardStore, aiFiltersStore} = useStore();
|
||||
const isSaving = metricStore.isSaving;
|
||||
const metric: any = metricStore.instance;
|
||||
const [initialInstance, setInitialInstance] = useState();
|
||||
|
|
@ -72,9 +73,9 @@ function WidgetForm(props: Props) {
|
|||
}
|
||||
}, [metric]);
|
||||
|
||||
const writeOption = ({ value, name }: { value: any; name: any }) => {
|
||||
const writeOption = ({value, name}: { value: any; name: any }) => {
|
||||
value = Array.isArray(value) ? value : value.value;
|
||||
const obj: any = { [name]: value };
|
||||
const obj: any = {[name]: value};
|
||||
|
||||
if (name === 'metricType') {
|
||||
switch (value) {
|
||||
|
|
@ -158,11 +159,12 @@ function WidgetForm(props: Props) {
|
|||
const testingKey = localStorage.getItem('__mauricio_testing_access') === 'true';
|
||||
return (
|
||||
<div className='p-6'>
|
||||
{/*
|
||||
<div className='form-group'>
|
||||
<div className='flex items-center'>
|
||||
<span className='mr-2'>Card showing</span>
|
||||
<MetricTypeDropdown onSelect={writeOption} />
|
||||
<MetricSubtypeDropdown onSelect={writeOption} />
|
||||
<MetricTypeDropdown onSelect={writeOption}/>
|
||||
<MetricSubtypeDropdown onSelect={writeOption}/>
|
||||
|
||||
{isPathAnalysis && (
|
||||
<>
|
||||
|
|
@ -170,8 +172,8 @@ function WidgetForm(props: Props) {
|
|||
<Select
|
||||
name='startType'
|
||||
options={[
|
||||
{ value: 'start', label: 'With Start Point' },
|
||||
{ value: 'end', label: 'With End Point' }
|
||||
{value: 'start', label: 'With Start Point'},
|
||||
{value: 'end', label: 'With End Point'}
|
||||
]}
|
||||
defaultValue={metric.startType}
|
||||
// value={metric.metricOf}
|
||||
|
|
@ -183,10 +185,10 @@ function WidgetForm(props: Props) {
|
|||
<Select
|
||||
name='metricValue'
|
||||
options={[
|
||||
{ value: 'location', label: 'Pages' },
|
||||
{ value: 'click', label: 'Clicks' },
|
||||
{ value: 'input', label: 'Input' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
{value: 'location', label: 'Pages'},
|
||||
{value: 'click', label: 'Clicks'},
|
||||
{value: 'input', label: 'Input'},
|
||||
{value: 'custom', label: 'Custom'},
|
||||
]}
|
||||
defaultValue={metric.metricValue}
|
||||
isMulti={true}
|
||||
|
|
@ -231,7 +233,7 @@ function WidgetForm(props: Props) {
|
|||
<span className='mx-3'>showing</span>
|
||||
<Select
|
||||
name='metricFormat'
|
||||
options={[{ value: 'sessionCount', label: 'Session Count' }]}
|
||||
options={[{value: 'sessionCount', label: 'Session Count'}]}
|
||||
defaultValue={metric.metricFormat}
|
||||
onChange={writeOption}
|
||||
/>
|
||||
|
|
@ -240,6 +242,9 @@ function WidgetForm(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
*/}
|
||||
|
||||
|
||||
{isPathAnalysis && (
|
||||
<div className='form-group flex flex-col'>
|
||||
{metric.startType === 'start' ? 'Start Point' : 'End Point'}
|
||||
|
|
@ -251,13 +256,13 @@ function WidgetForm(props: Props) {
|
|||
onUpdate={(val) => {
|
||||
metric.updateStartPoint(val);
|
||||
}} onRemoveFilter={() => {
|
||||
}} />
|
||||
}}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPredefined && (
|
||||
<div className='flex items-center my-6 justify-center'>
|
||||
<Icon name='info-circle' size='18' color='gray-medium' />
|
||||
<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>
|
||||
|
|
@ -349,7 +354,7 @@ function WidgetForm(props: Props) {
|
|||
<div className='flex items-center'>
|
||||
{metric.exists() && (
|
||||
<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
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -42,7 +42,7 @@ function WidgetPredefinedChart(props: Props) {
|
|||
case FilterKey.ERRORS_PER_TYPE:
|
||||
return <ErrorsByType data={data} metric={metric} />
|
||||
case FilterKey.ERRORS_PER_DOMAINS:
|
||||
return <ErrorsPerDomain data={data} metric={metric} />
|
||||
return <ErrorsPerDomain data={metric.data} />
|
||||
case FilterKey.RESOURCES_BY_PARTY:
|
||||
return <ErrorsByOrigin data={data} metric={metric} />
|
||||
case FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS:
|
||||
|
|
@ -52,7 +52,7 @@ function WidgetPredefinedChart(props: Props) {
|
|||
case FilterKey.DOMAINS_ERRORS_5XX:
|
||||
return <CallsErrors5xx data={data} metric={metric} />
|
||||
case FilterKey.CALLS_ERRORS:
|
||||
return <CallWithErrors isTemplate={isTemplate} data={data} metric={metric} />
|
||||
return <CallWithErrors isTemplate={isTemplate} data={data} />
|
||||
|
||||
// PERFORMANCE
|
||||
case FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES:
|
||||
|
|
|
|||
|
|
@ -1,43 +1,42 @@
|
|||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { SegmentSelection, Button, Icon } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
|
||||
import {useStore} from 'App/mstore';
|
||||
// import {SegmentSelection, Button, Icon} from 'UI';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
// import {FilterKey} from 'Types/filter/filterType';
|
||||
// import WidgetDateRange from '../WidgetDateRange/WidgetDateRange';
|
||||
import ClickMapRagePicker from "Components/Dashboard/components/ClickMapRagePicker";
|
||||
import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
import { CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH } from 'App/constants/card';
|
||||
import { Space, Switch } from 'antd';
|
||||
// import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal';
|
||||
import {CLICKMAP, TABLE, TIMESERIES, RETENTION, USER_PATH} from 'App/constants/card';
|
||||
import {Space, Switch} from 'antd';
|
||||
// import AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
name: string;
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
function WidgetPreview(props: Props) {
|
||||
const [showDashboardSelectionModal, setShowDashboardSelectionModal] = React.useState(false);
|
||||
const { className = '' } = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const {className = ''} = props;
|
||||
const {metricStore, dashboardStore} = useStore();
|
||||
// const dashboards = dashboardStore.dashboards;
|
||||
const metric: any = metricStore.instance;
|
||||
const isTimeSeries = metric.metricType === TIMESERIES;
|
||||
const isTable = metric.metricType === TABLE;
|
||||
const isRetention = metric.metricType === RETENTION;
|
||||
const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS;
|
||||
|
||||
const changeViewType = (_, { name, value }: any) => {
|
||||
metric.update({ [ name ]: value });
|
||||
}
|
||||
|
||||
const canAddToDashboard = metric.exists() && dashboards.length > 0;
|
||||
// const isTimeSeries = metric.metricType === TIMESERIES;
|
||||
// const isTable = metric.metricType === TABLE;
|
||||
// const isRetention = metric.metricType === RETENTION;
|
||||
// const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS;
|
||||
//
|
||||
// const changeViewType = (_, {name, value}: any) => {
|
||||
// metric.update({[name]: value});
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn(className, 'bg-white rounded border')}>
|
||||
<div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
|
||||
<div className="flex items-center justify-between px-4 pt-2">
|
||||
<h2 className="text-2xl">
|
||||
<h2 className="text-xl">
|
||||
{props.name}
|
||||
</h2>
|
||||
<div className="flex items-center">
|
||||
|
|
@ -46,7 +45,7 @@ function WidgetPreview(props: Props) {
|
|||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
metric.update({ hideExcess: !metric.hideExcess });
|
||||
metric.update({hideExcess: !metric.hideExcess});
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
|
|
@ -58,91 +57,79 @@ function WidgetPreview(props: Props) {
|
|||
</Space>
|
||||
</a>
|
||||
)}
|
||||
{isTimeSeries && (
|
||||
<>
|
||||
<span className="mr-4 color-gray-medium">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary
|
||||
size="small"
|
||||
onSelect={ changeViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={ [
|
||||
{ value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },
|
||||
{ value: 'progress', name: 'Progress', icon: 'hash' },
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!disableVisualization && isTable && (
|
||||
<>
|
||||
<span className="mr-4 color-gray-medium">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary={true}
|
||||
size="small"
|
||||
onSelect={ changeViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={[
|
||||
{ value: 'table', name: 'Table', icon: 'table' },
|
||||
{ value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },
|
||||
]}
|
||||
disabledMessage="Chart view is not supported"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{/*{isTimeSeries && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={ [*/}
|
||||
{/* { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' },*/}
|
||||
{/* { value: 'progress', name: 'Progress', icon: 'hash' },*/}
|
||||
{/* ]}*/}
|
||||
{/* />*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{isRetention && (
|
||||
<>
|
||||
<span className="mr-4 color-gray-medium">Visualization</span>
|
||||
<SegmentSelection
|
||||
name="viewType"
|
||||
className="my-3"
|
||||
primary={true}
|
||||
size="small"
|
||||
onSelect={ changeViewType }
|
||||
value={{ value: metric.viewType }}
|
||||
list={[
|
||||
{ value: 'trend', name: 'Trend', icon: 'graph-up-arrow' },
|
||||
{ value: 'cohort', name: 'Cohort', icon: 'dice-3' },
|
||||
]}
|
||||
disabledMessage="Chart view is not supported"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="mx-4" />
|
||||
{/*{!disableVisualization && isTable && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary={true}*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={[*/}
|
||||
{/* { value: 'table', name: 'Table', icon: 'table' },*/}
|
||||
{/* { value: 'pieChart', name: 'Chart', icon: 'pie-chart-fill' },*/}
|
||||
{/* ]}*/}
|
||||
{/* disabledMessage="Chart view is not supported"*/}
|
||||
{/* />*/}
|
||||
{/* </>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/*{isRetention && (*/}
|
||||
{/* <>*/}
|
||||
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
|
||||
{/* <SegmentSelection*/}
|
||||
{/* name="viewType"*/}
|
||||
{/* className="my-3"*/}
|
||||
{/* primary={true}*/}
|
||||
{/* size="small"*/}
|
||||
{/* onSelect={ changeViewType }*/}
|
||||
{/* value={{ value: metric.viewType }}*/}
|
||||
{/* list={[*/}
|
||||
{/* { value: 'trend', name: 'Trend', icon: 'graph-up-arrow' },*/}
|
||||
{/* { value: 'cohort', name: 'Cohort', icon: 'dice-3' },*/}
|
||||
{/* ]}*/}
|
||||
{/* disabledMessage="Chart view is not supported"*/}
|
||||
{/* />*/}
|
||||
{/*</>*/}
|
||||
{/*)}*/}
|
||||
|
||||
<div className="mx-4"/>
|
||||
{metric.metricType === CLICKMAP ? (
|
||||
<ClickMapRagePicker />
|
||||
<ClickMapRagePicker/>
|
||||
) : null}
|
||||
<WidgetDateRange />
|
||||
|
||||
|
||||
{/* add to dashboard */}
|
||||
{metric.exists() && (
|
||||
<Button
|
||||
variant="text-primary"
|
||||
className="ml-2 p-0"
|
||||
onClick={() => setShowDashboardSelectionModal(true)}
|
||||
disabled={!canAddToDashboard}
|
||||
>
|
||||
<Icon name="columns-gap-filled" size="14" className="mr-2" color="teal"/>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
)}
|
||||
{/*{metric.exists() && (*/}
|
||||
{/* <AddToDashboardButton metricId={metric.metricId}/>*/}
|
||||
{/*)}*/}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 pt-0">
|
||||
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName />
|
||||
<div className="pt-0">
|
||||
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName/>
|
||||
</div>
|
||||
</div>
|
||||
{ canAddToDashboard && (
|
||||
<DashboardSelectionModal
|
||||
metricId={metric.metricId}
|
||||
show={showDashboardSelectionModal}
|
||||
closeHandler={() => setShowDashboardSelectionModal(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,18 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import cn from 'classnames';
|
||||
import { Icon, Loader, NoContent } from 'UI';
|
||||
import WidgetForm from '../WidgetForm';
|
||||
import React, {useState} from 'react';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {Icon, Loader, NoContent} from 'UI';
|
||||
import WidgetPreview from '../WidgetPreview';
|
||||
import WidgetSessions from '../WidgetSessions';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import WidgetName from '../WidgetName';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import {useObserver} from 'mobx-react-lite';
|
||||
import {dashboardMetricDetails, metricDetails, withSiteId} from 'App/routes';
|
||||
import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues';
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
import { FilterKey } from 'Types/filter/filterType';
|
||||
import { Prompt } from 'react-router';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import {FilterKey} from 'Types/filter/filterType';
|
||||
import {Prompt, useHistory} from 'react-router';
|
||||
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
|
|
@ -21,36 +18,46 @@ import {
|
|||
INSIGHTS,
|
||||
USER_PATH,
|
||||
RETENTION,
|
||||
} from 'App/constants/card';
|
||||
} from 'App/constants/card';
|
||||
import CardIssues from '../CardIssues';
|
||||
import CardUserList from '../CardUserList/CardUserList';
|
||||
import WidgetViewHeader from "Components/Dashboard/components/WidgetView/WidgetViewHeader";
|
||||
import WidgetFormNew from "Components/Dashboard/components/WidgetForm/WidgetFormNew";
|
||||
import {Space} from "antd";
|
||||
import {renderClickmapThumbnail} from "Components/Dashboard/components/WidgetForm/renderMap";
|
||||
import Widget from "App/mstore/types/widget";
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
match: any;
|
||||
siteId: any;
|
||||
}
|
||||
|
||||
function WidgetView(props: Props) {
|
||||
const {
|
||||
match: {
|
||||
params: { siteId, dashboardId, metricId },
|
||||
params: {siteId, dashboardId, metricId},
|
||||
},
|
||||
} = props;
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
// const siteId = location.pathname.split('/')[1];
|
||||
// const dashboardId = location.pathname.split('/')[3];
|
||||
const {metricStore, dashboardStore} = useStore();
|
||||
const widget = useObserver(() => metricStore.instance);
|
||||
const loading = useObserver(() => metricStore.isLoading);
|
||||
const [expanded, setExpanded] = useState(!metricId || metricId === 'create');
|
||||
const hasChanged = useObserver(() => widget.hasChanged);
|
||||
|
||||
const dashboards = useObserver(() => dashboardStore.dashboards);
|
||||
const dashboard = useObserver(() => dashboards.find((d: any) => d.dashboardId == dashboardId));
|
||||
const dashboardName = dashboard ? dashboard.name : null;
|
||||
const [metricNotFound, setMetricNotFound] = useState(false);
|
||||
const history = useHistory();
|
||||
const [initialInstance, setInitialInstance] = useState();
|
||||
const isClickMap = widget.metricType === CLICKMAP;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (metricId && metricId !== 'create') {
|
||||
metricStore.fetch(metricId, dashboardStore.period).catch((e) => {
|
||||
if (e.status === 404 || e.status === 422) {
|
||||
if (e.response.status === 404 || e.response.status === 422) {
|
||||
setMetricNotFound(true);
|
||||
}
|
||||
});
|
||||
|
|
@ -59,13 +66,44 @@ function WidgetView(props: Props) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const onBackHandler = () => {
|
||||
props.history.goBack();
|
||||
// const onBackHandler = () => {
|
||||
// props.history.goBack();
|
||||
// };
|
||||
//
|
||||
// const openEdit = () => {
|
||||
// if (expanded) return;
|
||||
// setExpanded(true);
|
||||
// };
|
||||
|
||||
const undoChanges = () => {
|
||||
const w = new Widget();
|
||||
metricStore.merge(w.fromJson(initialInstance), false);
|
||||
};
|
||||
|
||||
const openEdit = () => {
|
||||
if (expanded) return;
|
||||
setExpanded(true);
|
||||
const onSave = async () => {
|
||||
const wasCreating = !widget.exists();
|
||||
if (isClickMap) {
|
||||
try {
|
||||
widget.thumbnail = await renderClickmapThumbnail();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
const savedMetric = await metricStore.save(widget);
|
||||
setInitialInstance(widget.toJson());
|
||||
if (wasCreating) {
|
||||
if (parseInt(dashboardId, 10) > 0) {
|
||||
history.replace(
|
||||
withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)
|
||||
);
|
||||
void dashboardStore.addWidgetToDashboard(
|
||||
dashboardStore.getDashboard(parseInt(dashboardId, 10))!,
|
||||
[savedMetric.metricId]
|
||||
);
|
||||
} else {
|
||||
history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return useObserver(() => (
|
||||
|
|
@ -80,57 +118,47 @@ function WidgetView(props: Props) {
|
|||
}}
|
||||
/>
|
||||
|
||||
<div style={{ maxWidth: '1360px', margin: 'auto'}}>
|
||||
<div style={{maxWidth: '1360px', margin: 'auto'}}>
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: dashboardName ? dashboardName : 'Cards',
|
||||
to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId),
|
||||
},
|
||||
{ label: widget.name },
|
||||
{label: widget.name},
|
||||
]}
|
||||
/>
|
||||
<NoContent
|
||||
show={metricNotFound}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-between">
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100} />
|
||||
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100}/>
|
||||
<div className="mt-4">Metric not found!</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="bg-white rounded border">
|
||||
<div
|
||||
className={cn('px-6 py-4 flex justify-between items-center', {
|
||||
'cursor-pointer hover:bg-active-blue hover:shadow-border-blue rounded': !expanded,
|
||||
})}
|
||||
onClick={openEdit}
|
||||
>
|
||||
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
|
||||
<WidgetName name={widget.name} onUpdate={(name) => metricStore.merge({ name })} canEdit={expanded} />
|
||||
</h1>
|
||||
<div className="text-gray-600 w-full cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="flex items-center select-none w-fit ml-auto">
|
||||
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Edit'}</span>
|
||||
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Space direction="vertical" size={20} className="w-full">
|
||||
<WidgetViewHeader onSave={onSave} undoChanges={undoChanges}/>
|
||||
|
||||
{expanded && <WidgetForm onDelete={onBackHandler} {...props} />}
|
||||
</div>
|
||||
<WidgetFormNew/>
|
||||
|
||||
<WidgetPreview className="mt-8" name={widget.name} isEditing={expanded} />
|
||||
{/*<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.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) && <WidgetSessions className="mt-8" />}
|
||||
{widget.metricType === FUNNEL && <FunnelIssues />}
|
||||
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) &&
|
||||
<WidgetSessions/>}
|
||||
{widget.metricType === FUNNEL && <FunnelIssues/>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{widget.metricType === USER_PATH && <CardIssues />}
|
||||
{widget.metricType === RETENTION && <CardUserList />}
|
||||
{widget.metricType === USER_PATH && <CardIssues/>}
|
||||
{widget.metricType === RETENTION && <CardUserList/>}
|
||||
</Space>
|
||||
</NoContent>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -1,26 +1,33 @@
|
|||
import React from 'react';
|
||||
import WidgetIcon from './WidgetIcon';
|
||||
import { useStore } from 'App/mstore';
|
||||
import {useStore} from 'App/mstore';
|
||||
import {Button} from "antd";
|
||||
import {BellIcon} from "lucide-react";
|
||||
import {useModal} from "Components/ModalContext";
|
||||
import AlertFormModal from "Components/Alerts/AlertFormModal/AlertFormModal";
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
initAlert: Function;
|
||||
initAlert?: Function;
|
||||
}
|
||||
|
||||
function AlertButton(props: Props) {
|
||||
const { seriesId } = props;
|
||||
const { dashboardStore, alertsStore } = useStore();
|
||||
const {seriesId} = props;
|
||||
const {dashboardStore, alertsStore} = useStore();
|
||||
const {openModal, closeModal} = useModal();
|
||||
const onClick = () => {
|
||||
dashboardStore.toggleAlertModal(true);
|
||||
alertsStore.init({ query: { left: seriesId }})
|
||||
// dashboardStore.toggleAlertModal(true);
|
||||
alertsStore.init({query: {left: seriesId}})
|
||||
openModal(<AlertFormModal
|
||||
onClose={closeModal}
|
||||
/>, {
|
||||
// title: 'Set Alerts',
|
||||
placement: 'right',
|
||||
width: 620,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div onClick={onClick}>
|
||||
<WidgetIcon
|
||||
className="cursor-pointer"
|
||||
icon="bell-plus"
|
||||
tooltip="Set Alert"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={onClick} type="text" icon={<BellIcon size={16}/>}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -97,7 +97,7 @@ function WidgetWrapper(props: Props & RouteComponentProps) {
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative rounded bg-white border group',
|
||||
'relative rounded bg-white border group rounded-lg',
|
||||
'col-span-' + widget.config.col,
|
||||
{ 'hover:shadow-border-gray': !isTemplate && isWidget },
|
||||
{ 'hover:shadow-border-main': isTemplate }
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.overlayDashboard {
|
||||
top: 20%!important;
|
||||
top: 40px !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function ForgotPassword(props: Props) {
|
|||
<div className="m-10 ">
|
||||
<img src="/assets/logo.svg" width={200} />
|
||||
</div>
|
||||
<div className="border rounded bg-white" style={{ width: '350px' }}>
|
||||
<div className="border rounded-lg bg-white shadow-sm" style={{ width: '350px' }}>
|
||||
{creatingNewPassword ? (
|
||||
<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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue