change(ui) - dashboard updates - wip
This commit is contained in:
parent
e04c9d7816
commit
809ddcc2c2
20 changed files with 484 additions and 324 deletions
|
|
@ -0,0 +1,16 @@
|
|||
import Modal from 'App/components/Modal/Modal';
|
||||
import React from 'react';
|
||||
import MetricTypeList from '../MetricTypeList';
|
||||
|
||||
function AddCardModal() {
|
||||
return (
|
||||
<>
|
||||
<Modal.Header title="Add Card" />
|
||||
<Modal.Content className="p-0">
|
||||
<MetricTypeList />
|
||||
</Modal.Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddCardModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './AddCardModal';
|
||||
|
|
@ -11,6 +11,7 @@ import DashboardOptions from '../DashboardOptions';
|
|||
import withModal from 'App/components/Modal/withModal';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import DashboardEditModal from '../DashboardEditModal';
|
||||
import AddCardModal from '../AddCardModal';
|
||||
|
||||
interface IProps {
|
||||
siteId: string;
|
||||
|
|
@ -23,7 +24,7 @@ function DashboardHeader(props: Props) {
|
|||
const { siteId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const { showModal } = useModal();
|
||||
const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
// const [showTooltip, setShowTooltip] = React.useState(false);
|
||||
const [focusTitle, setFocusedInput] = React.useState(true);
|
||||
const [showEditModal, setShowEditModal] = React.useState(false);
|
||||
const period = dashboardStore.period;
|
||||
|
|
@ -79,7 +80,11 @@ function DashboardHeader(props: Props) {
|
|||
/>
|
||||
</div>
|
||||
<div className="flex items-center" style={{ flex: 1, justifyContent: 'end' }}>
|
||||
<Button variant="primary" onClick={() => setShowTooltip(true)} icon="plus">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => showModal(<AddCardModal />, { right: true })}
|
||||
icon="plus"
|
||||
>
|
||||
Add Card
|
||||
</Button>
|
||||
<div className="mx-4"></div>
|
||||
|
|
|
|||
|
|
@ -1,52 +1,12 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon } from 'UI';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withSiteId } from 'App/routes';
|
||||
import Select from 'Shared/Select';
|
||||
import DashboardList from './DashboardList';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
import { sort } from 'App/duck/sessions';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import Header from './Header';
|
||||
|
||||
function DashboardsView({ history, siteId }: { history: any; siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
const sort = useObserver(() => dashboardStore.sort);
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => {
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }} className="bg-white rounded py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>
|
||||
New Dashboard
|
||||
</Button>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Header history={history} siteId={siteId} />
|
||||
<DashboardList />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle } from 'UI';
|
||||
import Select from 'Shared/Select';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
function Header({ history, siteId }: { history: any; siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
const sort = useObserver(() => dashboardStore.sort);
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => {
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>
|
||||
New Dashboard
|
||||
</Button>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/4" style={{ minWidth: 300 }}>
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
|
|
@ -76,8 +76,8 @@ function DashboardView(props: Props) {
|
|||
return (
|
||||
<Loader loading={loading}>
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }}>
|
||||
{/* @ts-ignore */}
|
||||
<DashboardHeader renderReport={props.renderReport} />
|
||||
{/* @ts-ignore */}
|
||||
<DashboardHeader renderReport={props.renderReport} siteId={siteId} />
|
||||
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
|
|
|
|||
|
|
@ -1,66 +1,74 @@
|
|||
import React from 'react';
|
||||
import { Icon, Tooltip } from 'UI';
|
||||
import { Icon, Checkbox, Tooltip } from 'UI';
|
||||
import { checkForRecent } from 'App/date';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
metric: any;
|
||||
siteId: string;
|
||||
metric: any;
|
||||
siteId: string;
|
||||
selected?: boolean;
|
||||
toggleSelection?: any;
|
||||
}
|
||||
|
||||
function MetricTypeIcon({ type }: any) {
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'funnel':
|
||||
return 'filter';
|
||||
case 'table':
|
||||
return 'list-alt';
|
||||
case 'timeseries':
|
||||
return 'bar-chart-line';
|
||||
}
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case 'funnel':
|
||||
return 'filter';
|
||||
case 'table':
|
||||
return 'list-alt';
|
||||
case 'timeseries':
|
||||
return 'bar-chart-line';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={<div className="capitalize">{type}</div>}
|
||||
>
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon()} size="16" color="tealx" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
return (
|
||||
<Tooltip title={<div className="capitalize">{type}</div>} >
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name={getIcon()} size="16" color="tealx" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function MetricListItem(props: Props) {
|
||||
const { metric, history, siteId } = props;
|
||||
const { metric, history, siteId, selected, toggleSelection = () => {} } = props;
|
||||
|
||||
const onItemClick = () => {
|
||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div className="grid grid-cols-12 py-4 border-t select-none items-center hover:bg-active-blue cursor-pointer px-6" onClick={onItemClick}>
|
||||
<div className="col-span-4 flex items-start">
|
||||
<div className="flex items-center">
|
||||
<MetricTypeIcon type={metric.metricType} />
|
||||
<div className="link capitalize-first">
|
||||
{metric.name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-4">{metric.owner}</div>
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center">
|
||||
<Icon name={metric.isPublic ? "user-friends" : "person-fill"} className="mr-2" />
|
||||
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-right">{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
const onItemClick = () => {
|
||||
const path = withSiteId(`/metrics/${metric.metricId}`, siteId);
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-12 py-4 border-t select-none items-center hover:bg-active-blue cursor-pointer px-6"
|
||||
onClick={onItemClick}
|
||||
>
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-4"
|
||||
type="checkbox"
|
||||
checked={selected}
|
||||
onClick={toggleSelection}
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<MetricTypeIcon type={metric.metricType} />
|
||||
<div className="link capitalize-first">{metric.name}</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
<div className="col-span-4">{metric.owner}</div>
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center">
|
||||
<Icon name={metric.isPublic ? 'user-friends' : 'person-fill'} className="mr-2" />
|
||||
<span>{metric.isPublic ? 'Team' : 'Private'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-right">
|
||||
{metric.lastModified && checkForRecent(metric.lastModified, 'LLL dd, yyyy, hh:mm a')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(MetricListItem);
|
||||
|
|
|
|||
|
|
@ -11,14 +11,19 @@ export interface MetricType {
|
|||
|
||||
interface Props {
|
||||
metric: MetricType;
|
||||
onClick?: any;
|
||||
}
|
||||
|
||||
function MetricTypeItem(props: Props) {
|
||||
const {
|
||||
metric: { title, icon, description, slug },
|
||||
onClick = () => {},
|
||||
} = props;
|
||||
return (
|
||||
<div className="flex items-start p-4 hover:bg-active-blue cursor-pointer group hover-color-teal">
|
||||
<div
|
||||
className="flex items-start p-4 hover:bg-active-blue cursor-pointer group hover-color-teal"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="pr-4 pt-1">
|
||||
<Icon name={icon} size="20" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useModal } from 'App/components/Modal';
|
||||
import React from 'react';
|
||||
import MetricsLibraryModal from '../MetricsLibraryModal';
|
||||
import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem';
|
||||
|
||||
const METRIC_TYPES: MetricType[] = [
|
||||
|
|
@ -71,10 +73,16 @@ const METRIC_TYPES: MetricType[] = [
|
|||
];
|
||||
|
||||
function MetricTypeList() {
|
||||
const { showModal } = useModal();
|
||||
const onClick = ({ slug }: MetricType) => {
|
||||
if (slug === 'library') {
|
||||
showModal(<MetricsLibraryModal />, { right: true, width: 700 });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{METRIC_TYPES.map((metric: MetricType) => (
|
||||
<MetricTypeItem metric={metric} />
|
||||
<MetricTypeItem metric={metric} onClick={() => onClick(metric)} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { Icon, PageTitle, Button, Link } from 'UI';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import Select from 'Shared/Select';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function MetricViewHeader() {
|
||||
const { metricStore } = useStore();
|
||||
const sort = useObserver(() => metricStore.sort);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Link to={'/metrics/create'}>
|
||||
<Button variant="primary">Create</Button>
|
||||
</Link>
|
||||
<div className="mx-2">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Newest', value: 'desc' },
|
||||
{ label: 'Oldest', value: 'asc' },
|
||||
]}
|
||||
defaultValue={sort.by}
|
||||
plain
|
||||
onChange={({ value }) => metricStore.updateKey('sort', { by: value.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex items-center px-6">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Create custom Metrics to capture key interactions and track KPIs.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricViewHeader;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricViewHeader';
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import Modal from 'App/components/Modal/Modal';
|
||||
import React from 'react';
|
||||
|
||||
function MetricsLibraryModal() {
|
||||
return (
|
||||
<>
|
||||
<Modal.Header title="Cards Library" />
|
||||
<Modal.Content>Hello</Modal.Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricsLibraryModal;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './MetricsLibraryModal';
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useEffect } from 'react';
|
||||
import { NoContent, Pagination, Icon } from 'UI';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { NoContent, Pagination, Icon, Checkbox } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { filterList } from 'App/utils';
|
||||
import MetricListItem from '../MetricListItem';
|
||||
|
|
@ -11,6 +11,16 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
const { metricStore } = useStore();
|
||||
const metrics = metricStore.sortedWidgets;
|
||||
const metricsSearch = metricStore.metricsSearch;
|
||||
const [selectedMetrics, setSelectedMetrics] = useState([]);
|
||||
|
||||
const toggleMetricSelection = (id: any) => {
|
||||
console.log('id', id);
|
||||
if (selectedMetrics.includes(id)) {
|
||||
selectedMetrics.splice(selectedMetrics.indexOf(id), 1);
|
||||
} else {
|
||||
selectedMetrics.push(id);
|
||||
}
|
||||
};
|
||||
|
||||
const filterByDashboard = (item: Widget, searchRE: RegExp) => {
|
||||
const dashboardsStr = item.dashboards.map((d: any) => d.name).join(' ');
|
||||
|
|
@ -40,7 +50,16 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
>
|
||||
<div className="mt-3 border-b rounded bg-white">
|
||||
<div className="grid grid-cols-12 py-2 font-medium px-6">
|
||||
<div className="col-span-4">Title</div>
|
||||
<div className="col-span-4 flex items-center">
|
||||
<Checkbox
|
||||
name="slack"
|
||||
className="mr-4"
|
||||
type="checkbox"
|
||||
checked={false}
|
||||
onClick={() => setSelectedMetrics(list.map((i: any) => i.metricId))}
|
||||
/>
|
||||
<span>Title</span>
|
||||
</div>
|
||||
<div className="col-span-4">Owner</div>
|
||||
<div className="col-span-2">Visibility</div>
|
||||
<div className="col-span-2 text-right">Last Modified</div>
|
||||
|
|
@ -48,7 +67,15 @@ function MetricsList({ siteId }: { siteId: string }) {
|
|||
|
||||
{sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => (
|
||||
<React.Fragment key={metric.metricId}>
|
||||
<MetricListItem metric={metric} siteId={siteId} />
|
||||
<MetricListItem
|
||||
metric={metric}
|
||||
siteId={siteId}
|
||||
selected={selectedMetrics[parseInt(metric.metricId)]}
|
||||
toggleSelection={(e: any) => {
|
||||
e.stopPropagation();
|
||||
toggleMetricSelection(parseInt(metric.metricId));
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,40 +1,26 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon, Link } from 'UI';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import MetricsList from '../MetricsList';
|
||||
import MetricsSearch from '../MetricsSearch';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import MetricViewHeader from '../MetricViewHeader';
|
||||
|
||||
interface Props {
|
||||
siteId: string;
|
||||
siteId: string;
|
||||
}
|
||||
function MetricsView({ siteId }: Props) {
|
||||
const { metricStore } = useStore();
|
||||
const { metricStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
return useObserver(() => (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<Link to={'/metrics/create'}><Button variant="primary">Create Metric</Button></Link>
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
<MetricsSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-base text-disabled-text flex items-center px-6">
|
||||
<Icon name="info-circle-fill" className="mr-2" size={16} />
|
||||
Create custom Metrics to capture user frustrations, monitor your app's performance and track other KPIs.
|
||||
</div>
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
));
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
}, []);
|
||||
|
||||
return useObserver(() => (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto' }} className="bg-white rounded py-4 border">
|
||||
<MetricViewHeader />
|
||||
<MetricsList siteId={siteId} />
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export default withPageTitle('Metrics - OpenReplay')(MetricsView);
|
||||
export default withPageTitle('Cards - OpenReplay')(MetricsView);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,18 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import ModalOverlay from './ModalOverlay';
|
||||
import cn from 'classnames';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
export default function Modal({ component, props, hideModal }: any) {
|
||||
const DEFAULT_WIDTH = 350;
|
||||
interface Props {
|
||||
component: any;
|
||||
className?: string;
|
||||
props: any;
|
||||
hideModal?: boolean;
|
||||
width?: number;
|
||||
}
|
||||
function Modal({ component, className = 'bg-white', props, hideModal }: Props) {
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -12,11 +21,15 @@ export default function Modal({ component, props, hideModal }: any) {
|
|||
document.querySelector('body').style.overflow = 'visible';
|
||||
}
|
||||
});
|
||||
});
|
||||
return component ? (
|
||||
});return component ? (
|
||||
ReactDOM.createPortal(
|
||||
<ModalOverlay hideModal={hideModal} left={!props.right} right={props.right}>
|
||||
{component}
|
||||
<div
|
||||
className={className}
|
||||
style={{ width: `${props.width ? props.width : DEFAULT_WIDTH}px` }}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
</ModalOverlay>,
|
||||
document.querySelector('#modal-root')
|
||||
)
|
||||
|
|
@ -24,3 +37,17 @@ export default function Modal({ component, props, hideModal }: any) {
|
|||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
Modal.Header = ({ title }: { title: string }) => {
|
||||
return (
|
||||
<div className="text-lg flex items-center p-4 font-medium">
|
||||
<div>{title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => {
|
||||
return <div className={cn('h-screen overflow-y-auto', className)}>{children}</div>;
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
|
|
|||
|
|
@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react';
|
|||
import Modal from './Modal';
|
||||
|
||||
const ModalContext = createContext({
|
||||
component: null,
|
||||
props: {
|
||||
right: true,
|
||||
onClose: () => {},
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {},
|
||||
component: null,
|
||||
props: {
|
||||
right: true,
|
||||
onClose: () => {},
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {},
|
||||
});
|
||||
|
||||
export class ModalProvider extends Component {
|
||||
handleKeyDown = (e: any) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
showModal = (component, props = { right: true }) => {
|
||||
this.setState({
|
||||
component,
|
||||
props,
|
||||
});
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
hideModal = () => {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'visible';
|
||||
const { props } = this.state;
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
this.setState({
|
||||
component: null,
|
||||
props: {},
|
||||
});
|
||||
};
|
||||
|
||||
state = {
|
||||
component: null,
|
||||
get isModalActive() { return this.component !== null },
|
||||
props: {},
|
||||
showModal: this.showModal,
|
||||
hideModal: this.hideModal,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalContext.Provider value={this.state}>
|
||||
<Modal {...this.state} />
|
||||
{this.props.children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
handleKeyDown = (e: any) => {
|
||||
if (e.keyCode === 27) {
|
||||
this.hideModal();
|
||||
}
|
||||
};
|
||||
|
||||
showModal = (component, props = { right: true }) => {
|
||||
this.setState({
|
||||
component,
|
||||
props,
|
||||
});
|
||||
document.addEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'hidden';
|
||||
};
|
||||
|
||||
hideModal = () => {
|
||||
document.removeEventListener('keydown', this.handleKeyDown);
|
||||
document.querySelector('body').style.overflow = 'visible';
|
||||
const { props } = this.state;
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
this.setState({
|
||||
component: null,
|
||||
props: {},
|
||||
});
|
||||
};
|
||||
|
||||
state = {
|
||||
component: null,
|
||||
get isModalActive() { return this.component !== null },props: {},
|
||||
showModal: this.showModal,
|
||||
hideModal: this.hideModal,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ModalContext.Provider value={this.state}>
|
||||
<Modal {...this.state} />
|
||||
{this.props.children}
|
||||
</ModalContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ModalConsumer = ModalContext.Consumer;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import React from 'react';
|
||||
import { ModalConsumer } from './';
|
||||
|
||||
|
||||
export default BaseComponent => React.memo(props => (
|
||||
<ModalConsumer>
|
||||
{ value => <BaseComponent { ...value } { ...props } /> }
|
||||
</ModalConsumer>
|
||||
));
|
||||
export default (BaseComponent) =>
|
||||
React.memo((props) => (
|
||||
<ModalConsumer>{(value) => <BaseComponent {...value} {...props} />}</ModalConsumer>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export default class DashboardStore {
|
|||
return this.dashboards.filter((d) => ids.includes(d.dashboardId));
|
||||
}
|
||||
|
||||
initDashboard(dashboard: Dashboard) {
|
||||
initDashboard(dashboard?: Dashboard) {
|
||||
this.dashboardInstance = dashboard
|
||||
? new Dashboard().fromJson(dashboard)
|
||||
: new Dashboard();
|
||||
|
|
|
|||
|
|
@ -1,153 +1,164 @@
|
|||
import { makeAutoObservable, computed } from "mobx"
|
||||
import Widget from "./types/widget";
|
||||
import { metricService, errorService } from "App/services";
|
||||
import { makeAutoObservable, computed } from 'mobx';
|
||||
import Widget from './types/widget';
|
||||
import { metricService, errorService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import Error from "./types/error";
|
||||
import Error from './types/error';
|
||||
|
||||
export default class MetricStore {
|
||||
isLoading: boolean = false
|
||||
isSaving: boolean = false
|
||||
isLoading: boolean = false;
|
||||
isSaving: boolean = false;
|
||||
|
||||
metrics: Widget[] = []
|
||||
instance = new Widget()
|
||||
metrics: Widget[] = [];
|
||||
instance = new Widget();
|
||||
|
||||
page: number = 1
|
||||
pageSize: number = 10
|
||||
metricsSearch: string = ""
|
||||
sort: any = {}
|
||||
page: number = 1;
|
||||
pageSize: number = 10;
|
||||
metricsSearch: string = '';
|
||||
sort: any = { by: 'desc' };
|
||||
|
||||
sessionsPage: number = 1
|
||||
sessionsPageSize: number = 10
|
||||
sessionsPage: number = 1;
|
||||
sessionsPageSize: number = 10;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
@computed
|
||||
get sortedWidgets() {
|
||||
return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified);
|
||||
}
|
||||
|
||||
// State Actions
|
||||
init(metric?: Widget | null) {
|
||||
this.instance.update(metric || new Widget());
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
// @ts-ignore
|
||||
this[key] = value;
|
||||
}
|
||||
|
||||
merge(object: any) {
|
||||
Object.assign(this.instance, object);
|
||||
this.instance.updateKey('hasChanged', true);
|
||||
}
|
||||
|
||||
reset(id: string) {
|
||||
const metric = this.findById(id);
|
||||
if (metric) {
|
||||
this.instance = metric;
|
||||
}
|
||||
}
|
||||
|
||||
@computed
|
||||
get sortedWidgets() {
|
||||
return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified)
|
||||
addToList(metric: Widget) {
|
||||
this.metrics.push(metric);
|
||||
}
|
||||
|
||||
updateInList(metric: Widget) {
|
||||
// @ts-ignore
|
||||
const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY]);
|
||||
if (index >= 0) {
|
||||
this.metrics[index] = metric;
|
||||
}
|
||||
}
|
||||
|
||||
// State Actions
|
||||
init(metric?: Widget | null) {
|
||||
this.instance.update(metric || new Widget())
|
||||
}
|
||||
findById(id: string) {
|
||||
// @ts-ignore
|
||||
return this.metrics.find((m) => m[Widget.ID_KEY] === id);
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
// @ts-ignore
|
||||
this[key] = value
|
||||
}
|
||||
removeById(id: string): void {
|
||||
// @ts-ignore
|
||||
this.metrics = this.metrics.filter((m) => m[Widget.ID_KEY] !== id);
|
||||
}
|
||||
|
||||
merge(object: any) {
|
||||
Object.assign(this.instance, object)
|
||||
this.instance.updateKey('hasChanged', true)
|
||||
}
|
||||
get paginatedList(): Widget[] {
|
||||
const start = (this.page - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.metrics.slice(start, end);
|
||||
}
|
||||
|
||||
reset(id: string) {
|
||||
const metric = this.findById(id)
|
||||
if (metric) {
|
||||
this.instance = metric
|
||||
}
|
||||
}
|
||||
|
||||
addToList(metric: Widget) {
|
||||
this.metrics.push(metric)
|
||||
}
|
||||
|
||||
updateInList(metric: Widget) {
|
||||
// @ts-ignore
|
||||
const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY])
|
||||
if (index >= 0) {
|
||||
this.metrics[index] = metric
|
||||
}
|
||||
}
|
||||
|
||||
findById(id: string) {
|
||||
// @ts-ignore
|
||||
return this.metrics.find(m => m[Widget.ID_KEY] === id)
|
||||
}
|
||||
|
||||
removeById(id: string): void {
|
||||
// @ts-ignore
|
||||
this.metrics = this.metrics.filter(m => m[Widget.ID_KEY] !== id)
|
||||
}
|
||||
|
||||
get paginatedList(): Widget[] {
|
||||
const start = (this.page - 1) * this.pageSize
|
||||
const end = start + this.pageSize
|
||||
return this.metrics.slice(start, end)
|
||||
}
|
||||
|
||||
// API Communication
|
||||
save(metric: Widget, dashboardId?: string): Promise<any> {
|
||||
const wasCreating = !metric.exists()
|
||||
this.isSaving = true
|
||||
return new Promise((resolve, reject) => {
|
||||
metricService.saveMetric(metric, dashboardId)
|
||||
.then((metric: any) => {
|
||||
const _metric = new Widget().fromJson(metric)
|
||||
if (wasCreating) {
|
||||
toast.success('Metric created successfully')
|
||||
this.addToList(_metric)
|
||||
this.instance = _metric
|
||||
} else {
|
||||
toast.success('Metric updated successfully')
|
||||
this.updateInList(_metric)
|
||||
}
|
||||
resolve(_metric)
|
||||
}).catch(() => {
|
||||
toast.error('Error saving metric')
|
||||
reject()
|
||||
}).finally(() => {
|
||||
this.instance.updateKey('hasChanged', false)
|
||||
this.isSaving = false
|
||||
})
|
||||
// API Communication
|
||||
save(metric: Widget, dashboardId?: string): Promise<any> {
|
||||
const wasCreating = !metric.exists();
|
||||
this.isSaving = true;
|
||||
return new Promise((resolve, reject) => {
|
||||
metricService
|
||||
.saveMetric(metric, dashboardId)
|
||||
.then((metric: any) => {
|
||||
const _metric = new Widget().fromJson(metric);
|
||||
if (wasCreating) {
|
||||
toast.success('Metric created successfully');
|
||||
this.addToList(_metric);
|
||||
this.instance = _metric;
|
||||
} else {
|
||||
toast.success('Metric updated successfully');
|
||||
this.updateInList(_metric);
|
||||
}
|
||||
resolve(_metric);
|
||||
})
|
||||
}
|
||||
|
||||
fetchList() {
|
||||
this.isLoading = true
|
||||
return metricService.getMetrics()
|
||||
.then((metrics: any[]) => {
|
||||
this.metrics = metrics.map(m => new Widget().fromJson(m))
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
fetch(id: string, period?: any) {
|
||||
this.isLoading = true
|
||||
return metricService.getMetric(id)
|
||||
.then((metric: any) => {
|
||||
return this.instance = new Widget().fromJson(metric, period)
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
}
|
||||
|
||||
delete(metric: Widget) {
|
||||
this.isSaving = true
|
||||
// @ts-ignore
|
||||
return metricService.deleteMetric(metric[Widget.ID_KEY])
|
||||
.then(() => {
|
||||
// @ts-ignore
|
||||
this.removeById(metric[Widget.ID_KEY])
|
||||
toast.success('Metric deleted successfully')
|
||||
}).finally(() => {
|
||||
this.instance.updateKey('hasChanged', false)
|
||||
this.isSaving = false
|
||||
})
|
||||
}
|
||||
|
||||
fetchError(errorId: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
errorService.one(errorId).then((error: any) => {
|
||||
resolve(new Error().fromJSON(error))
|
||||
}).catch((error: any) => {
|
||||
toast.error('Failed to fetch error details.')
|
||||
reject(error)
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error('Error saving metric');
|
||||
reject();
|
||||
})
|
||||
}
|
||||
.finally(() => {
|
||||
this.instance.updateKey('hasChanged', false);
|
||||
this.isSaving = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fetchList() {
|
||||
this.isLoading = true;
|
||||
return metricService
|
||||
.getMetrics()
|
||||
.then((metrics: any[]) => {
|
||||
this.metrics = metrics.map((m) => new Widget().fromJson(m));
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
fetch(id: string, period?: any) {
|
||||
this.isLoading = true;
|
||||
return metricService
|
||||
.getMetric(id)
|
||||
.then((metric: any) => {
|
||||
return (this.instance = new Widget().fromJson(metric, period));
|
||||
})
|
||||
.finally(() => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
delete(metric: Widget) {
|
||||
this.isSaving = true;
|
||||
// @ts-ignore
|
||||
return metricService
|
||||
.deleteMetric(metric[Widget.ID_KEY])
|
||||
.then(() => {
|
||||
// @ts-ignore
|
||||
this.removeById(metric[Widget.ID_KEY]);
|
||||
toast.success('Metric deleted successfully');
|
||||
})
|
||||
.finally(() => {
|
||||
this.instance.updateKey('hasChanged', false);
|
||||
this.isSaving = false;
|
||||
});
|
||||
}
|
||||
|
||||
fetchError(errorId: any): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
errorService
|
||||
.one(errorId)
|
||||
.then((error: any) => {
|
||||
resolve(new Error().fromJSON(error));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error('Failed to fetch error details.');
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue