feat(ui) - dashboard improvements - wip
This commit is contained in:
parent
8e9a09d6d3
commit
7bd6ee3065
24 changed files with 591 additions and 133 deletions
|
|
@ -2,12 +2,16 @@ import Modal from 'App/components/Modal/Modal';
|
|||
import React from 'react';
|
||||
import MetricTypeList from '../MetricTypeList';
|
||||
|
||||
function AddCardModal() {
|
||||
interface Props {
|
||||
siteId: string;
|
||||
dashboardId: string;
|
||||
}
|
||||
function AddCardModal(props: Props) {
|
||||
return (
|
||||
<>
|
||||
<Modal.Header title="Add Card" />
|
||||
<Modal.Content className="p-0">
|
||||
<MetricTypeList />
|
||||
<MetricTypeList siteId={props.siteId} dashboardId={props.dashboardId} />
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import DashboardEditModal from '../DashboardEditModal';
|
|||
import AddCardModal from '../AddCardModal';
|
||||
|
||||
interface IProps {
|
||||
dashboardId: string;
|
||||
siteId: string;
|
||||
renderReport?: any;
|
||||
}
|
||||
|
|
@ -21,10 +22,9 @@ interface IProps {
|
|||
type Props = IProps & RouteComponentProps;
|
||||
|
||||
function DashboardHeader(props: Props) {
|
||||
const { siteId } = props;
|
||||
const { siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { showModal } = useModal();
|
||||
// const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
const [focusTitle, setFocusedInput] = React.useState(true);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
const period = dashboardStore.period;
|
||||
|
|
@ -82,7 +82,9 @@ function DashboardHeader(props: Props) {
|
|||
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => showModal(<AddCardModal />, { right: true })}
|
||||
onClick={() =>
|
||||
showModal(<AddCardModal dashboardId={dashboardId} siteId={siteId} />, { right: true })
|
||||
}
|
||||
icon="plus"
|
||||
>
|
||||
Add Card
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function DashboardSideMenu(props: Props) {
|
|||
<SideMenuitem
|
||||
active={isMetric}
|
||||
id="menu-manage-alerts"
|
||||
title="Metrics"
|
||||
title="Cards"
|
||||
iconName="bar-chart-line"
|
||||
onClick={() => redirect(withSiteId(metrics(), siteId))}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ function DashboardWidgetGrid(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 p-8 gap-2">
|
||||
<MetricTypeList />
|
||||
<MetricTypeList dashboardId={dashboardId} siteId={siteId} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Icon } from 'UI';
|
|||
|
||||
export interface MetricType {
|
||||
title: string;
|
||||
icon: IconNames;
|
||||
icon?: IconNames;
|
||||
description: string;
|
||||
slug: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,90 +2,37 @@ import { useModal } from 'App/components/Modal';
|
|||
import React from 'react';
|
||||
import MetricsLibraryModal from '../MetricsLibraryModal';
|
||||
import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem';
|
||||
import { TYPES, LIBRARY } from 'App/constants/card';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
|
||||
const METRIC_TYPES: MetricType[] = [
|
||||
{
|
||||
title: 'Add From Library',
|
||||
icon: 'grid',
|
||||
description: 'Select a pre existing card from card library',
|
||||
slug: 'library',
|
||||
},
|
||||
{
|
||||
title: 'Timeseries',
|
||||
icon: 'graph-up',
|
||||
description: 'Trend of sessions count in over the time.',
|
||||
slug: 'timeseries',
|
||||
},
|
||||
{
|
||||
title: 'Table',
|
||||
icon: 'list-alt',
|
||||
description: 'See list of Users, Sessions, Errors, Issues, etc.,',
|
||||
slug: 'table',
|
||||
},
|
||||
{
|
||||
title: 'Funnel',
|
||||
icon: 'funnel',
|
||||
description: 'Uncover the issues impacting user journeys.',
|
||||
slug: 'funnel',
|
||||
},
|
||||
{
|
||||
title: 'Errors Tracking',
|
||||
icon: 'exclamation-circle',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'errors',
|
||||
},
|
||||
{
|
||||
title: 'Performance Monitoring',
|
||||
icon: 'speedometer2',
|
||||
description: 'Retention graph of users / features over a period of time.',
|
||||
slug: 'performance',
|
||||
},
|
||||
{
|
||||
title: 'Resource Monitoring',
|
||||
icon: 'files',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'resource-monitoring',
|
||||
},
|
||||
{
|
||||
title: 'Web Vitals',
|
||||
icon: 'activity',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'web-vitals',
|
||||
},
|
||||
{
|
||||
title: 'User Path',
|
||||
icon: 'signpost-split',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'user-path',
|
||||
},
|
||||
{
|
||||
title: 'Retention',
|
||||
icon: 'arrow-repeat',
|
||||
description: 'Retension graph of users / features over a period of time.',
|
||||
slug: 'retention',
|
||||
},
|
||||
{
|
||||
title: 'Feature Adoption',
|
||||
icon: 'card-checklist',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'feature-adoption',
|
||||
},
|
||||
];
|
||||
interface Props extends RouteComponentProps {
|
||||
dashboardId: number;
|
||||
siteId: string;
|
||||
}
|
||||
function MetricTypeList(props: Props) {
|
||||
const { dashboardId, siteId, history } = props;
|
||||
const { hideModal } = useModal();
|
||||
|
||||
function MetricTypeList() {
|
||||
const { showModal } = useModal();
|
||||
const onClick = ({ slug }: MetricType) => {
|
||||
if (slug === 'library') {
|
||||
showModal(<MetricsLibraryModal />, { right: true, width: 700 });
|
||||
hideModal();
|
||||
if (slug === LIBRARY) {
|
||||
return showModal(<MetricsLibraryModal siteId={siteId} dashboardId={dashboardId} />, { right: true, width: 800 });
|
||||
}
|
||||
|
||||
// TODO redirect to card builder with metricType query param
|
||||
const path = withSiteId(dashboardMetricCreate(dashboardId + ''), siteId);
|
||||
history.push(path);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{METRIC_TYPES.map((metric: MetricType) => (
|
||||
{TYPES.map((metric: MetricType) => (
|
||||
<MetricTypeItem metric={metric} onClick={() => onClick(metric)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricTypeList;
|
||||
export default withRouter(MetricTypeList);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,65 @@
|
|||
import Modal from 'App/components/Modal/Modal';
|
||||
import React from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import MetricsList from '../MetricsList';
|
||||
import { Button } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
interface Props {
|
||||
dashboardId: number;
|
||||
siteId: string;
|
||||
}
|
||||
function MetricsLibraryModal(props: Props) {
|
||||
const { siteId, dashboardId } = props;
|
||||
const [selectedList, setSelectedList] = useState([]);
|
||||
|
||||
console.log('dashboardId', dashboardId);
|
||||
|
||||
const onSelectionChange = (list: any) => {
|
||||
setSelectedList(list);
|
||||
};
|
||||
|
||||
function MetricsLibraryModal() {
|
||||
return (
|
||||
<>
|
||||
<Modal.Header title="Cards Library" />
|
||||
<Modal.Content>Hello</Modal.Content>
|
||||
<Modal.Content>
|
||||
<div className="border">
|
||||
<MetricsList siteId={siteId} onSelectionChange={onSelectionChange} />
|
||||
</div>
|
||||
{/* TODO should show the dynamic values */}
|
||||
<SelectedContent dashboardId={dashboardId} selected={selectedList} />
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsLibraryModal;
|
||||
|
||||
function SelectedContent({ dashboardId, selected }: any) {
|
||||
const { hideModal } = useModal();
|
||||
const { metricStore, dashboardStore } = useStore();
|
||||
const total = useObserver(() => metricStore.sortedWidgets.length);
|
||||
const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]);
|
||||
|
||||
const addSelectedToDashboard = () => {
|
||||
dashboardStore.addWidgetToDashboard(dashboard, selected).then(hideModal);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center rounded border bg-gray-light-shade absolute justify-between p-3 left-4 right-4 bottom-4">
|
||||
<div>
|
||||
Selected <span className="font-medium">{selected.length}</span> of{' '}
|
||||
<span className="font-medium">{total}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button variant="text-primary" className="mr-2" onClick={hideModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={selected.length === 0} variant="primary" onClick={addSelectedToDashboard}>
|
||||
Add Selected to Dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,18 +7,31 @@ import MetricListItem from '../MetricListItem';
|
|||
import { sliceListPerPage } from 'App/utils';
|
||||
import Widget from 'App/mstore/types/widget';
|
||||
|
||||
function MetricsList({ siteId }: { siteId: string }) {
|
||||
function MetricsList({
|
||||
siteId,
|
||||
onSelectionChange = () => {},
|
||||
}: {
|
||||
siteId: string;
|
||||
onSelectionChange?: (selected: any[]) => void;
|
||||
}) {
|
||||
const { metricStore } = useStore();
|
||||
const metrics = metricStore.sortedWidgets;
|
||||
const metricsSearch = metricStore.metricsSearch;
|
||||
const [selectedMetrics, setSelectedMetrics] = useState([]);
|
||||
const [selectedMetrics, setSelectedMetrics] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectionChange(selectedMetrics);
|
||||
}, [selectedMetrics]);
|
||||
|
||||
const toggleMetricSelection = (id: any) => {
|
||||
console.log('id', id);
|
||||
if (selectedMetrics.includes(id)) {
|
||||
selectedMetrics.splice(selectedMetrics.indexOf(id), 1);
|
||||
setSelectedMetrics(selectedMetrics.filter((i: number) => i !== id));
|
||||
} else {
|
||||
selectedMetrics.push(id);
|
||||
setSelectedMetrics([...selectedMetrics, id]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -48,7 +61,7 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border-b rounded bg-white">
|
||||
<div className="mt-3 rounded bg-white">
|
||||
<div className="grid grid-cols-12 py-2 font-medium px-6">
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Checkbox
|
||||
|
|
@ -70,7 +83,7 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
<MetricListItem
|
||||
metric={metric}
|
||||
siteId={siteId}
|
||||
selected={selectedMetrics[parseInt(metric.metricId)]}
|
||||
selected={selectedMetrics.includes(parseInt(metric.metricId))}
|
||||
toggleSelection={(e: any) => {
|
||||
e.stopPropagation();
|
||||
toggleMetricSelection(parseInt(metric.metricId));
|
||||
|
|
@ -80,7 +93,7 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
))}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4 px-6">
|
||||
<div className="w-full flex items-center justify-between py-4 px-6 border-t">
|
||||
<div className="text-disabled-text">
|
||||
Showing{' '}
|
||||
<span className="font-semibold">{Math.min(list.length, metricStore.pageSize)}</span> out
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import MetricsList from '../MetricsList';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import MetricViewHeader from '../MetricViewHeader';
|
||||
|
||||
|
|
@ -9,14 +8,8 @@ interface Props {
|
|||
siteId: string;
|
||||
}
|
||||
function MetricsView({ siteId }: Props) {
|
||||
const { metricStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
|
||||
return useObserver(() => (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }} className="bg-white rounded py-4 border">
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }} className="bg-white rounded pt-4 border">
|
||||
<MetricViewHeader />
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import FilterSeries from '../FilterSeries';
|
|||
import { confirm, Tooltip } from 'UI';
|
||||
import Select from 'Shared/Select'
|
||||
import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'
|
||||
import MetricTypeDropdown from './components/MetricTypeDropdown';
|
||||
import MetricSubtypeDropdown from './components/MetricSubtypeDropdown';
|
||||
|
||||
interface Props {
|
||||
history: any;
|
||||
|
|
@ -103,18 +105,10 @@ function WidgetForm(props: Props) {
|
|||
<div className="form-group">
|
||||
<label className="font-medium">Metric Type</label>
|
||||
<div className="flex items-center">
|
||||
<SegmentSelection
|
||||
icons
|
||||
outline
|
||||
name="metricType"
|
||||
className="my-3"
|
||||
onSelect={ onSelect }
|
||||
value={metricTypes.find((i) => i.value === metric.metricType) || metricTypes[0]}
|
||||
// @ts-ignore
|
||||
list={metricTypes.map((i) => ({ value: i.value, name: i.label, icon: metricIcons[i.value] }))}
|
||||
/>
|
||||
<MetricTypeDropdown onSelect={writeOption} />
|
||||
<MetricSubtypeDropdown onSelect={writeOption} />
|
||||
|
||||
{metric.metricType === 'timeseries' && (
|
||||
{/* {metric.metricType === 'timeseries' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<Select
|
||||
|
|
@ -124,9 +118,9 @@ function WidgetForm(props: Props) {
|
|||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{metric.metricType === 'table' && (
|
||||
{/* {metric.metricType === 'table' && (
|
||||
<>
|
||||
<span className="mx-3">of</span>
|
||||
<Select
|
||||
|
|
@ -136,7 +130,7 @@ function WidgetForm(props: Props) {
|
|||
onChange={ writeOption }
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
|
||||
{metric.metricOf === FilterKey.ISSUE && (
|
||||
<>
|
||||
|
|
@ -177,7 +171,7 @@ function WidgetForm(props: Props) {
|
|||
variant="text-primary"
|
||||
onClick={() => metric.addSeries()}
|
||||
disabled={!canAddSeries}
|
||||
>Add Series</Button>
|
||||
>ADD</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { TYPES } from 'App/constants/card';
|
||||
import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem';
|
||||
import React from 'react';
|
||||
import Select from 'Shared/Select';
|
||||
import { components } from 'react-select';
|
||||
import CustomDropdownOption from 'Shared/CustomDropdownOption';
|
||||
|
||||
interface Props {
|
||||
onSelect: any;
|
||||
}
|
||||
function MetricSubtypeDropdown(props: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const metric: any = useObserver(() => metricStore.instance);
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
const type = TYPES.find((i: MetricType) => i.slug === metric.metricType);
|
||||
if (type && type.subTypes) {
|
||||
const options = type.subTypes.map((i: MetricType) => ({
|
||||
label: i.title,
|
||||
icon: i.icon,
|
||||
value: i.slug,
|
||||
description: i.description,
|
||||
}));
|
||||
return options;
|
||||
}
|
||||
return false;
|
||||
}, [metric.metricType]);
|
||||
|
||||
return options ? (
|
||||
<>
|
||||
<div className="mx-3">of</div>
|
||||
<Select
|
||||
name="metricOf"
|
||||
placeholder="Select Card Type"
|
||||
options={options}
|
||||
value={options.find((i: any) => i.value === metric.metricOf)}
|
||||
onChange={props.onSelect}
|
||||
// className="mx-2"
|
||||
components={{
|
||||
MenuList: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<components.MenuList {...props} className="!p-3">
|
||||
{children}
|
||||
</components.MenuList>
|
||||
);
|
||||
},
|
||||
Option: ({ children, ...props }: any) => {
|
||||
const { data } = props;
|
||||
return <CustomDropdownOption children={children} {...props} {...data} />;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default MetricSubtypeDropdown;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricSubtypeDropdown';
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
import MetricTypeDropdown from './';
|
||||
|
||||
export default {
|
||||
title: 'Dashboad/Cards/Form/MetricTypeDropdown',
|
||||
component: MetricTypeDropdown,
|
||||
};
|
||||
|
||||
const Template = (args: any) => <MetricTypeDropdown {...args} />;
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { TYPES, LIBRARY } from 'App/constants/card';
|
||||
import Select from 'Shared/Select';
|
||||
import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem';
|
||||
import { components } from 'react-select';
|
||||
import CustomDropdownOption from 'Shared/CustomDropdownOption';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
interface Props {
|
||||
onSelect: any;
|
||||
}
|
||||
function MetricTypeDropdown(props: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const metric: any = useObserver(() => metricStore.instance);
|
||||
const options: any = useMemo(() => {
|
||||
// TYPES.shift(); // remove "Add from library" item
|
||||
return TYPES.filter((i: MetricType) => i.slug !== LIBRARY).map((i: MetricType) => ({
|
||||
label: i.title,
|
||||
icon: i.icon,
|
||||
value: i.slug,
|
||||
description: i.description,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const onSelect = (_: any, option: Record<string, any>) =>
|
||||
props.onSelect({ value: { value: option.value }, name: option.name });
|
||||
|
||||
return (
|
||||
<Select
|
||||
name="metricType"
|
||||
placeholder="Select Card Type"
|
||||
options={options}
|
||||
value={options.find((i: any) => i.value === metric.metricType) || options[0]}
|
||||
onChange={props.onSelect}
|
||||
// onSelect={onSelect}
|
||||
components={{
|
||||
MenuList: ({ children, ...props }: any) => {
|
||||
return (
|
||||
<components.MenuList {...props} className="!p-3">
|
||||
{children}
|
||||
</components.MenuList>
|
||||
);
|
||||
},
|
||||
Option: ({ children, ...props }: any) => {
|
||||
const { data } = props;
|
||||
return <CustomDropdownOption children={children} {...props} {...data} />;
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricTypeDropdown;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricTypeDropdown';
|
||||
|
|
@ -100,7 +100,7 @@ function WidgetView(props: Props) {
|
|||
</h1>
|
||||
<div className="text-gray-600 w-full cursor-pointer" onClick={() => setExpanded(!expanded)}>
|
||||
<div className="flex items-center select-none w-fit ml-auto">
|
||||
<span className="mr-2 color-teal">{expanded ? 'Close' : 'Edit'}</span>
|
||||
<span className="mr-2 color-teal">{expanded ? 'Collapse' : 'Edit'}</span>
|
||||
<Icon name={expanded ? 'chevron-up' : 'chevron-down'} size="16" color="teal" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Modal.Header = ({ title }: { title: string }) => {
|
|||
};
|
||||
|
||||
Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => {
|
||||
return <div className={cn('h-screen overflow-y-auto', className)}>{children}</div>;
|
||||
return <div className={cn('overflow-y-auto relative', className)} style={{ height: 'calc(100vh - 52px)'}}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import { components, OptionProps } from 'react-select';
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
|
||||
export interface Props extends OptionProps {
|
||||
icon?: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
function CustomDropdownOption(props: Props) {
|
||||
const { icon = '', label, description, isSelected, isFocused } = props;
|
||||
return (
|
||||
<components.Option {...props} className="!p-0 mb-2">
|
||||
<div
|
||||
className={cn(
|
||||
'group p-2 flex item-start border border-transparent rounded hover:border-teal hover:!bg-active-blue !leading-0'
|
||||
)}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
// @ts-ignore
|
||||
name={icon}
|
||||
className="pt-2 mr-3"
|
||||
size={18}
|
||||
color={isSelected || isFocused ? 'teal' : 'gray-dark'}
|
||||
/>
|
||||
)}
|
||||
<div className={cn('flex flex-col', { '!color-teal': isFocused || isSelected })}>
|
||||
<div className="font-medium leading-0">{label}</div>
|
||||
<div className="text-sm color-gray-dark">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomDropdownOption;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './CustomDropdownOption';
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import SankeyChart from './SankeyChart';
|
||||
import SankeyChart, { SankeyChartData } from './SankeyChart';
|
||||
import React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
const data = {
|
||||
const data: SankeyChartData = {
|
||||
nodes: [
|
||||
{ name: 'Home Page' },
|
||||
{ name: 'Dashboard' },
|
||||
{ name: 'Preferences' },
|
||||
{ name: 'Billing' },
|
||||
|
||||
],
|
||||
links: [
|
||||
{ source: 0, target: 1, value: 100 },
|
||||
|
|
@ -17,4 +17,12 @@ const data = {
|
|||
],
|
||||
};
|
||||
|
||||
storiesOf('SankeyChart', module).add('Pure', () => <SankeyChart data={data} />);
|
||||
export default {
|
||||
title: 'Dashboad/Cards/SankeyChart',
|
||||
component: SankeyChart,
|
||||
} as ComponentMeta<typeof SankeyChart>;
|
||||
|
||||
const Template: ComponentStory<typeof SankeyChart> = (args: any) => <SankeyChart {...args} />;
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
Simple.args = { data };
|
||||
|
|
|
|||
|
|
@ -1,11 +1,27 @@
|
|||
import React from 'react';
|
||||
import { Sankey, Tooltip, Rectangle, Layer, ResponsiveContainer } from 'recharts';
|
||||
|
||||
type Node = {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type Link = {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface SankeyChartData {
|
||||
links: Link[];
|
||||
nodes: Node[];
|
||||
}
|
||||
interface Props {
|
||||
data: any;
|
||||
data: SankeyChartData;
|
||||
nodePadding?: number;
|
||||
nodeWidth?: number;
|
||||
}
|
||||
function SankeyChart(props: Props) {
|
||||
const { data } = props;
|
||||
const { data, nodePadding = 50, nodeWidth = 10 } = props;
|
||||
return (
|
||||
<div className="rounded border shadow">
|
||||
<div className="text-lg p-3 border-b bg-gray-lightest">Sankey Chart</div>
|
||||
|
|
@ -17,8 +33,8 @@ function SankeyChart(props: Props) {
|
|||
data={data}
|
||||
// node={{ stroke: '#77c878', strokeWidth: 0 }}
|
||||
node={<CustomNodeComponent />}
|
||||
nodePadding={50}
|
||||
nodeWidth={10}
|
||||
nodePadding={nodePadding}
|
||||
nodeWidth={nodeWidth}
|
||||
margin={{
|
||||
left: 10,
|
||||
right: 100,
|
||||
|
|
@ -33,7 +49,7 @@ function SankeyChart(props: Props) {
|
|||
<stop offset="100%" stopColor="rgba(0, 197, 159, 0.3)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Tooltip content={<CustomTooltip /> }/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</Sankey>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
|
@ -44,10 +60,7 @@ function SankeyChart(props: Props) {
|
|||
export default SankeyChart;
|
||||
|
||||
const CustomTooltip = (props: any) => {
|
||||
console.log('props', props);
|
||||
return (
|
||||
<div className="rounded bg-white border p-0 px-1 text-sm">test</div>
|
||||
)
|
||||
return <div className="rounded bg-white border p-0 px-1 text-sm">test</div>;
|
||||
// if (active && payload && payload.length) {
|
||||
// return (
|
||||
// <div className="custom-tooltip">
|
||||
|
|
|
|||
213
frontend/app/constants/card.ts
Normal file
213
frontend/app/constants/card.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { IconNames } from 'App/components/ui/SVG';
|
||||
import { FilterKey, IssueType } from 'Types/filter/filterType';
|
||||
|
||||
export interface CardType {
|
||||
title: string;
|
||||
icon?: IconNames;
|
||||
description: string;
|
||||
slug: string;
|
||||
subTypes?: CardType[];
|
||||
}
|
||||
|
||||
export const LIBRARY = 'library';
|
||||
export const TIMESERIES = 'timeseries';
|
||||
export const TABLE = 'table';
|
||||
|
||||
export const TYPES: CardType[] = [
|
||||
{
|
||||
title: 'Add From Library',
|
||||
icon: 'grid',
|
||||
description: 'Select a pre existing card from card library',
|
||||
slug: LIBRARY,
|
||||
},
|
||||
{
|
||||
title: 'Timeseries',
|
||||
icon: 'graph-up',
|
||||
description: 'Trend of sessions count in over the time.',
|
||||
slug: TIMESERIES,
|
||||
subTypes: [{ title: 'Session Count', slug: 'sessionCount', description: '' }],
|
||||
},
|
||||
{
|
||||
title: 'Table',
|
||||
icon: 'list-alt',
|
||||
description: 'See list of Users, Sessions, Errors, Issues, etc.,',
|
||||
slug: TABLE,
|
||||
subTypes: [
|
||||
{ title: 'Users', slug: FilterKey.USERID, description: '' },
|
||||
{ title: 'Sessions', slug: FilterKey.SESSIONS, description: '' },
|
||||
{ title: 'JS Errors', slug: FilterKey.ERRORS, description: '' },
|
||||
{ title: 'Issues', slug: FilterKey.ISSUE, description: '' },
|
||||
{ title: 'Browser', slug: FilterKey.USER_BROWSER, description: '' },
|
||||
{ title: 'Devices', slug: FilterKey.USER_DEVICE, description: '' },
|
||||
{ title: 'Countries', slug: FilterKey.USER_COUNTRY, description: '' },
|
||||
{ title: 'URLs', slug: FilterKey.LOCATION, description: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Funnel',
|
||||
icon: 'funnel',
|
||||
description: 'Uncover the issues impacting user journeys.',
|
||||
slug: 'funnel',
|
||||
},
|
||||
{
|
||||
title: 'Errors Tracking',
|
||||
icon: 'exclamation-circle',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'errors',
|
||||
subTypes: [
|
||||
{ title: 'Resources by Party', slug: FilterKey.RESOURCES_BY_PARTY, description: '' },
|
||||
{ title: 'Errors per Domains', slug: FilterKey.ERRORS_PER_DOMAINS, description: '' },
|
||||
{ title: 'Errors per type', slug: FilterKey.ERRORS_PER_TYPE, description: '' },
|
||||
{ title: 'Calls_Errors', slug: FilterKey.CALLS_ERRORS, description: '' },
|
||||
{ title: 'Domains_Errors_4xx', slug: FilterKey.DOMAINS_ERRORS_4XX, description: '' },
|
||||
{ title: 'Domains_Errors_5xx', slug: FilterKey.DOMAINS_ERRORS_5XX, description: '' },
|
||||
{
|
||||
title: 'Impacted_Sessions_By_Js_Errors',
|
||||
slug: FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Performance Monitoring',
|
||||
icon: 'speedometer2',
|
||||
description: 'Retention graph of users / features over a period of time.',
|
||||
slug: 'performance',
|
||||
subTypes: [
|
||||
{ title: 'Cpu', slug: FilterKey.CPU, description: '' },
|
||||
{ title: 'Crashes', slug: FilterKey.CRASHES, description: '' },
|
||||
{ title: 'Fps', slug: FilterKey.FPS, description: '' },
|
||||
{ title: 'Pages_Dom_Build_Time', slug: FilterKey.PAGES_DOM_BUILD_TIME, description: '' },
|
||||
{ title: 'Memory_Consumption', slug: FilterKey.MEMORY_CONSUMPTION, description: '' },
|
||||
{ title: 'Pages_Response_Time', slug: FilterKey.PAGES_RESPONSE_TIME, description: '' },
|
||||
{
|
||||
title: 'Pages_Response_Time_Distribution',
|
||||
slug: FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION,
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
title: 'Resources_Vs_Visually_Complete',
|
||||
slug: FilterKey.RESOURCES_VS_VISUALLY_COMPLETE,
|
||||
description: '',
|
||||
},
|
||||
{ title: 'Sessions_Per_Browser', slug: FilterKey.SESSIONS_PER_BROWSER, description: '' },
|
||||
{ title: 'Slowest_Domains', slug: FilterKey.SLOWEST_DOMAINS, description: '' },
|
||||
{ title: 'Speed_Location', slug: FilterKey.SPEED_LOCATION, description: '' },
|
||||
{ title: 'Time_To_Render', slug: FilterKey.TIME_TO_RENDER, description: '' },
|
||||
{
|
||||
title: 'Impacted_Sessions_By_Slow_Pages',
|
||||
slug: FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Resource Monitoring',
|
||||
icon: 'files',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'resource-monitoring',
|
||||
subTypes: [
|
||||
{
|
||||
title: 'Breakdown_Of_Loaded_Resources',
|
||||
slug: FilterKey.BREAKDOWN_OF_LOADED_RESOURCES,
|
||||
description: '',
|
||||
},
|
||||
{ title: 'Missing_Resources', slug: FilterKey.MISSING_RESOURCES, description: '' },
|
||||
{
|
||||
title: 'Resource_Type_Vs_Response_End',
|
||||
slug: FilterKey.RESOURCE_TYPE_VS_RESPONSE_END,
|
||||
description: '',
|
||||
},
|
||||
{ title: 'Resource_Fetch_Time', slug: FilterKey.RESOURCE_FETCH_TIME, description: '' },
|
||||
{ title: 'Slowest_Resources', slug: FilterKey.SLOWEST_RESOURCES, description: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Web Vitals',
|
||||
icon: 'activity',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'web-vitals',
|
||||
subTypes: [
|
||||
{
|
||||
title: 'Resources_Count_By_Type',
|
||||
slug: FilterKey.RESOURCES_COUNT_BY_TYPE,
|
||||
description: '',
|
||||
},
|
||||
{ title: 'Resources_Loading_Time', slug: FilterKey.RESOURCES_LOADING_TIME, description: '' },
|
||||
{
|
||||
title: 'CPU Load',
|
||||
slug: FilterKey.AVG_CPU,
|
||||
description: 'Uncover the issues impacting user journeys',
|
||||
},
|
||||
{
|
||||
title: 'DOM Build Time',
|
||||
slug: FilterKey.AVG_DOM_CONTENT_LOADED,
|
||||
description: 'Keep a close eye on errors and track their type, origin and domain.',
|
||||
},
|
||||
{
|
||||
title: 'DOM Content Loaded Start',
|
||||
slug: FilterKey.AVG_DOM_CONTENT_LOAD_START,
|
||||
description:
|
||||
'FInd out which resources are missing and those that may be slowign your web app.',
|
||||
},
|
||||
{
|
||||
title: 'DOM Content Loaded',
|
||||
slug: FilterKey.AVG_FIRST_CONTENTFUL_PIXEL,
|
||||
description:
|
||||
"Optimize your app's performance by tracking slow domains, page resposne times, memory consumption, CPU usage and more.",
|
||||
},
|
||||
{
|
||||
title: 'First Paint',
|
||||
slug: FilterKey.AVG_FIRST_PAINT,
|
||||
description:
|
||||
'Find out which resources are missing and those that may be slowing your web app.',
|
||||
},
|
||||
{ title: 'Frame Rate', slug: FilterKey.AVG_FPS, description: '' },
|
||||
{
|
||||
title: 'Image Load Time',
|
||||
slug: FilterKey.AVG_IMAGE_LOAD_TIME,
|
||||
description:
|
||||
'Find out which resources are missing and those that may be slowing your web app.',
|
||||
},
|
||||
{ title: 'Page Load Time', slug: FilterKey.AVG_PAGE_LOAD_TIME, description: '' },
|
||||
{ title: 'DOM Build Time', slug: FilterKey.AVG_PAGES_DOM_BUILD_TIME, description: '' },
|
||||
{ title: 'Pages Response Time', slug: FilterKey.AVG_PAGES_RESPONSE_TIME, description: '' },
|
||||
{ title: 'Request Load Time', slug: FilterKey.AVG_REQUEST_LOADT_IME, description: '' },
|
||||
{ title: 'Response Time ', slug: FilterKey.AVG_RESPONSE_TIME, description: '' },
|
||||
{ title: 'Session Dueration', slug: FilterKey.AVG_SESSION_DURATION, description: '' },
|
||||
{ title: 'Time Till First Byte', slug: FilterKey.AVG_TILL_FIRST_BYTE, description: '' },
|
||||
{ title: 'Time to be Interactive', slug: FilterKey.AVG_TIME_TO_INTERACTIVE, description: '' },
|
||||
{ title: 'Time to Render', slug: FilterKey.AVG_TIME_TO_RENDER, description: '' },
|
||||
{ title: 'JS Heap Size', slug: FilterKey.AVG_USED_JS_HEAP_SIZE, description: '' },
|
||||
{ title: 'Visited Pages', slug: FilterKey.AVG_VISITED_PAGES, description: '' },
|
||||
{
|
||||
title: 'Captured Requests',
|
||||
slug: FilterKey.COUNT_REQUESTS,
|
||||
description: 'Trend of sessions count in over the time.',
|
||||
},
|
||||
{
|
||||
title: 'Captured Sessions',
|
||||
slug: FilterKey.COUNT_SESSIONS,
|
||||
description: 'See list of users, sessions, errors, issues, etc.,',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'User Path',
|
||||
icon: 'signpost-split',
|
||||
description: 'Discover user journeys between 2 points.',
|
||||
slug: 'user-path',
|
||||
},
|
||||
{
|
||||
title: 'Retention',
|
||||
icon: 'arrow-repeat',
|
||||
description: 'Retension graph of users / features over a period of time.',
|
||||
slug: 'retention',
|
||||
},
|
||||
{
|
||||
title: 'Feature Adoption',
|
||||
icon: 'card-checklist',
|
||||
description: 'Find the adoption of your all features in your app.',
|
||||
slug: 'feature-adoption',
|
||||
},
|
||||
];
|
||||
|
|
@ -277,9 +277,9 @@ export default class DashboardStore {
|
|||
);
|
||||
}
|
||||
|
||||
getDashboard(dashboardId: string): Dashboard | null {
|
||||
getDashboard(dashboardId: string|number): Dashboard | null {
|
||||
return (
|
||||
this.dashboards.find((d) => d.dashboardId === dashboardId) || null
|
||||
this.dashboards.find((d) => d.dashboardId == dashboardId) || null
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,4 +220,58 @@ export enum FilterKey {
|
|||
|
||||
SESSIONS = 'SESSIONS',
|
||||
ERRORS = 'js_exception',
|
||||
|
||||
RESOURCES_COUNT_BY_TYPE = 'resourcesCountByType',
|
||||
RESOURCES_LOADING_TIME = 'resourcesLoadingTime',
|
||||
AVG_CPU = 'avgCpu',
|
||||
AVG_DOM_CONTENT_LOADED = 'avgDomContentLoaded',
|
||||
AVG_DOM_CONTENT_LOAD_START = 'avgDomContentLoadStart',
|
||||
AVG_FIRST_CONTENTFUL_PIXEL = 'avgFirstContentfulPixel',
|
||||
AVG_FIRST_PAINT = 'avgFirstPaint',
|
||||
AVG_FPS = 'avgFps',
|
||||
AVG_IMAGE_LOAD_TIME = 'avgImageLoadTime',
|
||||
AVG_PAGE_LOAD_TIME = 'avgPageLoadTime',
|
||||
AVG_PAGES_DOM_BUILD_TIME = 'avgPagesDomBuildtime',
|
||||
AVG_PAGES_RESPONSE_TIME = 'avgPagesResponseTime',
|
||||
AVG_REQUEST_LOADT_IME = 'avgRequestLoadTime',
|
||||
AVG_RESPONSE_TIME = 'avgResponseTime',
|
||||
AVG_SESSION_DURATION = 'avgSessionDuration',
|
||||
AVG_TILL_FIRST_BYTE = 'avgTillFirstByte',
|
||||
AVG_TIME_TO_INTERACTIVE = 'avgTimeToInteractive',
|
||||
AVG_TIME_TO_RENDER = 'avgTimeToRender',
|
||||
AVG_USED_JS_HEAP_SIZE = 'avgUsedJsHeapSize',
|
||||
AVG_VISITED_PAGES = 'avgVisitedPages',
|
||||
COUNT_REQUESTS = 'countRequests',
|
||||
COUNT_SESSIONS = 'countSessions',
|
||||
|
||||
// Errors
|
||||
RESOURCES_BY_PARTY = 'resourcesByParty',
|
||||
ERRORS_PER_DOMAINS = 'errorsPerDomains',
|
||||
ERRORS_PER_TYPE = 'errorsPerType',
|
||||
CALLS_ERRORS = 'callsErrors',
|
||||
DOMAINS_ERRORS_4XX = 'domainsErrors4Xx',
|
||||
DOMAINS_ERRORS_5XX = 'domainsErrors5Xx',
|
||||
IMPACTED_SESSIONS_BY_JS_ERRORS = 'impactedSessionsByJsErrors',
|
||||
|
||||
// Performance
|
||||
CPU = 'cpu',
|
||||
CRASHES = 'crashes',
|
||||
FPS = 'fps',
|
||||
PAGES_DOM_BUILD_TIME = 'pagesDomBuildtime',
|
||||
MEMORY_CONSUMPTION = 'memoryConsumption',
|
||||
PAGES_RESPONSE_TIME = 'pagesResponseTime',
|
||||
PAGES_RESPONSE_TIME_DISTRIBUTION = 'pagesResponseTimeDistribution',
|
||||
RESOURCES_VS_VISUALLY_COMPLETE = 'resourcesVsVisuallyComplete',
|
||||
SESSIONS_PER_BROWSER = 'sessionsPerBrowser',
|
||||
SLOWEST_DOMAINS = 'slowestDomains',
|
||||
SPEED_LOCATION = 'speedLocation',
|
||||
TIME_TO_RENDER = 'timeToRender',
|
||||
IMPACTED_SESSIONS_BY_SLOW_PAGES = 'impactedSessionsBySlowPages',
|
||||
|
||||
// Resources
|
||||
BREAKDOWN_OF_LOADED_RESOURCES = 'breakdownOfLoadedResources',
|
||||
MISSING_RESOURCES = 'missingResources',
|
||||
RESOURCE_TYPE_VS_RESPONSE_END = 'resourceTypeVsResponseEnd',
|
||||
RESOURCE_FETCH_TIME = 'resourceFetchTime',
|
||||
SLOWEST_RESOURCES = 'slowestResources',
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue