Merge branch 'dashboards-redesign' into dev

This commit is contained in:
Shekar Siri 2024-07-02 13:43:40 +02:00
commit 9cbb1bf283
108 changed files with 2782 additions and 2442 deletions

View file

@ -7,7 +7,7 @@ import {
edit as editFilter,
fetchFilterSearch,
} from 'Duck/liveSearch';
import { Button } from 'UI';
import { Button } from 'antd';
import { useModal } from 'App/components/Modal';
import SessionSearchField from 'Shared/SessionSearchField';
import { MODULES } from 'Components/Client/Modules';
@ -42,11 +42,11 @@ function AssistSearchField(props: Props) {
<SessionSearchField />
</div>
{props.isEnterprise && props.modules.includes(MODULES.OFFLINE_RECORDINGS)
? <Button variant="outline" onClick={showRecords}>Training Videos</Button> : null
? <Button type="primary" ghost onClick={showRecords}>Training Videos</Button> : null
}
<Button variant="outline" onClick={showStats} disabled={!props.modules.includes(MODULES.ASSIST_STATS) || !props.modules.includes(MODULES.ASSIST)}>Co-Browsing Reports</Button>
<Button type="primary" ghost onClick={showStats} disabled={!props.modules.includes(MODULES.ASSIST_STATS) || !props.modules.includes(MODULES.ASSIST)}>Co-Browsing Reports</Button>
<Button
variant="text-primary"
type="link"
className="ml-auto font-medium"
disabled={!hasFilters && !hasEvents}
onClick={() => props.clearSearch()}

View file

@ -1,8 +1,13 @@
import { withCopy } from 'HOCs';
import React from 'react';
import { Tag } from "antd";
function ProjectKey({ value }: any) {
return <div className="rounded border bg-gray-lightest w-fit px-2">{value}</div>;
return <div className="w-fit">
<Tag bordered={false} className='text-base font-mono'>
{value}
</Tag>
</div>;
}
export default withCopy(ProjectKey);

View file

@ -77,7 +77,7 @@ const Sites = ({ loading, sites, user, init }: PropsFromRedux) => {
</div>
<span className="ml-2">{project.host}</span>
<div className={'ml-4 flex items-center gap-2'}>
{project.platform === 'web' ? null : <Tag color="error">MOBILE BETA</Tag>}
{project.platform === 'web' ? null : <Tag bordered={false} color="green">MOBILE BETA</Tag>}
</div>
</div>
</div>

View file

@ -1,60 +1,60 @@
import React from 'react';
import { List, Progress, Typography } from "antd";
import cn from "classnames";
import { List, Progress, Typography } from 'antd';
import cn from 'classnames';
interface Props {
list: any;
selected: any;
onClickHandler: (event: any, data: any) => void;
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>
function CardSessionsByList({ list, selected, onClickHandler = () => null }: 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.displayName}</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>
<Progress
percent={row.progress}
showInfo={false}
strokeColor={{
'0%': '#394EFF',
'100%': '#394EFF'
}}
size={['small', 2]}
style={{
padding: '0 0px',
margin: '0 0px',
height: 4
}}
/>
</div>
)}
/>
);
/>
</List.Item>
)}
/>
);
}
export default CardSessionsByList;

View file

@ -27,7 +27,7 @@ function CustomMetricOverviewChart(props: Props) {
/>
</div>
</div>
<ResponsiveContainer height={100} width="100%">
<ResponsiveContainer height={240} width="100%" className='rounded-lg overflow-hidden'>
<AreaChart
data={data.chart}
margin={{

View file

@ -1,88 +1,90 @@
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";
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;
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 { 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;
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)
setSelected(data.name);
filters.push(filter);
onClick(filters);
}
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,
})
}
const showMore = (e: any) => {
e.stopPropagation();
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>
)}
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 }}>
{/* TODO - remove slice once the api pagination is fixed */}
<CardSessionsByList list={data.values.slice(0, 3)}
selected={selected}
onClickHandler={onClickHandler} />
{total > 3 && (
<div className="flex">
<Button type="link" onClick={showMore}>
<Space className="flex font-medium gap-1">
{total - 3} More
<ArrowRight size={16} />
</Space>
</Button>
</div>
)}
</div>
);
)}
</div>
);
}
export default SessionsBy;

View file

@ -0,0 +1,65 @@
import React from 'react';
import { List, Progress, Typography } from 'antd';
import cn from 'classnames';
interface ListItem {
icon?: any;
title: string;
progress: number;
value?: number;
}
interface Props {
list: ListItem[];
}
function ListWithIcons({ list = [] }: Props) {
return (
<List
dataSource={list}
split={false}
renderItem={(row: any) => (
<List.Item
key={row.domain}
// 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')} // 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.value}</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 ListWithIcons;

View file

@ -1,38 +1,34 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from './Bar';
import { NO_METRIC_DATA } from 'App/constants/messages'
import { Icon, NoContent } from 'UI';
import { NO_METRIC_DATA } from 'App/constants/messages';
import ListWithIcons from 'Components/Dashboard/Widgets/ListWithIcons';
interface Props {
data: any
data: any;
}
function ErrorsPerDomain(props: Props) {
const { data } = props;
// const firstAvg = 10;
const firstAvg = data.chart[0] && data.chart[0].errorsCount;
return (
<NoContent
size="small"
show={ data.chart.length === 0 }
style={{ height: '240px'}}
title={NO_METRIC_DATA}
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"
avg={numberWithCommas(Math.round(item.errorsCount))}
width={Math.round((item.errorsCount * 100) / firstAvg) - 10}
domain={item.domain}
color={Styles.colors[i]}
/>
)}
</div>
</NoContent>
);
const { data } = props;
const highest = data.chart[0] && data.chart[0].errorsCount;
const list = data.chart.slice(0, 4).map((item: any) => ({
name: item.domain,
icon: <Icon name="link-45deg" size={24} />,
value: Math.round(item.errorsCount),
progress: Math.round((item.errorsCount * 100) / highest)
}));
return (
<NoContent
size="small"
show={data.chart.length === 0}
style={{ height: '240px' }}
title={NO_METRIC_DATA}
>
<div className="w-full" style={{ height: '240px' }}>
<ListWithIcons list={list} />
</div>
</NoContent>
);
}
export default ErrorsPerDomain;

View file

@ -2,43 +2,43 @@ import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import Bar from './Bar';
import { NO_METRIC_DATA } from 'App/constants/messages'
import { NO_METRIC_DATA } from 'App/constants/messages';
interface Props {
data: any
metric?: any
data: any;
}
function SessionsPerBrowser(props: Props) {
const { data, metric } = props;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].count;
const getVersions = item => {
return Object.keys(item)
.filter(i => i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp')
.map(i => ({ key: 'v' +i, value: item[i]}))
}
return (
<NoContent
size="small"
title={NO_METRIC_DATA}
show={ metric.data.chart.length === 0 }
style={{ minHeight: 220 }}
>
<div className="w-full" style={{ height: '240px' }}>
{metric.data.chart.map((item, i) =>
<Bar
key={i}
className="mb-4"
avg={Math.round(item.count)}
versions={getVersions(item)}
width={Math.round((item.count * 100) / firstAvg) - 10}
domain={item.browser}
colors={Styles.colors}
/>
)}
</div>
</NoContent>
);
function SessionsPerBrowser(props: Props) {
const { data } = props;
const firstAvg = data.chart[0] && data.chart[0].count;
const getVersions = item => {
return Object.keys(item)
.filter(i => i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp')
.map(i => ({ key: 'v' + i, value: item[i] }));
};
return (
<NoContent
size="small"
title={NO_METRIC_DATA}
show={data.chart.length === 0}
style={{ minHeight: 220 }}
>
<div className="w-full" style={{ height: '240px' }}>
{data.chart.map((item, i) =>
<Bar
key={i}
className="mb-4"
avg={Math.round(item.count)}
versions={getVersions(item)}
width={Math.round((item.count * 100) / firstAvg) - 10}
domain={item.browser}
colors={Styles.colors}
/>
)}
</div>
</NoContent>
);
}
export default SessionsPerBrowser;

View file

@ -1,38 +1,35 @@
import React from 'react';
import { NoContent } from 'UI';
import { Styles } from '../../common';
import { numberWithCommas } from 'App/utils';
import Bar from './Bar';
import { NO_METRIC_DATA } from 'App/constants/messages'
import { Icon, NoContent } from 'UI';
import { NO_METRIC_DATA } from 'App/constants/messages';
import ListWithIcons from 'Components/Dashboard/Widgets/ListWithIcons';
interface Props {
data: any
metric?: any
data: any;
}
function SlowestDomains(props: Props) {
const { data, metric } = props;
const firstAvg = metric.data.chart[0] && metric.data.chart[0].value;
return (
<NoContent
size="small"
show={ metric.data.chart.length === 0 }
style={{ minHeight: 220 }}
title={NO_METRIC_DATA}
>
<div className="w-full" style={{ height: '240px' }}>
{metric.data.chart.map((item, i) =>
<Bar
key={i}
className="mb-2"
avg={numberWithCommas(Math.round(item.value))}
width={Math.round((item.value * 100) / firstAvg) - 10}
domain={item.domain}
color={Styles.colors[i]}
/>
)}
</div>
</NoContent>
);
const { data } = props;
// TODO - move this to the store
const highest = data.chart[0]?.value;
const list = data.chart.slice(0, 4).map((item: any) => ({
name: item.domain,
icon: <Icon name="link-45deg" size={24} />,
value: Math.round(item.value) + 'ms',
progress: Math.round((item.value * 100) / highest)
}));
return (
<NoContent
size="small"
show={list.length === 0}
style={{ minHeight: 220 }}
title={NO_METRIC_DATA}
>
<div className="w-full" style={{ height: '240px' }}>
<ListWithIcons list={list} />
</div>
</NoContent>
);
}
export default SlowestDomains;

View file

@ -1,25 +1,25 @@
import React from 'react'
import React from 'react';
import { Styles } from '../../common';
import cn from 'classnames';
import stl from './scale.module.css';
function Scale({ colors }) {
const lastIndex = (Styles.colorsTeal.length - 1)
const lastIndex = (Styles.compareColors.length - 1);
return (
<div className={ cn(stl.bars, 'absolute bottom-0 mb-4')}>
{Styles.colorsTeal.map((c, i) => (
<div className={cn(stl.bars, 'absolute bottom-0 mb-4')}>
{Styles.compareColors.map((c, i) => (
<div
key={i}
style={{ backgroundColor: c, width: '6px', height: '15px', marginBottom: '1px' }}
className="flex items-center justify-center"
>
{ i === 0 && <div className="text-xs pl-12">Slow</div>}
{ i === lastIndex && <div className="text-xs pl-12">Fast</div>}
{i === 0 && <div className="text-xs pl-12">Slow</div>}
{i === lastIndex && <div className="text-xs pl-12">Fast</div>}
</div>
))}
</div>
)
);
}
export default Scale
export default Scale;

View file

@ -15,7 +15,7 @@
&:focus,
&:hover {
fill: $teal !important;
fill: #2E3ECC !important;
outline: 0;
}
}
@ -25,23 +25,23 @@
}
.heat_index5 {
fill: #3EAAAF !important;
fill: #B0B8FF !important;
}
.heat_index4 {
fill:#5FBABF !important;
fill:#6171FF !important;
}
.heat_index3 {
fill: #7BCBCF !important;
fill: #394EFF !important;
}
.heat_index2 {
fill: #96DCDF !important;
fill: #2E3ECC !important;
}
.heat_index1 {
fill: #ADDCDF !important;
fill: #222F99 !important;
}
.tooltip {
@ -52,4 +52,4 @@
background-color: white;
font-size: 12px;
line-height: 1.2;
}
}

View file

@ -8,99 +8,105 @@ import WorldMap from '@svg-maps/world';
import { SVGMap } from 'react-svg-map';
import stl from './SpeedIndexByLocation.module.css';
import cn from 'classnames';
import { NO_METRIC_DATA } from 'App/constants/messages'
import { NO_METRIC_DATA } from 'App/constants/messages';
interface Props {
metric?: any;
data?: any;
}
function SpeedIndexByLocation(props: Props) {
const { metric } = props;
const wrapper: any = React.useRef(null);
let map: any = null;
const [tooltipStyle, setTooltipStyle] = React.useState({ display: 'none' });
const [pointedLocation, setPointedLocation] = React.useState<any>(null);
const dataMap: any = React.useMemo(() => {
const data: any = {};
const max = metric.data.chart.reduce((acc: any, item: any) => Math.max(acc, item.value), 0);
const min = metric.data.chart.reduce((acc: any, item: any) => Math.min(acc, item.value), 0);
metric.data.chart.forEach((item: any) => {
if (!item || !item.userCountry) { return }
item.perNumber = positionOfTheNumber(min, max, item.value, 5);
data[item.userCountry.toLowerCase()] = item;
});
return data;
}, []);
const { data } = props;
console.log('data', data);
const wrapper: any = React.useRef(null);
let map: any = null;
const [tooltipStyle, setTooltipStyle] = React.useState({ display: 'none' });
const [pointedLocation, setPointedLocation] = React.useState<any>(null);
const getLocationClassName = (location: any) => {
const i = dataMap[location.id] ? dataMap[location.id].perNumber : 0;
const cls = stl['heat_index' + i];
return cn(stl.location, cls);
const dataMap: any = React.useMemo(() => {
const _data: any = {};
const max = data.chart?.reduce((acc: any, item: any) => Math.max(acc, item.value), 0);
const min = data.chart?.reduce((acc: any, item: any) => Math.min(acc, item.value), 0);
data.chart?.forEach((item: any) => {
console.log('item', item);
if (!item || !item.userCountry) {
return;
}
item.perNumber = positionOfTheNumber(min, max, item.value, 5);
_data[item.userCountry.toLowerCase()] = item;
});
return _data;
}, [data.chart]);
const getLocationClassName = (location: any) => {
const i = dataMap[location.id] ? dataMap[location.id].perNumber : 0;
const cls = stl['heat_index' + i];
return cn(stl.location, cls);
};
const getLocationName = (event: any) => {
if (!event) return null;
const id = event.target.attributes.id.value;
const name = event.target.attributes.name.value;
const percentage = dataMap[id] ? dataMap[id].perNumber : 0;
return { name, id, percentage };
};
const handleLocationMouseOver = (event: any) => {
const pointedLocation = getLocationName(event);
setPointedLocation(pointedLocation);
};
const handleLocationMouseOut = () => {
setTooltipStyle({ display: 'none' });
setPointedLocation(null);
};
const handleLocationMouseMove = (event: any) => {
const tooltipStyle = {
display: 'block',
top: event.clientY + 10,
left: event.clientX - 100
};
setTooltipStyle(tooltipStyle);
};
const getLocationName = (event: any) => {
if (!event) return null;
const id = event.target.attributes.id.value;
const name = event.target.attributes.name.value;
const percentage = dataMap[id] ? dataMap[id].perNumber : 0;
return { name, id, percentage };
};
const handleLocationMouseOver = (event: any) => {
const pointedLocation = getLocationName(event);
setPointedLocation(pointedLocation);
};
const handleLocationMouseOut = () => {
setTooltipStyle({ display: 'none' });
setPointedLocation(null);
};
const handleLocationMouseMove = (event: any) => {
const tooltipStyle = {
display: 'block',
top: event.clientY + 10,
left: event.clientX - 100,
};
setTooltipStyle(tooltipStyle);
};
return (
<NoContent size="small" show={false} style={{ height: '240px' }} title={NO_METRIC_DATA}>
<div className="absolute right-0 mr-4 top=0 w-full flex justify-end">
<AvgLabel text="Avg" count={Math.round(metric.data.value)} unit="ms" />
return (
<NoContent size="small" show={false} style={{ height: '240px' }} title={NO_METRIC_DATA}>
<div className="absolute right-0 mr-4 top=0 w-full flex justify-end">
<AvgLabel text="Avg" count={Math.round(data.value)} unit="ms" />
</div>
<Scale colors={Styles.compareColors} />
<div className="map-target"></div>
<div
style={{
height: '234px',
width: '100%',
margin: '0 auto',
display: 'flex'
}}
ref={wrapper}
>
<SVGMap
map={WorldMap}
className={stl.maps}
locationClassName={getLocationClassName}
onLocationMouseOver={handleLocationMouseOver}
onLocationMouseOut={handleLocationMouseOut}
onLocationMouseMove={handleLocationMouseMove}
/>
</div>
<div className={stl.tooltip} style={tooltipStyle}>
{pointedLocation && (
<>
<div>{pointedLocation.name}</div>
<div>
Avg: <strong>{dataMap[pointedLocation.id] ? numberWithCommas(parseInt(dataMap[pointedLocation.id].value)) : 0}</strong>
</div>
<Scale colors={Styles.colors} />
<div className="map-target"></div>
<div
style={{
height: '234px',
width: '100%',
margin: '0 auto',
display: 'flex',
}}
ref={wrapper}
>
<SVGMap
map={WorldMap}
className={stl.maps}
locationClassName={getLocationClassName}
onLocationMouseOver={handleLocationMouseOver}
onLocationMouseOut={handleLocationMouseOut}
onLocationMouseMove={handleLocationMouseMove}
/>
</div>
<div className={stl.tooltip} style={tooltipStyle}>
{pointedLocation && (
<>
<div>{pointedLocation.name}</div>
<div>
Avg: <strong>{dataMap[pointedLocation.id] ? numberWithCommas(parseInt(dataMap[pointedLocation.id].value)) : 0}</strong>
</div>
</>
)}
</div>
</NoContent>
);
</>
)}
</div>
</NoContent>
);
}
export default observer(SpeedIndexByLocation);

View file

@ -6,7 +6,7 @@ const colors = ['#6774E2', '#929ACD', '#3EAAAF', '#565D97', '#8F9F9F', '#376F72'
const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse();
const compareColors = ['#394EFF', '#4D5FFF', '#808DFF', '#B3BBFF', '#E5E8FF'];
const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse();
const customMetricColors = ['#3EAAAF', '#394EFF', '#565D97'];
const customMetricColors = ['#394EFF', '#3EAAAF', '#565D97'];
const colorsPie = colors.concat(["#DDDDDD"]);
const countView = count => {

View file

@ -1,6 +1,6 @@
import React from 'react';
import {Card, Col, Modal, Row, Typography} from "antd";
import {Grid2x2CheckIcon, Plus} from "lucide-react";
import {GalleryVertical, Plus} from "lucide-react";
import NewDashboardModal from "Components/Dashboard/components/DashboardList/NewDashModal";
import {useStore} from "App/mstore";
@ -33,24 +33,22 @@ function AddCardSelectionModal(props: Props) {
open={props.open}
footer={null}
onCancel={props.onClose}
className='addCard'
>
<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>
<div className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3" style={{height: '80px'}} onClick={() => onClick(true)}>
<GalleryVertical style={{fontSize: '24px', color: '#394EFF'}}/>
<Typography.Text strong>Add from library</Typography.Text>
{/*<p>Select from 12 available</p>*/}
</div>
</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>
<div className="flex flex-col items-center justify-center hover:bg-indigo-50 border rounded-lg shadow-sm cursor-pointer gap-3" style={{height: '80px'}} onClick={() => onClick(false)}>
<Plus style={{fontSize: '24px', color: '#394EFF'}}/>
<Typography.Text strong>Create New Card</Typography.Text>
</div>
</Col>
</Row>
</Modal>

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Icon } from 'UI';
import {Input} from 'antd';
import { debounce } from 'App/utils';
import { useStore } from 'App/mstore'
import { observer } from 'mobx-react-lite'
@ -22,11 +23,12 @@ function AlertsSearch() {
return (
<div className="relative">
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
<input
<Input.Search
value={inputValue}
allowClear
name="alertsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title"
className="w-full"
placeholder="Filter by alert title"
onChange={write}
/>
</div>

View file

@ -1,5 +1,7 @@
import React, { useEffect } from 'react';
import { Button, PageTitle, Icon, Link } from 'UI';
import { PageTitle, Icon, Link } from 'UI';
import { Button } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import withPageTitle from 'HOCs/withPageTitle';
import { withSiteId, alertCreate } from 'App/routes';
@ -32,7 +34,14 @@ function AlertsView({ siteId }: IAlertsView) {
<PageTitle title="Alerts" />
</div>
<div className="ml-auto flex items-center">
<Link to={withSiteId(alertCreate(), siteId)}><Button variant="primary">New Alert</Button></Link>
<Link to={withSiteId(alertCreate(), siteId)}>
<Button
type="primary"
icon={<PlusOutlined />}>
Create Alert
</Button>
</Link>
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
<AlertsSearch />
</div>

View file

@ -89,7 +89,7 @@ function CardIssues() {
};
return useObserver(() => (
<div className='my-8 bg-white rounded p-4 border'>
<div className='bg-white rounded p-4 border'>
<div className='flex justify-between'>
<div className='flex items-center'>
<h1 className='font-medium text-2xl'>Issues</h1>

View file

@ -41,7 +41,7 @@ function CardUserList(props: RouteComponentProps<Props>) {
}, [userId]);
return (
<div className="my-8 bg-white rounded p-4 border">
<div className="bg-white rounded p-4 border">
<div className="flex justify-between">
<h1 className="font-medium text-2xl">Returning users between</h1>
<div>

View file

@ -1,6 +1,8 @@
import { useObserver } from 'mobx-react-lite';
import React from 'react';
import { Button, Modal, Form, Icon, Checkbox, Input } from 'UI';
import { Modal, Form, Icon, Checkbox, Input } from 'UI';
import {Button} from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import { useStore } from 'App/mstore'
interface Props {
@ -32,14 +34,13 @@ function DashboardEditModal(props: Props) {
<Modal open={ show } onClose={closeHandler}>
<Modal.Header className="flex items-center justify-between">
<div>{ 'Edit Dashboard' }</div>
<Icon
role="button"
tabIndex="-1"
color="gray-dark"
size="14"
<Button
type='text'
name="close"
onClick={ closeHandler }
icon={<CloseOutlined />}
/>
</Modal.Header>
<Modal.Content>
@ -91,13 +92,13 @@ function DashboardEditModal(props: Props) {
<Modal.Footer>
<div className="-mx-2 px-2">
<Button
variant="primary"
type="primary"
onClick={ onSave }
className="float-left mr-2"
>
Save
</Button>
<Button className="mr-2" onClick={ closeHandler }>{ 'Cancel' }</Button>
<Button type='default' onClick={ closeHandler }>{ 'Cancel' }</Button>
</div>
</Modal.Footer>
</Modal>

View file

@ -1,23 +1,24 @@
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 {Breadcrumb} from 'Shared/Breadcrumb';
import BackButton from '../../../shared/Breadcrumb/BackButton';
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 CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import CreateCard from "Components/Dashboard/components/DashboardList/NewDashModal/CreateCard";
import CreateCardButton from "Components/Dashboard/components/CreateCardButton";
import CreateDashboardButton from 'Components/Dashboard/components/CreateDashboardButton';
import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard';
import CreateCardButton from 'Components/Dashboard/components/CreateCardButton';
interface IProps {
dashboardId: string;
siteId: string;
renderReport?: any;
dashboardId: string;
siteId: string;
renderReport?: any;
}
@ -25,104 +26,109 @@ type Props = IProps & RouteComponentProps;
const MAX_CARDS = 29;
function DashboardHeader(props: Props) {
const {siteId, dashboardId} = props;
const {dashboardStore} = useStore();
const {showModal} = useModal();
const [focusTitle, setFocusedInput] = React.useState(true);
const [showEditModal, setShowEditModal] = React.useState(false);
const period = dashboardStore.period;
const { siteId, dashboardId } = props;
const { dashboardStore } = useStore();
const { showModal } = useModal();
const [focusTitle, setFocusedInput] = React.useState(true);
const [showEditModal, setShowEditModal] = React.useState(false);
const period = dashboardStore.period;
const dashboard: any = dashboardStore.selectedDashboard;
const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS;
const dashboard: any = dashboardStore.selectedDashboard;
const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS;
const onEdit = (isTitle: boolean) => {
dashboardStore.initDashboard(dashboard);
setFocusedInput(isTitle);
setShowEditModal(true);
};
const onEdit = (isTitle: boolean) => {
dashboardStore.initDashboard(dashboard);
setFocusedInput(isTitle);
setShowEditModal(true);
};
const onDelete = async () => {
if (
await confirm({
header: 'Delete Dashboard',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`,
})
) {
dashboardStore.deleteDashboard(dashboard).then(() => {
props.history.push(withSiteId(`/dashboard`, siteId));
});
}
};
return (
<div>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
focusTitle={focusTitle}
/>
<Breadcrumb
items={[
{
label: 'Dashboards',
to: withSiteId('/dashboard', siteId),
},
{label: (dashboard && dashboard.name) || ''},
]}
/>
<div className="flex items-center mb-2 justify-between">
<div className="flex items-center" style={{flex: 3}}>
<PageTitle
title={
// @ts-ignore
<Tooltip delay={100} arrow title="Double click to edit">
{dashboard?.name}
</Tooltip>
}
onDoubleClick={() => onEdit(true)}
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
/>
</div>
<div className="flex items-center gap-2" style={{flex: 1, justifyContent: 'end'}}>
<CreateCardButton disabled={canAddMore} />
<div
className="flex items-center flex-shrink-0 justify-end dashboardDataPeriodSelector"
style={{width: 'fit-content'}}
>
<SelectDateRange
style={{width: '300px'}}
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
isAnt={true}
useButtonStyle={true}
/>
</div>
<div className="flex items-center flex-shrink-0">
<DashboardOptions
editHandler={onEdit}
deleteHandler={onDelete}
renderReport={props.renderReport}
isTitlePresent={!!dashboard?.description}
/>
</div>
</div>
</div>
<div className="pb-4">
{/* @ts-ignore */}
<Tooltip delay={100} arrow title="Double click to edit" className="w-fit !block">
<h2
className="my-2 font-normal w-fit text-disabled-text border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
onDoubleClick={() => onEdit(false)}
>
{/* {dashboard?.description || 'Describe the purpose of this dashboard'} */}
</h2>
</Tooltip>
</div>
const onDelete = async () => {
if (
await confirm({
header: 'Delete Dashboard',
confirmButton: 'Yes, delete',
confirmation: `Are you sure you want to permanently delete this Dashboard?`
})
) {
dashboardStore.deleteDashboard(dashboard).then(() => {
props.history.push(withSiteId(`/dashboard`, siteId));
});
}
};
return (
<div>
<DashboardEditModal
show={showEditModal}
closeHandler={() => setShowEditModal(false)}
focusTitle={focusTitle}
/>
<div className="flex items-center mb-2 justify-between">
<div className="flex items-center" style={{ flex: 3 }}>
<BackButton siteId={siteId} />
{/* <Breadcrumb
items={[
{
label: 'Back',
to: withSiteId('/dashboard', siteId),
},
{label: (dashboard && dashboard.name) || ''},
]}
/> */}
<PageTitle
title={
// @ts-ignore
<Tooltip delay={0} title="Double click to edit" placement="bottom">
{dashboard?.name}
</Tooltip>
}
onDoubleClick={() => onEdit(true)}
className="mr-3 select-none border-b border-b-borderColor-transparent hover:border-dashed hover:border-gray-medium cursor-pointer"
/>
</div>
);
<div className="flex items-center gap-2" style={{ flex: 1, justifyContent: 'end' }}>
<CreateCardButton disabled={canAddMore} />
<div
className="flex items-center flex-shrink-0 justify-end dashboardDataPeriodSelector"
style={{ width: 'fit-content' }}
>
<SelectDateRange
style={{ width: '300px' }}
period={period}
onChange={(period: any) => dashboardStore.setPeriod(period)}
right={true}
isAnt={true}
useButtonStyle={true}
/>
</div>
<div className="flex items-center flex-shrink-0">
<DashboardOptions
editHandler={onEdit}
deleteHandler={onDelete}
renderReport={props.renderReport}
isTitlePresent={!!dashboard?.description}
/>
</div>
</div>
</div>
<div className="pb-4">
{/* @ts-ignore */}
<Tooltip arrow title="Double click to edit" placement="top" className="w-fit !block">
<h2
className="my-2 font-normal w-fit text-disabled-text border-b border-b-borderColor-transparent hover:border-dotted hover:border-gray-medium cursor-pointer"
onDoubleClick={() => onEdit(false)}
>
{/* {dashboard?.description || 'Describe the purpose of this dashboard'} */}
</h2>
</Tooltip>
</div>
</div>
);
}
export default withRouter(withModal(observer(DashboardHeader)));

View file

@ -1,25 +1,29 @@
import {LockOutlined, TeamOutlined} from '@ant-design/icons';
import {Empty, Switch, Table, TableColumnsType, Tag, Tooltip, Typography} from 'antd';
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 {connect} from 'react-redux';
import {withRouter} from 'react-router-dom';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import {checkForRecent} from 'App/date';
import {useStore} from 'App/mstore';
import { checkForRecent } from 'App/date';
import { useStore } from 'App/mstore';
import Dashboard from 'App/mstore/types/dashboard';
import {dashboardSelected, withSiteId} from 'App/routes';
import { dashboardSelected, withSiteId } from 'App/routes';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import CreateDashboardButton from "Components/Dashboard/components/CreateDashboardButton";
import {useHistory} from "react-router";
import { useHistory } from "react-router";
import classNames from 'classnames';
function DashboardList({siteId}: { siteId: string }) {
const {dashboardStore} = useStore();
function DashboardList({ siteId }: { siteId: string }) {
const { dashboardStore } = useStore();
const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.filter.query;
const history = useHistory();
// Define custom width and height for each scenario
const searchImageDimensions = { width: 200, height: 'auto' };
const defaultImageDimensions = { width: 600, height: 'auto' };
const tableConfig: TableColumnsType<Dashboard> = [
{
@ -28,14 +32,6 @@ function DashboardList({siteId}: { siteId: string }) {
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',
@ -45,54 +41,79 @@ function DashboardList({siteId}: { siteId: string }) {
render: (date) => checkForRecent(date, 'LLL dd, yyyy, hh:mm a'),
},
{
title: 'Modified By',
dataIndex: 'updatedBy',
title: 'Owner',
dataIndex: 'owner',
width: '16.67%',
sorter: (a, b) => a.updatedBy.localeCompare(b.updatedBy),
sorter: (a, b) => a.owner?.localeCompare(b.owner),
sortDirections: ['ascend', 'descend'],
},
{
title: (
<div className={'flex items-center justify-between'}>
<div className={'flex items-center justify-start gap-2'}>
<div>Visibility</div>
<Switch checked={!dashboardStore.filter.showMine} onChange={() =>
dashboardStore.updateKey('filter', {
...dashboardStore.filter,
showMine: !dashboardStore.filter.showMine,
})} checkedChildren={'Public'} unCheckedChildren={'Private'}/>
<Tooltip title='Toggle to view your dashboards or all team dashboards.' placement='topRight'>
<Switch checked={!dashboardStore.filter.showMine} onChange={() =>
dashboardStore.updateKey('filter', {
...dashboardStore.filter,
showMine: !dashboardStore.filter.showMine,
})} checkedChildren={'Team'} unCheckedChildren={'Private'} />
</Tooltip>
</div>
),
width: '16.67%',
dataIndex: 'isPublic',
render: (isPublic: boolean) => (
<Tag icon={isPublic ? <TeamOutlined/> : <LockOutlined/>}>
<Tag icon={isPublic ? <TeamOutlined /> : <LockOutlined />}>
{isPublic ? 'Team' : 'Private'}
</Tag>
),
},
];
const emptyDescription = dashboardsSearch !== '' ? (
<div className="text-center">
<div>
<Typography.Text className="my-2 text-xl font-medium">
No search results found.
</Typography.Text>
<div className="mb-2 text-lg text-gray-500 mt-2 leading-normal">
Try adjusting your search criteria or creating a new dashboard.
</div>
</div>
</div>
) : (
<div className="text-center">
<div>
<Typography.Text className="my-2 text-xl font-medium">
Create your first dashboard.
</Typography.Text>
<div className="mb-2 text-lg text-gray-500 mt-2 leading-normal">
Organize your product and technical insights as cards in dashboards to see the bigger picture.
</div>
<div className="my-4">
<CreateDashboardButton />
</div>
</div>
</div>
);
const emptyImage = dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS;
const imageDimensions = dashboardsSearch !== '' ? searchImageDimensions : defaultImageDimensions;
return (
list.length === 0 && !dashboardStore.filter.showMine ? (
<div className='flex justify-center text-center'>
<Empty
image={<AnimatedSVG name={dashboardsSearch !== '' ? ICONS.NO_RESULTS : ICONS.NO_DASHBOARDS} size={600}/>}
imageStyle={{height: 300}}
description={(
<div className="text-center">
<div>
<Typography.Text className="my-2 text-xl font-medium">
Create your first dashboard.
</Typography.Text>
<div className="mb-2 text-lg text-gray-500 mt-2 leading-normal">
Organize your product and technical insights as cards in dashboards to see the bigger picture, <br/>take action and improve user experience.
</div>
<div className="my-4">
<CreateDashboardButton/>
</div>
</div>
</div>
)}
image={<AnimatedSVG name={emptyImage} size={imageDimensions.width} />}
imageStyle={{
width: imageDimensions.width,
height: imageDimensions.height,
margin: 'auto',
padding: '2rem 0'
}}
description={emptyDescription}
/>
</div>
) : (
<Table
dataSource={list}
@ -112,9 +133,10 @@ function DashboardList({siteId}: { siteId: string }) {
history.push(path);
},
})}
/>)
/>
)
);
}
export default connect((state: any) => ({

View file

@ -29,7 +29,7 @@ function DashboardSearch() {
allowClear
name="dashboardsSearch"
className="w-full"
placeholder="Filter by title or description"
placeholder="Filter by dashboard title"
onChange={write}
onSearch={(value) => dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value })}
/>

View file

@ -52,7 +52,7 @@ function CardsLibrary(props: Props) {
<div className={'col-span-' + metric.config.col}
onClick={() => onItemClick(metric.metricId)}>
<LazyLoad>
<Card hoverable
<Card className='border border-transparent hover:border-indigo-50 hover:shadow-sm rounded-lg'
style={{
border: selectedList.includes(metric.metricId) ? '1px solid #1890ff' : '1px solid #f0f0f0',
}}
@ -73,7 +73,7 @@ function CardsLibrary(props: Props) {
// isPreview={true}
metric={metric}
isTemplate={true}
isSaved={true}
isWidget={true}
/>
</Card>
</LazyLoad>

View file

@ -46,7 +46,7 @@ function AreaChartCard(props: Props) {
{/*<div className="flex items-center justify-end mb-3">*/}
{/* <AvgLabel text="Avg" className="ml-3" count={data?.value}/>*/}
{/*</div>*/}
<ResponsiveContainer height={207} width="100%">
<ResponsiveContainer width="100%">
<AreaChart
data={data?.chart}
margin={Styles.chartMargins}

View file

@ -1,70 +1,70 @@
import {GitCommitHorizontal} from 'lucide-react';
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";
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,
title: string;
type: string;
onCard: (card: string) => void;
onClick?: any;
data?: any,
}
function BarChartCard(props: Props) {
return (
<ExCard
{...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}
>
{/*<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>*/}
<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: props.data?.label || '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>
<ResponsiveContainer height={240} width="100%">
<BarChart
data={props.data?.chart}
margin={Styles.chartMargins}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#EEEEEE"/>
<XAxis
{...Styles.xaxis}
dataKey="time"
// interval={21}
/>
<YAxis
{...Styles.yaxis}
tickFormatter={val => Styles.tickFormatter(val)}
label={{...Styles.axisLabelLeft, value: "Number of Errors"}}
allowDecimals={false}
/>
<Legend/>
<Tooltip {...Styles.tooltip} />
<Bar minPointSize={1} name={<span className="float">One</span>}
dataKey="value" stackId="a" fill={Styles.colors[0]}/>
{/*<Bar name={<span className="float">3<sup>rd</sup> Party</span>} dataKey="thirdParty" stackId="a"*/}
{/* fill={Styles.colors[2]}/>*/}
</BarChart>
</ResponsiveContainer>
</ExCard>
);
);
}
export default BarChartCard;

View file

@ -15,7 +15,7 @@ function ExCard({
}) {
return (
<div
className={'rounded-lg overflow-hidden border border-transparent p-4 bg-white hover:border-blue hover:shadow-sm relative'}
className={'rounded-lg overflow-hidden border border-transparent p-4 bg-white hover:shadow-sm relative'}
style={{width: '100%', height: height || 286}}
>
<div className="absolute inset-0 z-10 cursor-pointer" onClick={() => onCard(type)}></div>

View file

@ -4,6 +4,7 @@ import React from 'react';
import { Circle } from '../Count';
import ExCard from '../ExCard';
import ByComponent from 'Components/Dashboard/components/DashboardList/NewDashModal/Examples/SessionsBy/Component';
function ByUrl(props: any) {
const [mode, setMode] = React.useState(0);
@ -47,57 +48,62 @@ function ByUrl(props: any) {
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>
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
// <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>
);
}

View file

@ -1,67 +1,36 @@
import ExCard from '../ExCard'
import React from 'react'
import CardSessionsByList from "Components/Dashboard/Widgets/CardSessionsByList";
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
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>
)
const _rows = rows.map((r) => ({
...r,
name: r.label,
displayName: 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} />
</div>
</ExCard>
);
}
export default ByComponent
export default ByComponent;

View file

@ -0,0 +1,49 @@
import React from 'react';
import ByComponent from './Component';
import { LinkOutlined } from '@ant-design/icons';
function SlowestDomains(props: any) {
const rows = [
{
label: 'res.cloudinary.com',
value: '500',
progress: 75,
icon: <LinkOutlined size={12} />
},
{
label: 'mintbase.vercel.app',
value: '306',
progress: 60,
icon: <LinkOutlined size={12} />
},
{
label: 'downloads.intercomcdn.com',
value: '198',
progress: 30,
icon: <LinkOutlined size={12} />
},
{
label: 'static.intercomassets.com',
value: '47',
progress: 15,
icon: <LinkOutlined size={12} />
},
{
label: 'mozbar.moz.com',
value: '5',
progress: 5,
icon: <LinkOutlined size={12} />
}
];
const lineWidth = 200;
return (
<ByComponent
{...props}
rows={rows}
lineWidth={lineWidth}
/>
);
}
export default SlowestDomains;

View file

@ -0,0 +1,38 @@
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';
import SessionsPerBrowser from 'Components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser';
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
}
function SessionsPerBrowserExample(props: Props) {
const data = {
chart: [
{
'browser': 'Chrome',
'count': 1524,
'126.0.0': 1157,
'125.0.0': 224
},
{
'browser': 'Edge',
'count': 159,
'126.0.0': 145
}
]
};
return (
<ExCard
{...props}
>
<SessionsPerBrowser data={data} />
</ExCard>
);
}
export default SessionsPerBrowserExample;

View file

@ -4,6 +4,7 @@ import React from 'react';
import { Circle } from './Count';
import ExCard from './ExCard';
// TODO - delete this
function SlowestDomain(props: any) {
const rows = [
{

View file

@ -0,0 +1,94 @@
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';
import SessionsPerBrowser from 'Components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser';
import SpeedIndexByLocation from 'Components/Dashboard/Widgets/PredefinedWidgets/SpeedIndexByLocation';
interface Props {
title: string;
type: string;
onCard: (card: string) => void;
}
function SpeedIndexByLocationExample(props: Props) {
const data = {
'value': 1480,
'chart': [
{
'userCountry': 'AT',
'value': 415
},
{
'userCountry': 'PL',
'value': 433.1666666666667
},
{
'userCountry': 'FR',
'value': 502
},
{
'userCountry': 'IT',
'value': 540.4117647058823
},
{
'userCountry': 'TH',
'value': 662.0
},
{
'userCountry': 'ES',
'value': 740.5454545454545
},
{
'userCountry': 'SG',
'value': 889.6666666666666
},
{
'userCountry': 'TW',
'value': 1008.0
},
{
'userCountry': 'HU',
'value': 1027.0
},
{
'userCountry': 'DE',
'value': 1054.4583333333333
},
{
'userCountry': 'BE',
'value': 1126.0
},
{
'userCountry': 'TR',
'value': 1174.0
},
{
'userCountry': 'US',
'value': 1273.3015873015872
},
{
'userCountry': 'GB',
'value': 1353.8095238095239
},
{
'userCountry': 'VN',
'value': 1473.8181818181818
},
{
'userCountry': 'HK',
'value': 1654.6666666666667
},
],
'unit': 'ms'
};
return (
<ExCard
{...props}
>
<SpeedIndexByLocation data={data} />
</ExCard>
);
}
export default SpeedIndexByLocationExample;

View file

@ -1,60 +1,70 @@
import React, {useEffect} from 'react';
import {Modal} from 'antd';
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";
import CreateCard from 'Components/Dashboard/components/DashboardList/NewDashModal/CreateCard';
import colors from 'tailwindcss/colors';
import { connect } from 'react-redux';
interface NewDashboardModalProps {
onClose: () => void;
open: boolean;
isAddingFromLibrary?: boolean;
onClose: () => void;
open: boolean;
isAddingFromLibrary?: boolean;
isEnterprise?: boolean;
}
const NewDashboardModal: React.FC<NewDashboardModalProps> = ({
onClose,
open,
isAddingFromLibrary = false,
onClose,
open,
isAddingFromLibrary = false,
isEnterprise = false
}) => {
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
const [step, setStep] = React.useState<number>(0);
const [selectedCategory, setSelectedCategory] = React.useState<string>('product-analytics');
useEffect(() => {
return () => {
setStep(0);
}
}, [open]);
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>
</>
)
;
return (
<>
<Modal
open={open}
onCancel={onClose}
width={900}
destroyOnClose={true}
footer={null}
closeIcon={false}
styles={{
content: {
backgroundColor: colors.gray[100]
}
}}
centered={true}
>
<div className="flex flex-col gap-4" style={{
height: 'calc(100vh - 100px)',
overflowY: 'auto',
overflowX: 'hidden'
}}>
{step === 0 && <SelectCard onClose={onClose}
selected={selectedCategory}
setSelectedCategory={setSelectedCategory}
onCard={() => setStep(step + 1)}
isLibrary={isAddingFromLibrary}
isEnterprise={isEnterprise} />}
{step === 1 && <CreateCard onBack={() => setStep(0)} />}
</div>
</Modal>
</>
)
;
};
export default NewDashboardModal;
const mapStateToProps = (state: any) => ({
isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee' ||
state.getIn(['user', 'account', 'edition']) === 'msaas'
});
export default connect(mapStateToProps)(NewDashboardModal);

View file

@ -1,137 +1,139 @@
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 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";
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>>;
onClose: (refresh?: boolean) => void;
onCard: () => void;
isLibrary?: boolean;
selected?: string;
setSelectedCategory?: React.Dispatch<React.SetStateAction<string>>;
isEnterprise?: boolean;
}
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 { onCard, isLibrary = false, selected, setSelectedCategory, isEnterprise } = 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 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 cardData: any = {
metricType: selectedCard.cardType,
name: selectedCard.title,
metricOf: selectedCard.metricOf
};
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]);
}
if (selectedCard.cardType === FUNNEL) {
cardData.series = [];
cardData.series.filter = [];
}
const onAddSelected = () => {
const dashboard = dashboardStore.getDashboard(dashboardId);
dashboardStore.addWidgetToDashboard(dashboard!, selectedCards).finally(() => {
dashboardStore.fetch(dashboardId);
props.onClose(true);
});
metricStore.merge(cardData);
metricStore.instance.resetDefaults();
onCard();
};
const cardItems = useMemo(() => {
return CARD_LIST.filter((card) => card.category === selected && (!card.isEnterprise || card.isEnterprise && isEnterprise))
.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]);
}
};
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>
) : ''}
const onAddSelected = () => {
const dashboard = dashboardStore.getDashboard(dashboardId);
dashboardStore.addWidgetToDashboard(dashboard!, selectedCards).finally(() => {
dashboardStore.fetch(dashboardId);
props.onClose(true);
});
};
<Input.Search
placeholder="Search"
onChange={(value) => setLibraryQuery(value.target.value)}
style={{width: 200}}
/>
</Space>
)}
</Space>
return (
<>
<Space className="items-center justify-between">
<div className="text-xl leading-4 font-medium">
{dashboardId ? (isLibrary ? 'Your Library' : '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>
) : ''}
{!isLibrary && <CategorySelector setSelected={setSelectedCategory} selected={selected}/>}
<Input.Search
placeholder="Find by card title"
onChange={(value) => setLibraryQuery(value.target.value)}
style={{ width: 200 }}
/>
</Space>
)}
</Space>
{isLibrary ?
<CardsLibrary query={libraryQuery}
selectedList={selectedCards}
category={selected}
onCard={onCardClick}/> :
<ExampleCardsGrid items={cardItems}/>}
</>
);
{!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;
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'
/>
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[];
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>
const ExampleCardsGrid: React.FC<ExampleCardsGridProps> = ({ items }) => (
<div
className="w-full grid grid-cols-4 gap-4 overflow-scroll"
style={{ maxHeight: 'calc(100vh - 100px)' }}
>
{items}
</div>
);
export default SelectCard;

View file

@ -1,10 +1,9 @@
import React, { useEffect } from 'react';
import WidgetWrapper from '../WidgetWrapper';
import { useObserver } from 'mobx-react-lite';
import { Icon } from 'UI';
import { Icon, Loader } from 'UI';
import cn from 'classnames';
import { useStore } from 'App/mstore';
import { Loader } from 'UI';
interface IWiProps {
category: Record<string, any>

View file

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

View file

@ -23,8 +23,8 @@ function AddStepButton({series, excludeFilterKeys}: Props) {
onFilterClick={onAddFilter}
excludeFilterKeys={excludeFilterKeys}
>
<Button type="link" className='border-none hover:bg-blue-50' icon={<PlusIcon size={16}/>} size="small">
ADD STEP
<Button type="text" className='border-none text-indigo-600 hover:text-indigo-600 align-bottom ms-2' icon={<PlusIcon size={16}/>} size="small">
<span className='font-medium'>Add Step</span>
</Button>
</FilterSelection>
);

View file

@ -6,7 +6,7 @@ import React from 'react';
import FilterItem from 'Shared/Filters/FilterItem';
import cn from 'classnames';
import { Button } from 'UI';
import { Button } from 'antd';
interface Props {
filter: Filter;
@ -47,7 +47,7 @@ function ExcludeFilters(props: Props) {
))}
</div>
) : (
<Button variant="text-primary" onClick={addPageFilter}>
<Button type="link" onClick={addPageFilter}>
Add Exclusion
</Button>
)}

View file

@ -85,7 +85,7 @@ function FilterSeries(props: Props) {
},
canDelete,
hideHeader = false,
emptyMessage = 'Add user event or filter to define the series by clicking Add Step.',
emptyMessage = 'Add an event or filter step to define the series.',
supportsEmpty = true,
excludeFilterKeys = [],
canExclude = false,

View file

@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Icon } from 'UI';
import {Input, Tooltip} from 'antd';
interface Props {
name: string;
@ -35,21 +36,26 @@ function SeriesName(props: Props) {
return (
<div className="flex items-center">
{ editing ? (
<input
<Input
ref={ ref }
name="name"
className="fluid border-0 -mx-2 px-2 h-8"
value={name}
// readOnly={!editing}
onChange={write}
onBlur={onBlur}
onFocus={() => setEditing(true)}
className='bg-white'
/>
) : (
<div className="text-base h-8 flex items-center border-transparent">{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }</div>
)}
<div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div>
<div className="ml-3 cursor-pointer " onClick={() => setEditing(true)}>
<Tooltip title='Rename' placement='bottom'>
<Icon name="pencil" size="14" />
</Tooltip>
</div>
</div>
);
}

View file

@ -57,11 +57,11 @@ function FunnelIssues() {
}, [stages.length, drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage]);
return useObserver(() => (
<div className="my-8 bg-white rounded p-4 border">
<div className="bg-white rounded-lg mt-4 p-4 border">
<div className="flex">
<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">
<div className="my-6 flex justify-between items-center">
<FunnelIssuesDropdown />
<div className="flex-shrink-0">
<FunnelIssuesSort />

View file

@ -2,6 +2,8 @@ import React, { useEffect } from 'react';
import Select from 'Shared/Select'
import { components } from 'react-select';
import { Icon } from 'UI';
import { Button } from 'antd';
import { FunnelPlotOutlined } from '@ant-design/icons';
import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters';
import { useStore } from 'App/mstore';
import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv';
@ -59,7 +61,7 @@ function FunnelIssuesDropdown() {
}
return (
<div className="flex items-start">
<div className="flex items-center gap-2">
<Select
menuIsOpen={isOpen}
// onMenuOpen={() => setIsOpen(true)}
@ -69,8 +71,8 @@ function FunnelIssuesDropdown() {
styles={{
control: (provided: any) => ({
...provided,
border: 'none',
boxShadow: 'none',
border:'transparent',
borderColor:'transparent',
backgroundColor: 'transparent',
minHeight: 'unset',
}),
@ -92,14 +94,16 @@ function FunnelIssuesDropdown() {
>
<components.Control {...props}>
{ children }
<button
<Button
id="dd-button"
className="px-2 py-1 bg-white rounded-2xl border border-teal border-dashed color-teal flex items-center hover:bg-active-blue select-none"
className="px-2 select-none gap-0"
onClick={() => setIsOpen(!isOpen)}
icon={<FunnelPlotOutlined />}
type='primary' ghost
size='small'
>
<Icon name="funnel" size={16} color="teal" className="pointer-events-none" />
<span className="ml-2 pointer-events-none">Issues</span>
</button>
</Button>
</components.Control>
</OutsideClickDetectingDiv>

View file

@ -1,5 +1,7 @@
import React from 'react';
import { Icon } from 'UI';
import{Tag} from 'antd';
import {CloseOutlined} from '@ant-design/icons'
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
@ -14,12 +16,15 @@ function FunnelIssuesSelectedFilters(props: Props) {
return (
<div className="flex items-center flex-wrap">
{issuesFilter.map((option, index) => (
<div key={index} className="transition-all ml-2 mb-2 flex items-center border rounded-2xl bg-white select-none overflow-hidden">
<span className="pl-3 color-gray-dark">{option.label}</span>
<button className="ml-1 hover:bg-active-blue px-2 py-2" onClick={() => removeSelectedValue(option.value)}>
<Icon name="close"/>
</button>
</div>
<Tag
bordered = {false}
key={index}
closable
onClose={() => removeSelectedValue(option.value)}
className="select-none rounded-lg text-base gap-1 bg-indigo-50 flex items-center"
>
{option.label}
</Tag>
))}
</div>
);

View file

@ -1,6 +1,7 @@
import { useStore } from 'App/mstore';
import React from 'react';
import Select from 'Shared/Select';
// import Select from 'Shared/Select';
import { Select } from 'antd';
const sortOptions = [
{ value: 'afectedUsers-desc', label: 'Affected Users (High)' },
@ -24,12 +25,19 @@ function FunnelIssuesSort(props: Props) {
return (
<div>
<Select
{/* <Select
plain
defaultValue={sortOptions[0].value}
options={sortOptions}
alignRight={true}
onChange={onSortChange}
/> */}
<Select
className='w-60 border-0 rounded-lg'
defaultValue={sortOptions[0].value}
options={sortOptions}
onChange={onSortChange}
size='small'
/>
</div>
);

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Icon, Checkbox, Tooltip, confirm, Modal } from 'UI';
import { Dropdown, Button, Input } from 'antd';
import { Icon, Checkbox, confirm, Modal } from 'UI';
import { Dropdown, Button, Input, Tooltip } from 'antd';
import { checkForRecent } from 'App/date';
import { withRouter, RouteComponentProps } from 'react-router-dom';
import { withSiteId } from 'App/routes';
@ -26,7 +26,7 @@ function MetricTypeIcon({ type }: any) {
}, [type]);
return (
<Tooltip delay={0} title={<div className="capitalize">{card.title}</div>}>
<Tooltip title={<div className="capitalize">{card.title}</div>}>
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
{card.icon && <Icon name={card.icon} size="16" color="tealx" />}
</div>

View file

@ -1,6 +1,7 @@
import React from 'react';
import {PageTitle, Button, Toggler, Icon} from "UI";
import {Segmented} from 'antd';
import {PageTitle, Toggler, Icon} from "UI";
import {Segmented, Button} from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import MetricsSearch from '../MetricsSearch';
import Select from 'Shared/Select';
import {useStore} from 'App/mstore';
@ -24,10 +25,11 @@ function MetricViewHeader({siteId}: { siteId: string }) {
<PageTitle title='Cards' className=''/>
</div>
<div className='ml-auto flex items-center'>
<Button variant='primary'
<Button type='primary'
// onClick={() => showModal(<AddCardModal siteId={siteId}/>, {right: true})}
onClick={() => setShowAddCardModal(true)}
>New Card</Button>
icon={<PlusOutlined />}
>Create Card</Button>
<div className='ml-4 w-1/4' style={{minWidth: 300}}>
<MetricsSearch/>
</div>
@ -119,7 +121,8 @@ function ListViewToggler() {
const listView = useObserver(() => metricStore.listView);
return (
<div className='flex items-center'>
<Segmented
<Segmented
size='small'
options={[
{
label: <div className={'flex items-center gap-2'}>

View file

@ -86,7 +86,7 @@ function MetricsList({
)}
<div className='w-full flex items-center justify-between py-4 px-6 border-t'>
<div className='text-disabled-text'>
<div className=''>
Showing{' '}
<span className='font-semibold'>{Math.min(cards.length, metricStore.pageSize)}</span> out
of <span className='font-semibold'>{cards.length}</span> cards

View file

@ -2,6 +2,7 @@ import { useObserver } from 'mobx-react-lite';
import React, { useEffect, useState } from 'react';
import { useStore } from 'App/mstore';
import { Icon } from 'UI';
import {Input} from 'antd';
import { debounce } from 'App/utils';
let debounceUpdate: any = () => {};
@ -22,12 +23,12 @@ function MetricsSearch() {
return useObserver(() => (
<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="metricsSearch"
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
placeholder="Filter by title and owner"
className="w-full"
placeholder="Filter by title or owner"
onChange={write}
/>
</div>

View file

@ -253,8 +253,8 @@ function WidgetChart(props: Props) {
return <div>Unknown metric type</div>;
};
return (
<Loader loading={loading} style={{height: `${isOverviewWidget ? 100 : 240}px`}}>
<div style={{minHeight: isOverviewWidget ? 100 : 240}}>{renderChart()}</div>
<Loader loading={loading} style={{height: `240px`}}>
<div style={{minHeight: 240}}>{renderChart()}</div>
</Loader>
);
}

View file

@ -27,6 +27,8 @@ function WidgetDateRange({
period={period}
onChange={onChangePeriod}
right={true}
isAnt={true}
useButtonStyle={true}
/>
</Space>
);

View file

@ -5,7 +5,7 @@ import {metricOf, issueOptions, issueCategories} from 'App/constants/filterOptio
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 {Card, Input, Space, Button, Segmented, Alert} from 'antd';
import {AudioWaveform} from "lucide-react";
import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select';
@ -28,16 +28,13 @@ const AIInput = ({value, setValue, placeholder, onEnter}) => (
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.target.value)}
className='w-full mb-2'
className='w-full mb-2 bg-white'
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>
<Alert message="Drilldown or filtering isn't supported on this legacy card." type='warning' showIcon closable className='border-transparent rounded-lg' />
);
const MetricTabs = ({metric, writeOption}: any) => {
@ -185,7 +182,7 @@ const SeriesList = observer(() => {
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.'
: 'Add an event or filter step to define the series.'
}
/>
</div>

View file

@ -4,6 +4,7 @@ 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 {Input, Alert} from 'antd'
import FilterSeries from '../FilterSeries';
import Select from 'Shared/Select';
import {withSiteId, dashboardMetricDetails, metricDetails} from 'App/routes';
@ -26,7 +27,6 @@ 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'
interface Props {
history: any;
@ -261,12 +261,7 @@ function WidgetForm(props: Props) {
)}
{isPredefined && (
<div className='flex items-center my-6 justify-center'>
<Icon name='info-circle' size='18' color='gray-medium'/>
<div className='ml-2'>
Filtering and drill-downs will be supported soon for this card type.
</div>
</div>
<Alert message="Drilldown or filtering isn't supported on this legacy card." type='warning' showIcon closable className='border-transparent rounded-lg' />
)}
{testingKey ? <Input
placeholder="AI Query"
@ -323,7 +318,7 @@ function WidgetForm(props: Props) {
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.'
: 'Add an event or filter step to define the series.'
}
/>
</div>

View file

@ -1,179 +1,172 @@
import React from 'react';
import {Card, Space, Typography, Button} from "antd";
import {useStore} from "App/mstore";
import {eventKeys} from "Types/filter/newFilter";
import { Card, Space, Typography, Button, Alert } 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";
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 { 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);
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>
);
return isPredefined ? <PredefinedMessage /> : (
<Space direction="vertical" className="w-full">
<AdditionalFilters />
{!hasFilters && (<DefineSteps metric={metric} excludeFilterKeys={excludeFilterKeys} />)}
{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>
);
function DefineSteps({ metric, excludeFilterKeys }: any) {
return (
<Card
styles={{
body: { padding: '0' },
cover: {}
}}
>
<div className="px-4 py-2 rounded-lg shadow-sm flex items-center">
<Typography.Text strong>Filter</Typography.Text>
<AddStepButton excludeFilterKeys={excludeFilterKeys} series={metric.series[0]} />
</div>
</Card>
);
}
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 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 isSingleSeries = isTable || isFunnel || isClickMap || isInsights || isRetention;
// const onAddFilter = (filter: any) => {
// metric.series[0].filter.addFilter(filter);
// metric.updateKey('hasChanged', true)
// }
// 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>
))
}
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 an event or filter step to define the series.'
}
expandable={isSingleSeries}
/>
</div>
))
}
{!isSingleSeries && canAddSeries && (
<Card styles={{body: {padding: '4px'}}}>
<Button
type='link'
onClick={() => {
metric.addSeries();
{!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>
)}
</>
);
})
}}
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 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;
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>
)
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>
<Alert message="Drilldown or filtering isn't supported on this legacy card." type="warning" showIcon closable
className="border-transparent rounded-lg" />
);

View file

@ -1,5 +1,6 @@
import React, { useState, useRef, useEffect } from 'react';
import { Icon, Tooltip } from 'UI';
import { Input } from 'antd';
import cn from 'classnames';
interface Props {
@ -53,10 +54,9 @@ function WidgetName(props: Props) {
return (
<div className="flex items-center">
{ editing ? (
<input
<Input
ref={ ref }
name="name"
className="rounded fluid border-0 -mx-2 px-2 h-8"
value={name}
onChange={write}
onBlur={() => onBlur()}
@ -80,7 +80,11 @@ function WidgetName(props: Props) {
</Tooltip>
)}
{ canEdit && <div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}><Icon name="pencil" size="14" /></div> }
{ canEdit && <div className="ml-3 cursor-pointer" onClick={() => setEditing(true)}>
<Tooltip title='Rename' placement='bottom'>
<Icon name="pencil" size="16" />
</Tooltip>
</div> }
</div>
);
}

View file

@ -60,7 +60,7 @@ function WidgetPredefinedChart(props: Props) {
case FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION:
return <ResponseTimeDistribution data={data} metric={metric} />
case FilterKey.SPEED_LOCATION:
return <SpeedIndexByLocation metric={metric} />
return <SpeedIndexByLocation data={data} />
case FilterKey.CPU:
return <CPULoad data={data} metric={metric} />
case FilterKey.CRASHES:
@ -76,9 +76,9 @@ function WidgetPredefinedChart(props: Props) {
case FilterKey.RESOURCES_VS_VISUALLY_COMPLETE:
return <ResourceLoadedVsVisuallyComplete data={data} metric={metric} />
case FilterKey.SESSIONS_PER_BROWSER:
return <SessionsPerBrowser data={data} metric={metric} />
return <SessionsPerBrowser data={data} />
case FilterKey.SLOWEST_DOMAINS:
return <SlowestDomains data={data} metric={metric} />
return <SlowestDomains data={data} />
case FilterKey.TIME_TO_RENDER:
return <TimeToRender data={data} metric={metric} />

View file

@ -1,65 +1,137 @@
import { Space, Switch } from 'antd';
import cn from 'classnames';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { CLICKMAP, USER_PATH } from 'App/constants/card';
import { useStore } from 'App/mstore';
import ClickMapRagePicker from 'Components/Dashboard/components/ClickMapRagePicker';
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 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 AddToDashboardButton from "Components/Dashboard/components/AddToDashboardButton";
interface Props {
className?: string;
name: string;
isEditing?: boolean;
className?: string;
name: string;
isEditing?: boolean;
}
function WidgetPreview(props: Props) {
const { className = '' } = props;
const { metricStore, dashboardStore } = useStore();
const metric: any = metricStore.instance;
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});
// }
return (
<>
<div
className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}
>
<div className="flex items-center justify-between px-4 pt-2">
<h2 className="text-xl">{props.name}</h2>
<div className="flex items-center">
{metric.metricType === USER_PATH && (
<a
href="#"
onClick={(e) => {
e.preventDefault();
metric.update({ hideExcess: !metric.hideExcess });
}}
>
<Space>
<Switch checked={metric.hideExcess} size="small" />
<span className="mr-4 color-gray-medium">
Hide Minor Paths
</span>
</Space>
</a>
)}
return (
<>
<div className={cn(className, 'bg-white rounded-xl border shadow-sm mt-0')}>
<div className="flex items-center justify-between px-4 pt-2">
<h2 className="text-xl">
{props.name}
</h2>
<div className="flex items-center">
{metric.metricType === USER_PATH && (
<a
href="#"
onClick={(e) => {
e.preventDefault();
metric.update({hideExcess: !metric.hideExcess});
}}
>
<Space>
<Switch
checked={metric.hideExcess}
size="small"
/>
<span className="mr-4 color-gray-medium">Hide Minor Paths</span>
</Space>
</a>
)}
<div className="mx-4" />
{metric.metricType === CLICKMAP ? <ClickMapRagePicker /> : null}
</div>
</div>
<div className="pt-0">
<WidgetWrapper
widget={metric}
isPreview={true}
// isSaved={metric.exists()}
hideName
/>
</div>
</div>
</>
);
{/*{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"*/}
{/* />*/}
{/* </>*/}
{/*)}*/}
{/*{isRetention && (*/}
{/* <>*/}
{/* <span className="mr-4 color-gray-medium">Visualization</span>*/}
{/* <SegmentSelection*/}
{/* name="viewType"*/}
{/* className="my-3"*/}
{/* primary={true}*/}
{/* size="small"*/}
{/* onSelect={ changeViewType }*/}
{/* value={{ value: metric.viewType }}*/}
{/* list={[*/}
{/* { value: 'trend', name: 'Trend', icon: 'graph-up-arrow' },*/}
{/* { value: 'cohort', name: 'Cohort', icon: 'dice-3' },*/}
{/* ]}*/}
{/* disabledMessage="Chart view is not supported"*/}
{/* />*/}
{/*</>*/}
{/*)}*/}
<div className="mx-4"/>
{metric.metricType === CLICKMAP ? (
<ClickMapRagePicker/>
) : null}
{/* add to dashboard */}
{/*{metric.exists() && (*/}
{/* <AddToDashboardButton metricId={metric.metricId}/>*/}
{/*)}*/}
</div>
</div>
<div className="pt-0">
<WidgetWrapper widget={metric} isPreview={true} isWidget={false} hideName/>
</div>
</div>
</>
);
}
export default observer(WidgetPreview);

View file

@ -115,7 +115,7 @@ function WidgetSessions(props: Props) {
};
return (
<div className={cn(className, 'bg-white p-3 pb-0 rounded border')}>
<div className={cn(className, 'bg-white p-3 pb-0 rounded-lg shadow-sm border mt-3')}>
<div className='flex items-center justify-between'>
<div className='flex items-baseline'>
<h2 className='text-xl'>{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}</h2>

View file

@ -1,7 +1,7 @@
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 {Button, Dropdown, MenuProps, message, Modal} from "antd";
import {BellIcon, EllipsisVertical, TrashIcon} from "lucide-react";
import {toast} from "react-toastify";
import React from "react";
@ -36,8 +36,7 @@ const CardViewMenu = () => {
},
{
key: 'remove',
danger: true,
label: 'Remove',
label: 'Delete',
icon: <TrashIcon size={16}/>,
onClick: () => {
Modal.confirm({

View file

@ -1,183 +1,174 @@
import { FilterKey } from 'Types/filter/filterType';
import { Space } from 'antd';
import { useObserver } from 'mobx-react-lite';
import React, { useState } from 'react';
import { Prompt, useHistory } from 'react-router';
import {
CLICKMAP,
FUNNEL,
INSIGHTS,
RETENTION,
TABLE,
TIMESERIES,
USER_PATH,
} from 'App/constants/card';
import { useStore } from 'App/mstore';
import Widget from 'App/mstore/types/widget';
import { dashboardMetricDetails, metricDetails, withSiteId } from 'App/routes';
import WidgetFormNew from 'Components/Dashboard/components/WidgetForm/WidgetFormNew';
import { renderClickmapThumbnail } from 'Components/Dashboard/components/WidgetForm/renderMap';
import WidgetViewHeader from 'Components/Dashboard/components/WidgetView/WidgetViewHeader';
import { Icon, Loader, NoContent } from 'UI';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import Breadcrumb from 'Shared/Breadcrumb';
import CardIssues from '../CardIssues';
import CardUserList from '../CardUserList/CardUserList';
import FunnelIssues from '../Funnels/FunnelIssues/FunnelIssues';
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 {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, useHistory} from 'react-router';
import AnimatedSVG, {ICONS} from 'Shared/AnimatedSVG/AnimatedSVG';
import {
TIMESERIES,
TABLE,
CLICKMAP,
FUNNEL,
INSIGHTS,
USER_PATH,
RETENTION,
} 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;
history: any;
match: any;
siteId: any;
}
function WidgetView(props: Props) {
const {
match: {
params: { siteId, dashboardId, metricId },
},
} = props;
// 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;
const {
match: {
params: {siteId, dashboardId, metricId},
},
} = props;
// 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.response.status === 404 || e.response.status === 422) {
setMetricNotFound(true);
React.useEffect(() => {
if (metricId && metricId !== 'create') {
metricStore.fetch(metricId, dashboardStore.period).catch((e) => {
if (e.response.status === 404 || e.response.status === 422) {
setMetricNotFound(true);
}
});
} else {
metricStore.init();
}
});
} else {
metricStore.init();
}
}, []);
}, []);
// const onBackHandler = () => {
// props.history.goBack();
// };
//
// const openEdit = () => {
// if (expanded) return;
// setExpanded(true);
// };
// 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 undoChanges = () => {
const w = new Widget();
metricStore.merge(w.fromJson(initialInstance), false);
};
const onSave = async () => {
const wasCreating = !widget.exists();
if (isClickMap) {
try {
widget.sessionId = widget.data.sessionId;
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)
);
}
}
};
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(() => (
<Loader loading={loading}>
<Prompt
when={hasChanged}
message={(location: any) => {
if (
location.pathname.includes('/metrics/') ||
location.pathname.includes('/metric/')
) {
return true;
}
return 'You have unsaved changes. Are you sure you want to leave?';
}}
/>
return useObserver(() => (
<Loader loading={loading}>
<Prompt
when={hasChanged}
message={(location: any) => {
if (location.pathname.includes('/metrics/') || location.pathname.includes('/metric/')) {
return true;
}
return 'You have unsaved changes. Are you sure you want to leave?';
}}
/>
<div style={{ maxWidth: '1360px', margin: 'auto' }}>
<Breadcrumb
items={[
{
label: dashboardName ? dashboardName : 'Cards',
to: dashboardId
? withSiteId('/dashboard/' + dashboardId, siteId)
: withSiteId('/metrics', siteId),
},
{ label: widget.name },
]}
/>
<NoContent
show={metricNotFound}
title={
<div className="flex flex-col items-center justify-between">
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100} />
<div className="mt-4">Metric not found!</div>
<div style={{maxWidth: '1360px', margin: 'auto'}}>
<Breadcrumb
items={[
{
label: dashboardName ? dashboardName : 'Cards',
to: dashboardId ? withSiteId('/dashboard/' + dashboardId, siteId) : withSiteId('/metrics', siteId),
},
{label: widget.name},
]}
/>
<NoContent
show={metricNotFound}
title={
<div className="flex flex-col items-center justify-between">
<AnimatedSVG name={ICONS.EMPTY_STATE} size={100}/>
<div className="mt-4">Metric not found!</div>
</div>
}
>
<div className="w-full">
<div className='my-3'>
<WidgetViewHeader onSave={onSave} undoChanges={undoChanges}/>
</div>
<div className='my-3'>
<WidgetFormNew/>
</div>
{/*<div className="bg-white rounded border mt-3">*/}
{/* <WidgetForm expanded={expanded} onDelete={onBackHandler} {...props} />*/}
{/*</div>*/}
<div className='my-3'>
<WidgetPreview name={widget.name} isEditing={expanded}/>
{widget.metricOf !== FilterKey.SESSIONS && widget.metricOf !== FilterKey.ERRORS && (
<>
{(widget.metricType === TABLE || widget.metricType === TIMESERIES || widget.metricType === CLICKMAP || widget.metricType === INSIGHTS) &&
<WidgetSessions/>}
{widget.metricType === FUNNEL && <FunnelIssues/>}
</>
)}
{widget.metricType === USER_PATH && <CardIssues/>}
{widget.metricType === RETENTION && <CardUserList/>}
</div>
</div>
</NoContent>
</div>
}
>
<Space direction="vertical" size={20} className="w-full">
<WidgetViewHeader onSave={onSave} undoChanges={undoChanges} />
<WidgetFormNew />
<WidgetPreview name={widget.name} isEditing={expanded} />
{widget.metricOf !== FilterKey.SESSIONS &&
widget.metricOf !== FilterKey.ERRORS && (
<>
{(widget.metricType === TABLE ||
widget.metricType === TIMESERIES ||
widget.metricType === CLICKMAP ||
widget.metricType === INSIGHTS) && <WidgetSessions />}
{widget.metricType === FUNNEL && <FunnelIssues />}
</>
)}
{widget.metricType === USER_PATH && <CardIssues />}
{widget.metricType === RETENTION && <CardUserList />}
</Space>
</NoContent>
</div>
</Loader>
));
</Loader>
));
}
export default WidgetView;

View file

@ -26,7 +26,8 @@ function WidgetViewHeader({onClick, onSave, undoChanges}: Props) {
<h1 className="mb-0 text-2xl mr-4 min-w-fit">
<WidgetName name={widget.name}
onUpdate={(name) => metricStore.merge({name})}
canEdit={true}/>
canEdit={true}
/>
</h1>
<Space>
<WidgetDateRange label=""/>

View file

@ -21,7 +21,7 @@ function CardMenu({card}: any) {
},
{
key: 'hide',
label: 'Hide',
label: 'Remove',
icon: <EyeOffIcon size={16}/>,
},
];

View file

@ -27,7 +27,7 @@ interface Props {
active?: boolean;
history?: any;
onClick?: () => void;
isSaved?: boolean;
isWidget?: boolean;
hideName?: boolean;
grid?: string;
isGridView?: boolean;
@ -36,7 +36,7 @@ interface Props {
function WidgetWrapperNew(props: Props & RouteComponentProps) {
const {dashboardStore} = useStore();
const {
isSaved = false,
isWidget = false,
active = false,
index = 0,
moveListItem = null,
@ -75,7 +75,7 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
});
const onChartClick = () => {
if (!isSaved || isPredefined) return;
// if (!isWidget || isPredefined) return;
props.history.push(
withSiteId(dashboardMetricDetails(dashboard?.dashboardId, widget.metricId), siteId)
);
@ -86,16 +86,16 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
const addOverlay =
isTemplate ||
(!isPredefined &&
isSaved &&
isWidget &&
widget.metricOf !== FilterKey.ERRORS &&
widget.metricOf !== FilterKey.SESSIONS);
return (
<Card
className={cn(
'relative group',
'relative group rounded-lg',
'col-span-' + widget.config.col,
{'hover:shadow': !isTemplate && isSaved},
{'hover:shadow-sm': !isTemplate && isWidget},
)}
style={{
userSelect: 'none',
@ -107,7 +107,7 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
onClick={props.onClick ? props.onClick : () => null}
id={`widget-${widget.widgetId}`}
title={!props.hideName ? widget.name : null}
extra={isSaved ? [
extra={isWidget ? [
<div className="flex items-center" id="no-print">
{!isPredefined && isTimeSeries && !isGridView && (
<AlertButton seriesId={widget.series[0] && widget.series[0].seriesId}/>
@ -131,7 +131,7 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
},
}}
>
{!isTemplate && isSaved && isPredefined && (
{!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')}>
@ -148,7 +148,7 @@ function WidgetWrapperNew(props: Props & RouteComponentProps) {
isPreview={isPreview}
metric={widget}
isTemplate={isTemplate}
isSaved={isSaved}
isWidget={isWidget}
/>
</div>
</LazyLoad>

View file

@ -1,6 +1,7 @@
import Copyright from 'Shared/Copyright';
import React from 'react';
import { Form, Input, Loader, Button, Link, Icon, Message } from 'UI';
import { Form, Input, Loader, Link, Icon, Message } from 'UI';
import {Button} from 'antd';
import { login as loginRoute } from 'App/routes';
import { connect } from 'react-redux';
import ResetPassword from './ResetPasswordRequest';
@ -41,7 +42,7 @@ function ForgotPassword(props: Props) {
<div className="flex flex-col items-center justify-center">
<div className="my-8">
<Link to={LOGIN}>
<div className="link">{'Back to Login'}</div>
<Button type="link" >{'Back to Login'}</Button>
</Link>
</div>
</div>

View file

@ -75,8 +75,8 @@ function ResetPasswordRequest(props: Props) {
required
/>
</Form.Field>
<Button type="submit" variant="primary" className="mt-4" loading={loading} disabled={loading}>
Email password reset link
<Button type="submit" variant="primary" className="mt-4 rounded-lg" loading={loading} disabled={loading}>
Email Password Reset Link
</Button>
</>
)}

View file

@ -1,148 +1,149 @@
import {durationFormatted} from 'App/date';
import { durationFormatted } from 'App/date';
import React from 'react';
import FunnelStepText from './FunnelStepText';
import {Icon} from 'UI';
import {Space} from "antd";
import { Icon } from 'UI';
import { Space } from 'antd';
import { Styles } from 'Components/Dashboard/Widgets/common';
interface Props {
filter: any;
index?: number;
focusStage?: (index: number, isFocused: boolean) => void
focusedFilter?: number | null
filter: any;
index?: number;
focusStage?: (index: number, isFocused: boolean) => void;
focusedFilter?: number | null;
}
function FunnelBar(props: Props) {
const {filter, index, focusStage, focusedFilter} = props;
const { filter, index, focusStage, focusedFilter } = props;
const isFocused = focusedFilter && index ? focusedFilter === index - 1 : false;
return (
<div className="w-full mb-4">
<FunnelStepText filter={filter}/>
<div
style={{
height: '25px',
width: '100%',
backgroundColor: '#f5f5f5',
position: 'relative',
borderRadius: '3px',
overflow: 'hidden',
}}
>
<div
className="flex items-center"
style={{
width: `${filter.completedPercentageTotal}%`,
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: '#394EFF',
}}
>
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
{filter.completedPercentageTotal}%
</div>
</div>
<div
style={{
width: `${100.1 - filter.completedPercentageTotal}%`,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
backgroundColor: isFocused ? 'rgba(204, 0, 0, 0.3)' : '#f5f5f5',
cursor: 'pointer',
}}
onClick={() => focusStage?.(index! - 1, filter.isActive)}
className={'hover:border border-red-lightest'}
/>
</div>
<div className="flex justify-between py-2">
{/* @ts-ignore */}
<div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green"/>
<span className="mx-1 font-medium">{filter.sessionsCount} Sessions</span>
<span className="color-gray-medium text-sm">
const isFocused = focusedFilter && index ? focusedFilter === index - 1 : false;
return (
<div className="w-full mb-4">
<FunnelStepText filter={filter} />
<div
style={{
height: '25px',
width: '100%',
backgroundColor: '#f5f5f5',
position: 'relative',
borderRadius: '3px',
overflow: 'hidden'
}}
>
<div
className="flex items-center"
style={{
width: `${filter.completedPercentageTotal}%`,
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: Styles.colors[0]
}}
>
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
{filter.completedPercentageTotal}%
</div>
</div>
<div
style={{
width: `${100.1 - filter.completedPercentageTotal}%`,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
backgroundColor: isFocused ? 'rgba(204, 0, 0, 0.3)' : '#fff0f0',
cursor: 'pointer'
}}
onClick={() => focusStage?.(index! - 1, filter.isActive)}
className={'hover:opacity-75'}
/>
</div>
<div className="flex justify-between py-2">
{/* @ts-ignore */}
<div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green" />
<span className="mx-1 font-medium">{filter.sessionsCount} Sessions</span>
<span className="color-gray-medium text-sm">
({filter.completedPercentage}%) Completed
</span>
</div>
<Space className="items-center">
<Icon name="caret-down-fill" color={filter.droppedCount > 0 ? "red" : "gray-light"} size={16}/>
<span
className={"font-medium mx-1 " + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>{filter.droppedCount} Sessions</span>
<span
className={"text-sm " + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>({filter.droppedPercentage}%) Dropped</span>
</Space>
</div>
</div>
);
<Space className="items-center">
<Icon name="caret-down-fill" color={filter.droppedCount > 0 ? 'red' : 'gray-light'} size={16} />
<span
className={'font-medium mx-1 ' + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>{filter.droppedCount} Sessions</span>
<span
className={'text-sm ' + (filter.droppedCount > 0 ? 'color-red' : 'disabled')}>({filter.droppedPercentage}%) Dropped</span>
</Space>
</div>
</div>
);
}
export function UxTFunnelBar(props: Props) {
const {filter} = props;
const { filter } = props;
return (
<div className="w-full mb-4">
<div className={'font-medium'}>{filter.title}</div>
<div
style={{
height: '25px',
width: '100%',
backgroundColor: '#f5f5f5',
position: 'relative',
borderRadius: '3px',
overflow: 'hidden',
}}
>
<div
className="flex items-center"
style={{
width: `${(filter.completed / (filter.completed + filter.skipped)) * 100}%`,
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: '#00b5ad',
}}
>
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
{((filter.completed / (filter.completed + filter.skipped)) * 100).toFixed(1)}%
</div>
</div>
</div>
<div className="flex justify-between py-2">
{/* @ts-ignore */}
<div className={'flex items-center gap-4'}>
<div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green"/>
<span className="mx-1 font-medium">{filter.completed}</span><span>completed this step</span>
</div>
<div className={'flex items-center'}>
<Icon name="clock" size="16"/>
<span className="mx-1 font-medium">
return (
<div className="w-full mb-4">
<div className={'font-medium'}>{filter.title}</div>
<div
style={{
height: '25px',
width: '100%',
backgroundColor: '#f5f5f5',
position: 'relative',
borderRadius: '3px',
overflow: 'hidden'
}}
>
<div
className="flex items-center"
style={{
width: `${(filter.completed / (filter.completed + filter.skipped)) * 100}%`,
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
backgroundColor: '#00b5ad'
}}
>
<div className="color-white absolute right-0 flex items-center font-medium mr-2 leading-3">
{((filter.completed / (filter.completed + filter.skipped)) * 100).toFixed(1)}%
</div>
</div>
</div>
<div className="flex justify-between py-2">
{/* @ts-ignore */}
<div className={'flex items-center gap-4'}>
<div className="flex items-center">
<Icon name="arrow-right-short" size="20" color="green" />
<span className="mx-1 font-medium">{filter.completed}</span><span>completed this step</span>
</div>
<div className={'flex items-center'}>
<Icon name="clock" size="16" />
<span className="mx-1 font-medium">
{durationFormatted(filter.avgCompletionTime)}
</span>
<span>
<span>
Avg. completion time
</span>
</div>
</div>
{/* @ts-ignore */}
<div className="flex items-center">
<Icon name="caret-down-fill" color="red" size={16}/>
<span className="font-medium mx-1">{filter.skipped}</span><span> skipped</span>
</div>
</div>
</div>
</div>
);
{/* @ts-ignore */}
<div className="flex items-center">
<Icon name="caret-down-fill" color="red" size={16} />
<span className="font-medium mx-1">{filter.skipped}</span><span> skipped</span>
</div>
</div>
</div>
);
}
export default FunnelBar;
const calculatePercentage = (completed: number, dropped: number) => {
const total = completed + dropped;
if (dropped === 0) return 100;
if (total === 0) return 0;
const total = completed + dropped;
if (dropped === 0) return 100;
if (total === 0) return 0;
return Math.round((completed / dropped) * 100);
return Math.round((completed / dropped) * 100);
};

View file

@ -5,6 +5,7 @@ import cn from 'classnames';
import stl from './FunnelWidget.module.css';
import { observer } from 'mobx-react-lite';
import { NoContent, Icon } from 'UI';
import { Tag, Tooltip } from 'antd';
import { useModal } from 'App/components/Modal';
interface Props {
@ -90,19 +91,21 @@ function FunnelWidget(props: Props) {
</div>
<div className="flex items-center pb-4">
<div className="flex items-center">
<span className="text-xl mr-2">Lost conversion</span>
<div className="rounded px-2 py-1 bg-red-lightest color-red">
<span className="text-xl mr-2 font-medium">{funnel.lostConversions}</span>
<span className="text-sm">({funnel.lostConversionsPercentage}%)</span>
</div>
<span className="text-base font-medium mr-2">Lost conversion</span>
<Tooltip title={`${funnel.lostConversions} Sessions ${funnel.lostConversionsPercentage}%`}>
<Tag bordered={false} color="red" className='text-lg font-medium rounded-lg'>
{funnel.lostConversions}
</Tag>
</Tooltip>
</div>
<div className="mx-3" />
<div className="flex items-center">
<span className="text-xl mr-2">Total conversion</span>
<div className="rounded px-2 py-1 bg-tealx-lightest color-tealx">
<span className="text-xl mr-2 font-medium">{funnel.totalConversions}</span>
<span className="text-sm">({funnel.totalConversionsPercentage}%)</span>
</div>
<span className="text-base font-medium mr-2">Total conversion</span>
<Tooltip title={`${funnel.totalConversions} Sessions ${funnel.totalConversionsPercentage}%`}>
<Tag bordered={false} color="cyan" className='text-lg font-medium rounded-lg'>
{funnel.totalConversions}
</Tag>
</Tooltip>
</div>
</div>
{funnel.totalDropDueToIssues > 0 && <div className="flex items-center mb-2"><Icon name="magic" /> <span className="ml-2">{funnel.totalDropDueToIssues} sessions dropped due to issues.</span></div>}

View file

@ -110,6 +110,7 @@ const Login: React.FC<LoginProps> = ({errors, loading, authDetails, login, setJw
onChange={(e) => setEmail(e.target.value)}
required
icon="envelope"
/>
</Form.Field>
<Form.Field>
@ -141,7 +142,7 @@ const Login: React.FC<LoginProps> = ({errors, loading, authDetails, login, setJw
<div className="px-8 w-full">
<Button
data-test-id={'log-button'}
className="mt-2 w-full text-center"
className="mt-2 w-full text-center rounded-lg"
type="submit"
variant="primary"
>

View file

@ -52,7 +52,7 @@ function SideMenu(props: Props) {
color={activeTab === OB_TABS.INSTALLING ? 'teal' : 'gray'}
/>
}
className={'!rounded hover-fill-teal'}
className={'!rounded-lg hover-fill-teal'}
>
Setup OpenReplay
</Menu.Item>
@ -66,7 +66,7 @@ function SideMenu(props: Props) {
color={activeTab === OB_TABS.IDENTIFY_USERS ? 'teal' : 'gray'}
/>
}
className={'!rounded hover-fill-teal'}
className={'!rounded-lg hover-fill-teal'}
>
Identify Users
</Menu.Item>
@ -80,7 +80,7 @@ function SideMenu(props: Props) {
color={activeTab === OB_TABS.MANAGE_USERS ? 'teal' : 'gray'}
/>
}
className={'!rounded hover-fill-teal'}
className={'!rounded-lg hover-fill-teal'}
>
Invite Collaborators
</Menu.Item>
@ -94,7 +94,7 @@ function SideMenu(props: Props) {
color={activeTab === OB_TABS.INTEGRATIONS ? 'teal' : 'gray'}
/>
}
className={'!rounded hover-fill-teal'}
className={'!rounded-lg hover-fill-teal'}
>
Integrations
</Menu.Item>
@ -109,7 +109,7 @@ function SideMenu(props: Props) {
color={activeTab === 'support' ? 'teal' : 'gray'}
/>
}
className={'!rounded hover-fill-teal'}
className={'!rounded-lg hover-fill-teal'}
>
Support
</Menu.Item>

View file

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { Popover, Icon } from 'UI';
import IssuesModal from './IssuesModal';
import { fetchProjects, fetchMeta } from 'Duck/assignments';
import { Popover as AntPopover, Button } from 'antd';
import { Tooltip, Button } from 'antd';
@connect(
(state) => ({
@ -67,11 +67,11 @@ class Issues extends React.Component {
)}
>
<div>
<AntPopover content={'Create Issue'}>
<Tooltip title={'Create Issue'} placement='bottom'>
<Button size={'small'} className={'flex items-center justify-center'}>
<Icon name={`integrations/${provider === 'jira' ? 'jira' : 'github'}`} />
</Button>
</AntPopover>
</Tooltip>
</div>
</Popover>
);

View file

@ -1,6 +1,7 @@
import React from 'react';
import { Icon } from 'UI';
import { Popover, Button } from 'antd';
import {Keyboard} from 'lucide-react'
import { Button, Tooltip } from 'antd';
import { useModal } from "../../../../Modal";
const Key = ({ label }: { label: string }) => <div style={{ minWidth: 52 }} className="whitespace-nowrap font-bold bg-gray-lightest rounded shadow px-2 py-1 text-figmaColors-text-primary text-center">{label}</div>;
@ -35,7 +36,7 @@ function ShortcutGrid() {
return (
<div className={'p-4 overflow-y-auto h-screen'}>
<div className={'mb-4 font-semibold text-xl'}>Keyboard Shortcuts</div>
<div className=" grid grid-cols-2 grid-flow-row-dense auto-cols-max gap-4 justify-items-start">
<div className=" grid grid-cols-1 grid-flow-row-dense auto-cols-max gap-4 justify-items-start">
<Cell shortcut="⇧ + U" text="Copy Session URL with time" />
<Cell shortcut="⇧ + C" text="Launch Console" />
<Cell shortcut="⇧ + N" text="Launch Network" />
@ -62,6 +63,7 @@ function ShortcutGrid() {
function KeyboardHelp() {
const { showModal } = useModal();
return (
<Tooltip placement='bottom' title='Keyboard Shortcuts'>
<Button
size={'small'}
className={'flex items-center justify-center'}
@ -69,8 +71,9 @@ function KeyboardHelp() {
showModal(<ShortcutGrid />, { right: true, width: 420 })
}}
>
<Icon name={'keyboard'} size={21} color={'black'} />
<Keyboard size={18}/>
</Button>
</Tooltip>
);
}

View file

@ -1,11 +1,12 @@
import React from 'react';
import { Icon } from 'UI';
import { Button, Tag } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { tagProps, Note } from 'App/services/NotesService';
import { formatTimeOrDate } from 'App/date';
import { useStore } from 'App/mstore';
import { observer } from 'mobx-react-lite';
import { TeamBadge } from 'Shared/SessionsTabOverview/components/Notes';
import { Tag } from 'antd'
interface Props {
note?: Note;
@ -21,7 +22,7 @@ function ReadNote(props: Props) {
return (
<div style={{ position: 'absolute', top: '45%', left: 'calc(50% - 200px)' }}>
<div
className="flex items-start flex-col p-4 border gap-2 rounded"
className="flex items-start flex-col p-4 border gap-2 rounded-lg"
style={{ background: '#FFFEF5', width: 400 }}
>
<div className="flex items-start font-semibold w-full text-xl">
@ -50,8 +51,8 @@ function ReadNote(props: Props) {
className="flex items-center justify-center"
>
<div
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded"
style={{ background: '#FFFEF5', width: 500 }}
className="flex items-start !text-lg flex-col p-4 border gap-2 rounded-lg bg-amber-50"
style={{ width: 500 }}
>
<div className="flex items-center w-full">
<div className="p-2 bg-gray-light rounded-full">
@ -71,23 +72,32 @@ function ReadNote(props: Props) {
{props.note.message}
</div>
<div className="w-full">
<div className="flex items-center gap-2 flex-wrap w-full">
<div className="flex items-center justify-between w-full">
<div className='flex gap-1 items-center'>
{props.note.tag ? (
<Tag
color={tagProps[props.note.tag]}
className='border-0 rounded-lg'
>
{props.note.tag}
</Tag>
) : null}
<Tag bordered={false} >
{!props.note.isPublic ? null : <TeamBadge />}
</Tag>
<div
onClick={props.onClose}
className="ml-auto rounded py-2 px-4 flex items-center text-blue gap-2 cursor-pointer hover:bg-active-blue"
>
<Icon size={20} name="play-fill" color="main" />
<span>Play Session</span>
</div>
<Button
onClick={props.onClose}
icon={<PlayCircleOutlined />}
type='default'
>
Play Session
</Button>
</div>
</div>
</div>

View file

@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { useStore } from 'App/mstore';
import KeyboardHelp from 'Components/Session_/Player/Controls/components/KeyboardHelp';
import { Icon, Tooltip } from 'UI';
import { Icon } from 'UI';
import QueueControls from './QueueControls';
import Bookmark from 'Shared/Bookmark';
import SharePopup from '../shared/SharePopup/SharePopup';
@ -13,7 +13,7 @@ import { connect } from 'react-redux';
import SessionTabs from 'Components/Session/Player/SharedComponents/SessionTabs';
import { IFRAME } from 'App/constants/storageKeys';
import cn from 'classnames';
import { Switch, Button as AntButton, Popover } from 'antd';
import { Switch, Button as AntButton, Popover, Tooltip } from 'antd';
import { ShareAltOutlined } from '@ant-design/icons';
import { checkParam } from 'App/utils';
@ -116,11 +116,11 @@ function SubHeader(props) {
showCopyLink={true}
trigger={
<div className="relative">
<Popover content={'Share Session'}>
<Tooltip title='Share Session' placement='bottom'>
<AntButton size={'small'} className="flex items-center justify-center">
<ShareAltOutlined />
</AntButton>
</Popover>
</Tooltip>
</div>
}
/>

View file

@ -2,7 +2,7 @@ import CreateNote from 'Components/Session_/Player/Controls/components/CreateNot
import React from 'react';
import { connect } from 'react-redux';
import { PlayerContext } from 'App/components/Session/playerContext';
import { Button, Popover } from 'antd';
import { Button, Tooltip } from 'antd';
import { MessageOutlined } from '@ant-design/icons';
import { useModal } from 'App/components/Modal';
@ -22,7 +22,7 @@ function NotePopup({ tooltipActive }: { tooltipActive: boolean }) {
};
return (
<Popover content={'Add Note'}>
<Tooltip title={'Add Note'} placement='bottom'>
<Button
size={'small'}
className={'flex items-center justify-center'}
@ -31,7 +31,7 @@ function NotePopup({ tooltipActive }: { tooltipActive: boolean }) {
>
<MessageOutlined />
</Button>
</Popover>
</Tooltip>
);
}

View file

@ -97,7 +97,7 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
<div className='m-10 '>
<img src='/assets/logo.svg' width={200} alt='Logo' />
</div>
<Form onSubmit={onSubmit} className='bg-white border rounded' style={{ maxWidth: '420px' }}>
<Form onSubmit={onSubmit} className='bg-white border rounded-lg shadow-sm' style={{ maxWidth: '420px' }}>
<div className='mb-8'>
<h2 className='text-center text-2xl font-medium mb-6 border-b p-5 w-full'>
Create Account
@ -138,6 +138,7 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
onChange={write}
required={true}
icon='envelope'
className='rounded-lg'
/>
</Form.Field>
<Form.Field>
@ -150,6 +151,7 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
onChange={write}
required={true}
icon='key'
className='rounded-lg'
/>
</Form.Field>
<Form.Field>
@ -161,6 +163,7 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
onChange={write}
required={true}
icon='user-alt'
className='rounded-lg'
/>
</Form.Field>
<Form.Field>
@ -172,13 +175,14 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
onChange={write}
required={true}
icon='buildings'
className='rounded-lg'
/>
</Form.Field>
{passwordError && (
// <Alert type='error' message={PASSWORD_POLICY} banner icon={null} />
<Alert
className='my-3'
className='my-3 rounded-lg'
// message="Error Text"
description={PASSWORD_POLICY}
type='error'
@ -186,14 +190,14 @@ const SignupForm: React.FC<SignupFormProps> = ({ tenants, errors, loading, signu
)}
{errors && errors.length && (
<Alert
className='my-3'
className='my-3 rounded-lg'
// message="Error Text"
description={errors[0]}
type='error'
/>
)}
<Button type='submit' variant='primary' loading={loading} className='w-full'>
<Button type='submit' variant='primary' loading={loading} className='w-full rounded-lg'>
Create Account
</Button>
<div className='my-6'>

View file

@ -8,7 +8,7 @@ import AnimatedSVG from 'Shared/AnimatedSVG';
import { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
import { Loader, NoContent, Pagination, Link, Icon } from 'UI';
import { checkForRecent, getDateFromMill } from 'App/date';
import { ArrowRightOutlined } from '@ant-design/icons';
import { ArrowRightOutlined, PlusOutlined } from '@ant-design/icons';
import { useHistory, useParams } from 'react-router-dom';
import { withSiteId, usabilityTestingEdit, usabilityTestingView } from 'App/routes';
import { debounce } from 'App/utils';
@ -110,9 +110,10 @@ function TestsTable() {
Usability Tests
</h1>
<div className={'ml-auto'} />
<Button type="primary" onClick={openModal}>
<Button type="primary" icon={<PlusOutlined />} onClick={openModal}>
Create Usability Test
</Button>
<Search
placeholder="Filter by title"
allowClear

View file

@ -1,4 +1,4 @@
import { Button, Popover } from 'antd';
import { Button, Tooltip } from 'antd';
import { BookmarkCheck, Bookmark as BookmarkIcn, Vault } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
@ -13,6 +13,7 @@ interface Props {
isEnterprise: boolean;
noMargin?: boolean;
}
function Bookmark(props: Props) {
const { sessionId, favorite, isEnterprise, noMargin } = props;
const [isFavorite, setIsFavorite] = useState(favorite);
@ -48,7 +49,7 @@ function Bookmark(props: Props) {
return (
<div onClick={toggleFavorite} className="w-full">
<Popover content={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD}>
<Tooltip title={isFavorite ? TOOLTIP_TEXT_REMOVE : TOOLTIP_TEXT_ADD} placement='bottom'>
<Button
type={isFavorite ? 'primary' : undefined}
ghost={isFavorite}
@ -57,7 +58,7 @@ function Bookmark(props: Props) {
>
{icon}
</Button>
</Popover>
</Tooltip>
</div>
);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
import { Button } from 'antd';
import { useHistory } from 'react-router-dom';
import { LeftOutlined } from '@ant-design/icons';
function BackButton() {
const history = useHistory();
const siteId = location.pathname.split('/')[1];
const handleBackClick = () => {
history.push(`/${siteId}/dashboard`);
};
return (
<Button type="text" onClick={handleBackClick} icon={<LeftOutlined />} className="px-1 pe-2 me-2 gap-1">
Back
</Button>
);
}
export default BackButton;

View file

@ -1,6 +1,8 @@
.wrapper {
background-color: white;
outline: solid thin #CCC;
background-color: #FFF;
outline: solid thin #EEE;
border-radius: .5rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
& .body {
display: flex;
border-bottom: solid thin $gray-light;
@ -11,7 +13,7 @@
.preSelections {
width: 130px;
background-color: white;
border-right: solid thin $gray-light;
border-right: solid thin #EEE;
& > div {
padding: 8px 10px;
width: 100%;

View file

@ -147,7 +147,7 @@ function FilterList(props: Props) {
width: 'calc(100% + 2.5rem)',
}}
className={
'hover:bg-active-blue px-5 gap-2 items-center flex z-10'
'hover:bg-active-blue px-5 gap-2 items-center flex'
}
id={`${filter.key}-${filterIndex}`}
onDragOver={(e) => handleDragOverEv(e, filterIndex)}

View file

@ -47,7 +47,7 @@ function FilterSelection(props: Props) {
})
) : (
<div
className={cn('rounded py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis hover:bg-gray-light-shade', { 'opacity-50 pointer-events-none': disabled })}
className={cn('rounded-lg py-1 px-3 flex items-center cursor-pointer bg-gray-lightest text-ellipsis hover:bg-gray-light-shade', { 'opacity-50 pointer-events-none': disabled })}
style={{ width: '150px', height: '26px', border: 'solid thin #e9e9e9' }}
onClick={() => setShowModal(true)}
>

View file

@ -41,7 +41,7 @@ function LiveSessionList(props: Props) {
var timeoutId: any;
const { filters } = filter;
const hasUserFilter = filters.map((i: any) => i.key).includes(KEYS.USERID);
const sortOptions = [{ label: 'Newest', value: 'timestamp' }].concat(
const sortOptions = [{ label: 'Freshness', value: 'timestamp' }].concat(
metaList
.map((i: any) => ({
label: capitalize(i),
@ -105,6 +105,7 @@ function LiveSessionList(props: Props) {
onChange={onSortChange}
value={sortOptions.find((i: any) => i.value === filter.sort) || sortOptions[0]}
/>
<div className="mx-2" />
<SortOrderButton
onChange={(state: any) => props.applyFilter({ order: state })}
@ -204,3 +205,4 @@ export default withPermissions(['ASSIST_LIVE'])(
}
)(LiveSessionList)
);

View file

@ -1,11 +1,16 @@
import React from 'react';
import { Icon, Button } from 'UI';
import { Alert, Space, Button } from 'antd';
import { connect } from 'react-redux';
import { onboarding as onboardingRoute } from 'App/routes';
import { withRouter } from 'react-router-dom';
import * as routes from '../../../routes';
import { indigo } from 'tailwindcss/colors';
import { SquareArrowOutUpRight } from 'lucide-react';
const withSiteId = routes.withSiteId;
const indigoWithOpacity = `rgba(${parseInt(indigo[500].slice(1, 3), 16)}, ${parseInt(indigo[500].slice(3, 5), 16)}, ${parseInt(indigo[500].slice(5, 7), 16)}, 0.1)`; // 0.5 is the opacity level
const NoSessionsMessage = (props) => {
const {
@ -19,32 +24,35 @@ const NoSessionsMessage = (props) => {
return (
<>
{showNoSessions && (
<div>
<div
className='rounded text-sm flex items-center p-2 justify-between mb-4'
style={{ backgroundColor: 'rgba(255, 239, 239, 1)', border: 'solid thin rgba(221, 181, 181, 1)' }}
>
<div className='flex items-center w-full'>
<div className='flex-shrink-0 w-8 flex justify-center'>
<Icon name='info-circle' size='14' color='gray-darkest' />
</div>
<div className='ml-2 color-gray-darkest mr-auto text-base'>
It might take a few minutes for first recording to appear.
<a href='https://docs.openreplay.com/en/troubleshooting/session-recordings/' className='link ml-2'>
Troubleshoot
</a>
.
</div>
<Button
variant='primary'
className='h-8 text-base'
onClick={() => props.history.push(onboardingPath)}
>
Complete Project Setup
</Button>
</div>
<div className="w-full mb-5">
<Space direction="vertical" className="w-full">
<Alert
className="border-transparent rounded-lg w-full"
message="Your sessions will appear here soon. It may take a few minutes as sessions are optimized for efficient playback."
type="warning"
showIcon
action={
<Space>
<Button
type="link"
size="small"
onClick={() => window.open('https://docs.openreplay.com/en/troubleshooting/session-recordings/', '_blank')}
icon={<SquareArrowOutUpRight size={16} />}
>
Troubleshoot
</Button>
<Button
type="default"
size="small"
onClick={() => history.push(onboardingPath)}
>
Complete Project Setup
</Button>
</Space>
}
/>
</Space>
</div>
</div>
)}
</>
);

View file

@ -1,5 +1,6 @@
import React from 'react';
import { CircularLoader, Icon, Tooltip, Button } from 'UI';
import {Button, Tooltip} from 'antd';
import { ListRestart } from 'lucide-react';
import cn from 'classnames';
interface Props {
@ -12,8 +13,9 @@ interface Props {
export default function ReloadButton(props: Props) {
const { loading, onClick, iconSize = '20', iconName = 'arrow-repeat', className = '' } = props;
return (
<Tooltip title="Refresh">
<Button icon={iconName} variant="text" onClick={onClick}>
<Tooltip title="Refresh" placement='right'>
<Button type="default" onClick={onClick}>
<ListRestart size={18} />
</Button>
</Tooltip>
);

View file

@ -54,7 +54,7 @@ function SessionSearchField(props: Props) {
id="search"
type="search"
autoComplete="off"
className="hover:border-gray-medium text-lg placeholder-lg"
className="hover:border-gray-medium text-lg placeholder-lg h-9 shadow-sm"
/>
{showModal && (

View file

@ -41,35 +41,43 @@ function NoteItem(props: Props) {
}
};
const menuItems = [
{ icon: 'link-45deg', text: 'Copy Note URL', onClick: onCopy },
{ icon: 'link-45deg', text: 'Copy Link', onClick: onCopy },
{ icon: 'trash', text: 'Delete', onClick: onDelete },
];
const safeStrMessage =
props.note.message.length > 150 ? props.note.message.slice(0, 150) + '...' : props.note.message;
return (
<div className="flex items-center p-2 border-b">
<div className="flex items-center px-2 border-b">
<Link
style={{ width: '90%' }}
to={
session(props.note.sessionId) +
(props.note.timestamp > 0
? `?jumpto=${props.note.timestamp}&note=${props.note.noteId}`
: `?note=${props.note.noteId}`)
}
>
<div className="flex flex-col gap-1 p-2 rounded cursor-pointer note-hover">
<div className="py-1 capitalize-first text-lg">{safeStrMessage}</div>
<div className="flex items-center">
{props.note.tag ? (
session(props.note.sessionId) +
(props.note.timestamp > 0
? `?jumpto=${props.note.timestamp}&note=${props.note.noteId}`
: `?note=${props.note.noteId}`)
}
>
<div className="flex flex-col p-2 rounded cursor-pointer">
<div className="flex py-1 text-base">
{props.note.tag ? (
<Tag
color={tagProps[props.note.tag]}
className='border-0 rounded-lg hover:inherit gap-2 w-14 text-center'
>
{props.note.tag}
</Tag>
) : null}
<div className="text-disabled-text flex items-center text-sm">
<span className="color-gray-darkest mr-1">By </span>
<div className='cap-first'>
{safeStrMessage}
</div>
</div>
<div className="flex items-center">
<div className="flex items-center text-sm">
<span className="text-gray-600 mr-1 capitalize">By </span>
{props.note.userName},{' '}
{formatTimeOrDate(props.note.createdAt as unknown as number, timezone)}
<div className="mx-2" />

View file

@ -5,7 +5,7 @@ export default function TeamBadge() {
return (
<div className="flex items-center ml-2">
<Icon name="user-friends" className="mr-1" color="gray-darkest" />
<span className="text-disabled-text text-sm">Team</span>
<span className="text-sm">Team</span>
</div>
)
}

View file

@ -215,7 +215,7 @@ function SessionList(props: Props) {
<div className='flex items-center justify-center flex-col'>
<AnimatedSVG name={NO_CONTENT.icon} size={180} />
<div className='mt-4' />
<div className='text-center relative'>
<div className='text-center relative text-lg'>
{NO_CONTENT.message}
</div>
</div>

View file

@ -1,5 +1,7 @@
import React from 'react';
import { Button, Icon } from 'UI';
import { Button } from 'antd';
import { LinkOutlined } from '@ant-design/icons';
import copy from 'copy-to-clipboard';
function SessionCopyLink({ time }: { time: number }) {
@ -20,11 +22,8 @@ function SessionCopyLink({ time }: { time: number }) {
return (
<div className="flex justify-between items-center w-full mt-2">
<Button variant="text-primary" onClick={copyHandler}>
<>
<Icon name="link-45deg" className="mr-2" color="teal" size="18" />
<span>Copy URL at current time</span>
</>
<Button type="text" onClick={copyHandler} icon={<LinkOutlined />}>
Copy URL at Current Time
</Button>
{copied && <div className="color-gray-medium">Copied</div>}
</div>

View file

@ -175,7 +175,7 @@ function ShareModalComp({
<div>
<div className={'flex flex-col gap-4'}>
<div>
<div className={'font-semibold flex items-center'}>
<div className={'font-medium flex items-center'}>
Share via
</div>
{hasBoth ? (
@ -214,7 +214,7 @@ function ShareModalComp({
</div>
<div>
<div className={'font-semibold'}>Select a channel or individual</div>
<div className={'font-medium'}>Select a channel or individual</div>
{shareTo === 'slack' ? (
<Select
options={slackOptions}
@ -233,7 +233,7 @@ function ShareModalComp({
</div>
<div>
<div className={'font-semibold'}>Message</div>
<div className={'font-medium'}>Message</div>
<textarea
name="message"
id="message"

View file

@ -1,5 +1,7 @@
import React from 'react';
import { Icon, Tooltip } from 'UI';
import { Segmented } from 'antd';
import { ArrowDownOutlined, ArrowUpOutlined } from '@ant-design/icons';
import cn from 'classnames';
interface Props {
@ -11,30 +13,22 @@ export default React.memo(function SortOrderButton(props: Props) {
const isAscending = sortOrder === 'asc';
return (
<div className="flex items-center border">
<Tooltip title={'Ascending'}>
<div
className={cn('p-2 hover:bg-active-blue', {
'cursor-pointer bg-white': !isAscending,
'bg-active-blue pointer-events-none': isAscending,
})}
onClick={() => onChange('asc')}
>
<Icon name="arrow-up" size="14" color={isAscending ? 'teal' : 'gray-medium'} />
</div>
</Tooltip>
<div className="rounded-full">
<Tooltip title={'Descending'}>
<div
className={cn('p-2 hover:bg-active-blue border-l', {
'cursor-pointer bg-white': isAscending,
'bg-active-blue pointer-events-none': !isAscending,
})}
onClick={() => onChange('desc')}
>
<Icon name="arrow-down" size="14" color={!isAscending ? 'teal' : 'gray-medium'} />
</div>
</Tooltip>
<Segmented
size='small'
options={[
{ label: 'Ascending', value: 'Ascending', icon: <ArrowUpOutlined /> },
{ label: 'Descending', value: 'Descending', icon: <ArrowDownOutlined /> },
]}
onChange={(value) => {
if (value === 'Ascending') {
onChange('asc');
} else if (value === 'Descending') {
onChange('desc');
}
}}
/>
</div>
);
});

View file

@ -12,6 +12,7 @@ interface Props {
function Activity(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M6 2a.5.5 0 0 1 .47.33L10 12.036l1.53-4.208A.5.5 0 0 1 12 7.5h3.5a.5.5 0 0 1 0 1h-3.15l-1.88 5.17a.5.5 0 0 1-.94 0L6 3.964 4.47 8.171A.5.5 0 0 1 4 8.5H.5a.5.5 0 0 1 0-1h3.15l1.88-5.17A.5.5 0 0 1 6 2Z"/></svg>
);
}

View file

@ -12,7 +12,7 @@ interface Props {
function Pdf_download(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 19 19" width={ `${ width }px` } height={ `${ height }px` } ><path d="M10.094 5.249a.594.594 0 0 0-1.188 0v4.504l-1.36-1.362a.595.595 0 0 0-.841.84l2.375 2.376a.596.596 0 0 0 .84 0l2.375-2.375a.595.595 0 0 0-.84-.841l-1.361 1.362V5.249Z"/><path d="M16.625 16.625V5.344L11.281 0H4.75a2.375 2.375 0 0 0-2.375 2.375v14.25A2.375 2.375 0 0 0 4.75 19h9.5a2.375 2.375 0 0 0 2.375-2.375ZM11.281 3.562a1.781 1.781 0 0 0 1.781 1.782h2.376v11.281a1.188 1.188 0 0 1-1.188 1.188h-9.5a1.187 1.187 0 0 1-1.188-1.188V2.375A1.188 1.188 0 0 1 4.75 1.187h6.531v2.375Z"/><path clipRule="evenodd" d="M15.58 13.49H3.42v4.37h1.789v-3.512H6.61c.282 0 .524.051.726.154.205.103.361.245.47.425.11.178.165.383.165.613 0 .226-.055.424-.164.593-.11.169-.266.3-.47.396-.203.093-.445.14-.727.14h-.554v1.191h2.37v-3.512h1.13c.238 0 .455.041.653.123a1.537 1.537 0 0 1 .854.88c.08.205.12.431.12.68v.148c0 .247-.04.474-.12.68a1.512 1.512 0 0 1-.849.88c-.193.08-.404.12-.635.121h2.046v-3.512h2.35v.654h-1.503v.808h1.365v.651h-1.365v1.399h3.108v-4.37Zm-9.524 1.512v1.013h.554c.12 0 .216-.02.29-.06a.367.367 0 0 0 .161-.167.547.547 0 0 0 .054-.244.668.668 0 0 0-.054-.267.431.431 0 0 0-.161-.198.496.496 0 0 0-.29-.077h-.554Zm3.512 2.207h-.295v-2.207h.283c.123 0 .233.021.328.065a.604.604 0 0 1 .24.195.88.88 0 0 1 .146.321c.033.127.05.275.05.444v.152c0 .225-.03.415-.089.569a.707.707 0 0 1-.256.345.696.696 0 0 1-.407.116Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-down"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M12 18v-6"/><path d="m9 15 3 3 3-3"/></svg>
);
}

View file

@ -12,7 +12,7 @@ interface Props {
function Pencil(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-pen-line"><path d="m18 5-2.414-2.414A2 2 0 0 0 14.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2"/><path d="M21.378 12.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><path d="M8 18h1"/></svg>
);
}

View file

@ -12,7 +12,7 @@ interface Props {
function Trash(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width={ `${ width }px` } height={ `${ height }px` } ><path d="M3 6h18M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
);
}

View file

@ -12,7 +12,8 @@ interface Props {
function Users(props: Props) {
const { size = 14, width = size, height = size, fill = '' } = props;
return (
<svg viewBox="0 0 16 16" width={ `${ width }px` } height={ `${ height }px` } ><path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width={ `${ width }px` } height={ `${ height }px` } viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users-round"><path d="M18 21a8 8 0 0 0-16 0"/><circle cx="10" cy="8" r="5"/><path d="M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3"/></svg>
);
}

View file

@ -24,7 +24,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
rows={rows}
style={{ resize: 'none' }}
maxLength={500}
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
className={cn('p-2 border border-gray-light bg-white w-full rounded-lg', className, { 'pl-10': icon })}
{...rest}
/>
) : (
@ -32,7 +32,7 @@ const Input = React.forwardRef((props: Props, ref: any) => {
ref={ref}
type={type}
style={{ height: `${height}px`, width: width? `${width}px` : '' }}
className={cn('p-2 border border-gray-light bg-white w-full rounded', className, { 'pl-10': icon })}
className={cn('p-2 border border-gray-light bg-white w-full rounded-lg', className, { 'pl-10': icon })}
{...rest}
/>
)}

View file

@ -1,7 +1,7 @@
import React from "react";
import { Icon, Popover, Tooltip } from "UI";
import { Dropdown, Menu, Button } from "antd";
import { MoreOutlined } from "@ant-design/icons";
import {EllipsisVertical} from 'lucide-react';
import styles from "./itemMenu.module.css";
import cn from "classnames";
@ -99,7 +99,7 @@ export default class ItemMenu extends React.PureComponent<Props> {
<Button
className={cn("select-none", !this.props.flat ? parentStyles : "", {
"": !this.props.flat && displayed && label,
})}
}, 'border-0 shadow-one')}
>
{label && (
<span className={cn("font-medium")}>
@ -114,7 +114,7 @@ export default class ItemMenu extends React.PureComponent<Props> {
className={cn("rounded-full flex items-center justify-center")}
role="button"
>
<MoreOutlined />
<EllipsisVertical size={16} />
</div>
)}
</Button>

View file

@ -8,7 +8,7 @@ import styles from './link.module.css';
const OpenReplayLink = ({ siteId, to, className="", dispatch, ...other }) => (
<Link
{ ...other }
className={ cn(className, styles.link) }
className={ cn(className, styles.link , 'px-2', 'rounded-lg', 'hover:text-inherit', 'hover:bg-amber-50', 'hover:shadow-sm') }
to={ withSiteId(to, siteId) }
/>
);

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