Merge branch 'dashboards-redesign' into dev
This commit is contained in:
commit
9cbb1bf283
108 changed files with 2782 additions and 2442 deletions
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
65
frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx
Normal file
65
frontend/app/components/Dashboard/Widgets/ListWithIcons.tsx
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
|
|
|
|||
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ function WidgetDateRange({
|
|||
period={period}
|
||||
onChange={onChangePeriod}
|
||||
right={true}
|
||||
isAnt={true}
|
||||
useButtonStyle={true}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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=""/>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ function CardMenu({card}: any) {
|
|||
},
|
||||
{
|
||||
key: 'hide',
|
||||
label: 'Hide',
|
||||
label: 'Remove',
|
||||
icon: <EyeOffIcon size={16}/>,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
21
frontend/app/components/shared/Breadcrumb/BackButton.tsx
Normal file
21
frontend/app/components/shared/Breadcrumb/BackButton.tsx
Normal 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;
|
||||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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}¬e=${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}¬e=${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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue