fix(ui): add new metric selection modals, fix dashboard view
This commit is contained in:
parent
58a42c72d5
commit
7806c15806
14 changed files with 503 additions and 166 deletions
|
|
@ -9,12 +9,20 @@ import cn from 'classnames';
|
|||
import { withSiteId } from 'App/routes';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<{}>) {
|
||||
const { history, match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
interface RouterProps {
|
||||
siteId: string;
|
||||
dashboardId: string;
|
||||
metricId: string;
|
||||
}
|
||||
|
||||
function NewDashboard(props: RouteComponentProps<RouterProps>) {
|
||||
const { history, match: { params: { siteId, dashboardId } } } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const loading = useObserver(() => dashboardStore.isLoading);
|
||||
const isMetricDetails = history.location.pathname.includes('/metrics/') || history.location.pathname.includes('/metric/');
|
||||
const isDashboardDetails = history.location.pathname.includes('/dashboard/')
|
||||
|
||||
const shouldHideMenu = isMetricDetails || isDashboardDetails;
|
||||
useEffect(() => {
|
||||
dashboardStore.fetchList().then((resp) => {
|
||||
if (parseInt(dashboardId) > 0) {
|
||||
|
|
@ -33,16 +41,16 @@ function NewDashboard(props: RouteComponentProps<{}>) {
|
|||
return useObserver(() => (
|
||||
<Loader loading={loading}>
|
||||
<div className="page-margin container-90">
|
||||
<div className={cn("side-menu", { 'hidden' : isMetricDetails })}>
|
||||
<div className={cn("side-menu", { 'hidden' : shouldHideMenu })}>
|
||||
<DashboardSideMenu siteId={siteId} />
|
||||
</div>
|
||||
<div
|
||||
className={cn({
|
||||
"side-menu-margined" : !isMetricDetails,
|
||||
"container-70" : isMetricDetails
|
||||
"side-menu-margined" : !shouldHideMenu,
|
||||
"container-70" : shouldHideMenu
|
||||
})}
|
||||
>
|
||||
<DashboardRouter siteId={siteId} />
|
||||
<DashboardRouter />
|
||||
</div>
|
||||
</div>
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,32 @@
|
|||
import React from 'react';
|
||||
import { Button, PageTitle, Icon, Link } from 'UI';
|
||||
import { Button, PageTitle, Icon } from 'UI';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { withSiteId } from 'App/routes';
|
||||
|
||||
import DashboardList from './DashboardList';
|
||||
import DashboardSearch from './DashboardSearch';
|
||||
|
||||
function DashboardsView() {
|
||||
function DashboardsView({ history, siteId }: { history: any, siteId: string }) {
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
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 p-4">
|
||||
<div className="flex items-center mb-4 justify-between px-4">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Dashboards" className="" />
|
||||
</div>
|
||||
<Link to={'/metrics/create'}><Button variant="primary">Create Dashboard</Button></Link>
|
||||
<Button variant="primary" onClick={onAddDashboardClick}>Create Dashboard</Button>
|
||||
<div className="ml-auto w-1/4">
|
||||
<DashboardSearch />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,9 +6,16 @@ import cn from 'classnames';
|
|||
import { useStore } from 'App/mstore';
|
||||
import { Loader } from 'UI';
|
||||
|
||||
function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds }) {
|
||||
interface IWiProps {
|
||||
category: Record<string, any>
|
||||
onClick: (category: Record<string, any>) => void
|
||||
isSelected: boolean
|
||||
selectedWidgetIds: string[]
|
||||
}
|
||||
|
||||
export function WidgetCategoryItem({ category, isSelected, onClick, selectedWidgetIds }: IWiProps) {
|
||||
const selectedCategoryWidgetsCount = useObserver(() => {
|
||||
return category.widgets.filter(widget => selectedWidgetIds.includes(widget.metricId)).length;
|
||||
return category.widgets.filter((widget: any) => selectedWidgetIds.includes(widget.metricId)).length;
|
||||
});
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -3,23 +3,22 @@ import { useObserver } from 'mobx-react-lite';
|
|||
import DashboardMetricSelection from '../DashboardMetricSelection';
|
||||
import DashboardForm from '../DashboardForm';
|
||||
import { Button } from 'UI';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId, dashboardSelected } from 'App/routes';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
|
||||
interface Props {
|
||||
interface Props extends RouteComponentProps {
|
||||
history: any
|
||||
siteId?: string
|
||||
dashboardId?: string
|
||||
onMetricAdd?: () => void;
|
||||
}
|
||||
function DashboardModal(props) {
|
||||
function DashboardModal(props: Props) {
|
||||
const { history, siteId, dashboardId } = props;
|
||||
const { dashboardStore } = useStore();
|
||||
const selectedWidgetsCount = useObserver(() => dashboardStore.selectedWidgets.length);
|
||||
const { hideModal } = useModal();
|
||||
const loadingTemplates = useObserver(() => dashboardStore.loadingTemplates);
|
||||
const dashboard = useObserver(() => dashboardStore.dashboardInstance);
|
||||
const loading = useObserver(() => dashboardStore.isSaving);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
metrics,
|
||||
|
|
@ -18,18 +18,18 @@ import WidgetView from '../WidgetView';
|
|||
import WidgetSubDetailsView from '../WidgetSubDetailsView';
|
||||
import DashboardsView from '../DashboardList';
|
||||
|
||||
function DashboardViewSelected({ siteId, dashboardId }) {
|
||||
function DashboardViewSelected({ siteId, dashboardId }: { siteId: string, dashboardId: string }) {
|
||||
return (
|
||||
<DashboardView siteId={siteId} dashboardId={dashboardId} />
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
history: any
|
||||
interface Props extends RouteComponentProps {
|
||||
match: any
|
||||
}
|
||||
function DashboardRouter(props: Props) {
|
||||
const { match: { params: { siteId, dashboardId, metricId } } } = props;
|
||||
const { match: { params: { siteId, dashboardId } }, history } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
|
|
@ -46,7 +46,7 @@ function DashboardRouter(props: Props) {
|
|||
</Route>
|
||||
|
||||
<Route exact path={withSiteId(dashboard(), siteId)}>
|
||||
<DashboardsView />
|
||||
<DashboardsView siteId={siteId} history={history} />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(dashboardMetricDetails(dashboardId), siteId)}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useStore } from "App/mstore";
|
||||
import { Button, PageTitle, Loader, NoContent } from "UI";
|
||||
import { Button, PageTitle, Loader } from "UI";
|
||||
import { withSiteId } from "App/routes";
|
||||
import withModal from "App/components/Modal/withModal";
|
||||
import DashboardWidgetGrid from "../DashboardWidgetGrid";
|
||||
|
|
@ -15,8 +15,6 @@ import withPageTitle from "HOCs/withPageTitle";
|
|||
import withReport from "App/components/hocs/withReport";
|
||||
import DashboardOptions from "../DashboardOptions";
|
||||
import SelectDateRange from "Shared/SelectDateRange";
|
||||
// @ts-ignore
|
||||
import DashboardIcon from "../../../../svg/dashboard-icn.svg";
|
||||
import { Tooltip } from "react-tippy";
|
||||
import Breadcrumb from 'Shared/Breadcrumb';
|
||||
|
||||
|
|
@ -31,23 +29,18 @@ type Props = IProps & RouteComponentProps;
|
|||
function DashboardView(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 { showModal } = useModal();
|
||||
|
||||
const showAlertModal = dashboardStore.showAlertModal;
|
||||
const loading = dashboardStore.fetchingDashboard;
|
||||
const dashboards = dashboardStore.dashboards;
|
||||
const dashboard: any = dashboardStore.selectedDashboard;
|
||||
const period = dashboardStore.period;
|
||||
|
||||
const queryParams = new URLSearchParams(props.location.search);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboard || !dashboard.dashboardId) return;
|
||||
dashboardStore.fetch(dashboard.dashboardId);
|
||||
}, [dashboard]);
|
||||
|
||||
const trimQuery = () => {
|
||||
if (!queryParams.has("modal")) return;
|
||||
queryParams.delete("modal");
|
||||
|
|
@ -60,21 +53,24 @@ function DashboardView(props: Props) {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboardId || (!dashboard && dashboardStore.dashboards.length > 0)) dashboardStore.selectDefaultDashboard();
|
||||
|
||||
if (queryParams.has("modal")) {
|
||||
onAddWidgets();
|
||||
trimQuery();
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dashboardStore.selectDefaultDashboard();
|
||||
}, [siteId])
|
||||
|
||||
useEffect(() => {
|
||||
dashboardStore.selectDashboardById(dashboardId);
|
||||
const isExists = dashboardStore.getDashboardById(dashboardId);
|
||||
if (!isExists) {
|
||||
props.history.push(withSiteId(`/dashboard`, siteId))
|
||||
}
|
||||
}, [dashboardId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!dashboard || !dashboard.dashboardId) return;
|
||||
dashboardStore.fetch(dashboard.dashboardId);
|
||||
}, [dashboard]);
|
||||
|
||||
const onAddWidgets = () => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
showModal(
|
||||
|
|
@ -87,11 +83,6 @@ function DashboardView(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
const onAddDashboardClick = () => {
|
||||
dashboardStore.initDashboard();
|
||||
showModal(<DashboardModal siteId={siteId} />, { right: true })
|
||||
}
|
||||
|
||||
const onEdit = (isTitle: boolean) => {
|
||||
dashboardStore.initDashboard(dashboard);
|
||||
setFocusedInput(isTitle);
|
||||
|
|
@ -107,131 +98,104 @@ function DashboardView(props: Props) {
|
|||
})
|
||||
) {
|
||||
dashboardStore.deleteDashboard(dashboard).then(() => {
|
||||
dashboardStore.selectDefaultDashboard().then(
|
||||
({ dashboardId }) => {
|
||||
props.history.push(
|
||||
withSiteId(`/dashboard/${dashboardId}`, siteId)
|
||||
);
|
||||
},
|
||||
() => {
|
||||
props.history.push(withSiteId("/dashboard", siteId));
|
||||
}
|
||||
);
|
||||
props.history.push(withSiteId(`/dashboard`, siteId));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (!dashboard) return null;
|
||||
|
||||
return (
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={
|
||||
dashboards.length === 0 ||
|
||||
!dashboard ||
|
||||
!dashboard.dashboardId
|
||||
}
|
||||
title={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<object
|
||||
style={{ width: "180px" }}
|
||||
type="image/svg+xml"
|
||||
data={DashboardIcon}
|
||||
className="no-result-icon"
|
||||
<div style={{ maxWidth: "1300px", margin: "auto" }}>
|
||||
<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-4 justify-between">
|
||||
<div className="flex items-center" style={{ flex: 3 }}>
|
||||
<PageTitle
|
||||
// @ts-ignore
|
||||
title={
|
||||
<Tooltip
|
||||
delay={100}
|
||||
arrow
|
||||
title="Double click to rename"
|
||||
>
|
||||
{dashboard?.name}
|
||||
</Tooltip>
|
||||
}
|
||||
onDoubleClick={() => onEdit(true)}
|
||||
className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer"
|
||||
actionButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddWidgets}
|
||||
>
|
||||
Add Metric
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<span>
|
||||
Gather and analyze <br /> important metrics in one
|
||||
place.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
<div style={{ maxWidth: "1300px", margin: "auto" }}>
|
||||
<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-4 justify-between">
|
||||
<div className="flex items-center" style={{ flex: 3 }}>
|
||||
<PageTitle
|
||||
// @ts-ignore
|
||||
title={
|
||||
<Tooltip
|
||||
delay={100}
|
||||
arrow
|
||||
title="Double click to rename"
|
||||
>
|
||||
{dashboard?.name}
|
||||
</Tooltip>
|
||||
}
|
||||
onDoubleClick={() => onEdit(true)}
|
||||
className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer"
|
||||
actionButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onAddWidgets}
|
||||
>
|
||||
Add Metric
|
||||
</Button>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ flex: 1, justifyContent: "end" }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 justify-end"
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<SelectDateRange
|
||||
style={{ width: "300px" }}
|
||||
period={period}
|
||||
onChange={(period: any) =>
|
||||
dashboardStore.setPeriod(period)
|
||||
}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center"
|
||||
style={{ flex: 1, justifyContent: "end" }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center flex-shrink-0 justify-end"
|
||||
style={{ width: "300px" }}
|
||||
>
|
||||
<SelectDateRange
|
||||
style={{ width: "300px" }}
|
||||
period={period}
|
||||
onChange={(period: any) =>
|
||||
dashboardStore.setPeriod(period)
|
||||
}
|
||||
right={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<DashboardOptions
|
||||
editHandler={onEdit}
|
||||
deleteHandler={onDelete}
|
||||
renderReport={props.renderReport}
|
||||
isTitlePresent={!!dashboard?.description}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx-4" />
|
||||
<div className="flex items-center flex-shrink-0">
|
||||
<DashboardOptions
|
||||
editHandler={onEdit}
|
||||
deleteHandler={onDelete}
|
||||
renderReport={props.renderReport}
|
||||
isTitlePresent={!!dashboard?.description}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="my-4 font-normal color-gray-dark">
|
||||
{dashboard?.description}
|
||||
</h2>
|
||||
</div>
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
dashboardId={dashboardId}
|
||||
onEditHandler={onAddWidgets}
|
||||
id="report"
|
||||
/>
|
||||
<AlertFormModal
|
||||
showModal={showAlertModal}
|
||||
onClose={() =>
|
||||
dashboardStore.updateKey("showAlertModal", false)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
<div>
|
||||
<h2
|
||||
className="my-4 w-fit font-normal color-gray-dark border-dashed border-b cursor-pointer border-b-gray-medium"
|
||||
onDoubleClick={() => onEdit(false)}
|
||||
>
|
||||
{dashboard?.description || "Describe the purpose of this dashboard"}
|
||||
</h2>
|
||||
</div>
|
||||
<DashboardWidgetGrid
|
||||
siteId={siteId}
|
||||
dashboardId={dashboardId}
|
||||
onEditHandler={onAddWidgets}
|
||||
id="report"
|
||||
/>
|
||||
<AlertFormModal
|
||||
showModal={showAlertModal}
|
||||
onClose={() =>
|
||||
dashboardStore.updateKey("showAlertModal", false)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button } from 'UI';
|
||||
import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
metrics: any[];
|
||||
siteId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function AddMetric({ metrics, history, siteId, title, description }: IProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const selectedWidgetIds = dashboardStore.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
||||
const onSave = () => {
|
||||
if (selectedWidgetIds.length === 0) return;
|
||||
dashboardStore
|
||||
.save(dashboard)
|
||||
.then(async (syncedDashboard) => {
|
||||
if (dashboard.exists()) {
|
||||
await dashboardStore.fetch(dashboard.dashboardId);
|
||||
}
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
})
|
||||
.then(hideModal);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
const path = withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId);
|
||||
if (!queryParams.has('modal')) history.push('?modal=addMetric');
|
||||
history.push(path);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '85vw', width: 1200 }}>
|
||||
<div className="border-l shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%' }}>
|
||||
<div className="mb-6 pt-8 px-8 flex items-start justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl">{title}</h1>
|
||||
<div className="text-disabled-text">{description}</div>
|
||||
</div>
|
||||
{title.includes('Custom') ? (
|
||||
<div>
|
||||
<span className="text-md link" onClick={onCreateNew}>
|
||||
+ Create new
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Don’t find the one you need?
|
||||
<span className="text-md link ml-2" onClick={onCreateNew}>
|
||||
+ Create custom metric
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid h-full grid-cols-4 gap-4 px-8 items-start py-1" style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto' }}>
|
||||
{metrics.map((metric: any) => (
|
||||
<WidgetWrapper
|
||||
key={metric.metricId}
|
||||
widget={metric}
|
||||
active={selectedWidgetIds.includes(metric.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={metric.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(metric)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="py-4 border-t px-8 bg-white w-full flex items-center justify-between">
|
||||
<div>
|
||||
{'Selected '}
|
||||
<span className="font-semibold">{selectedWidgetIds.length}</span>
|
||||
{' out of '}
|
||||
<span className="font-semibold">{metrics.length}</span>
|
||||
</div>
|
||||
<Button variant="primary" disabled={selectedWidgetIds.length === 0} onClick={onSave}>
|
||||
Add Selected
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(AddMetric));
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Icon } from 'UI';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { useStore } from 'App/mstore';
|
||||
import AddMetric from './AddMetric';
|
||||
import AddPredefinedMetric from './AddPredefinedMetric';
|
||||
|
||||
interface AddMetricButtonProps {
|
||||
iconName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function AddMetricButton({ iconName, title, description, onClick }: AddMetricButtonProps) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="px-4 py-8 flex items-center flex-col bg-tealx-lightest hover:bg-active-blue group border-teal-light border cursor-pointer"
|
||||
>
|
||||
<div className="p-4 mb-2 bg-gray-light rounded-full group-hover:bg-teal-light">
|
||||
<Icon name={iconName} size={26} />
|
||||
</div>
|
||||
<div className="font-bold mb-2">{title}</div>
|
||||
<div className="text-disabled-test w-2/3">{description}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddMetricContainer({ siteId }: any) {
|
||||
const { showModal } = useModal();
|
||||
const [categories, setCategories] = React.useState<Record<string, any>[]>([]);
|
||||
const { dashboardStore } = useStore();
|
||||
|
||||
React.useEffect(() => {
|
||||
dashboardStore?.fetchTemplates(true).then((cats) => setCategories(cats));
|
||||
}, []);
|
||||
|
||||
const onAddCustomMetrics = () => {
|
||||
dashboardStore.initDashboard(dashboardStore.selectedDashboard);
|
||||
showModal(
|
||||
<AddMetric
|
||||
siteId={siteId}
|
||||
title="Custom Metrics"
|
||||
description="Metrics that are manually created by you or your team."
|
||||
metrics={categories.find((category) => category.name === 'custom')?.widgets}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
|
||||
const onAddPredefinedMetrics = () => {
|
||||
dashboardStore.initDashboard(dashboardStore.selectedDashboard);
|
||||
showModal(
|
||||
<AddPredefinedMetric
|
||||
siteId={siteId}
|
||||
title="Ready-Made Metrics"
|
||||
description="Curated metrics predfined by OpenReplay."
|
||||
categories={categories.filter((category) => category.name !== 'custom')}
|
||||
/>,
|
||||
{ right: true }
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className="bg-white rounded p-8 grid grid-cols-2 gap-4 w-4/5 m-auto">
|
||||
<AddMetricButton
|
||||
title="+ Add custom Metric"
|
||||
description="Metrics that are manually created by you or your team"
|
||||
iconName="bar-chart-line"
|
||||
onClick={onAddCustomMetrics}
|
||||
/>
|
||||
<AddMetricButton
|
||||
title="+ Add Ready-Made Metric"
|
||||
description="Curated metrics predfined by OpenReplay."
|
||||
iconName="bar-chart-line"
|
||||
onClick={onAddPredefinedMetrics}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AddMetricContainer);
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import React from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Button } from 'UI';
|
||||
import WidgetWrapper from 'App/components/Dashboard/components/WidgetWrapper';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useModal } from 'App/components/Modal';
|
||||
import { dashboardMetricCreate, withSiteId } from 'App/routes';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { WidgetCategoryItem } from 'App/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection';
|
||||
|
||||
interface IProps extends RouteComponentProps {
|
||||
categories: Record<string, any>[];
|
||||
siteId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function AddPredefinedMetric({ categories, history, siteId, title, description }: IProps) {
|
||||
const { dashboardStore } = useStore();
|
||||
const { hideModal } = useModal();
|
||||
const [allCheck, setAllCheck] = React.useState(false);
|
||||
const [activeCategory, setActiveCategory] = React.useState<Record<string, any>>();
|
||||
|
||||
const scrollContainer = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const dashboard = dashboardStore.selectedDashboard;
|
||||
const selectedWidgetIds = dashboardStore.selectedWidgets.map((widget: any) => widget.metricId);
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const totalMetricCount = categories.reduce((acc, category) => acc + category.widgets.length, 0);
|
||||
|
||||
React.useEffect(() => {
|
||||
dashboardStore?.fetchTemplates(true).then((categories) => {
|
||||
const defaultCategory = categories.filter((category: any) => category.name !== 'custom')[0];
|
||||
setActiveCategory(defaultCategory);
|
||||
});
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (scrollContainer.current) {
|
||||
scrollContainer.current.scrollTop = 0;
|
||||
}
|
||||
}, [activeCategory, scrollContainer.current]);
|
||||
|
||||
const handleWidgetCategoryClick = (category: any) => {
|
||||
setActiveCategory(category);
|
||||
setAllCheck(false);
|
||||
};
|
||||
|
||||
const onSave = () => {
|
||||
if (selectedWidgetIds.length === 0) return;
|
||||
dashboardStore
|
||||
.save(dashboard)
|
||||
.then(async (syncedDashboard) => {
|
||||
if (dashboard.exists()) {
|
||||
await dashboardStore.fetch(dashboard.dashboardId);
|
||||
}
|
||||
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
|
||||
})
|
||||
.then(hideModal);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
const path = withSiteId(dashboardMetricCreate(dashboard.dashboardId), siteId);
|
||||
if (!queryParams.has('modal')) history.push('?modal=addMetric');
|
||||
history.push(path);
|
||||
hideModal();
|
||||
};
|
||||
|
||||
const toggleAllMetrics = ({ target: { checked } }: any) => {
|
||||
setAllCheck(checked);
|
||||
if (checked) {
|
||||
dashboardStore.selectWidgetsByCategory(activeCategory.name);
|
||||
} else {
|
||||
dashboardStore.removeSelectedWidgetByCategory(activeCategory);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '85vw', width: 1200 }}>
|
||||
<div className="border-l shadow h-screen" style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '100%' }}>
|
||||
<div className="mb-6 pt-8 px-8 flex items-start justify-between">
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-2xl">{title}</h1>
|
||||
<div className="text-disabled-text">{description}</div>
|
||||
</div>
|
||||
{title.includes('Custom') ? (
|
||||
<div>
|
||||
<span className="text-md link" onClick={onCreateNew}>
|
||||
+ Create new
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
Don’t find the one you need?
|
||||
<span className="text-md link ml-2" onClick={onCreateNew}>
|
||||
+ Create custom metric
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex px-8">
|
||||
<div style={{ flex: 3 }}>
|
||||
<div className="grid grid-cols-1 gap-4 py-1 pr-2" style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto' }}>
|
||||
{activeCategory &&
|
||||
categories.map((category) => (
|
||||
<WidgetCategoryItem
|
||||
key={category.name}
|
||||
onClick={handleWidgetCategoryClick}
|
||||
category={category}
|
||||
isSelected={activeCategory.name === category.name}
|
||||
selectedWidgetIds={selectedWidgetIds}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="grid h-full grid-cols-4 gap-4 items-start py-1"
|
||||
style={{ maxHeight: 'calc(100vh - 160px)', overflowY: 'auto', flex: 9 }}
|
||||
>
|
||||
{activeCategory &&
|
||||
activeCategory.widgets.map((metric: any) => (
|
||||
<WidgetWrapper
|
||||
key={metric.metricId}
|
||||
widget={metric}
|
||||
active={selectedWidgetIds.includes(metric.metricId)}
|
||||
isTemplate={true}
|
||||
isWidget={metric.metricType === 'predefined'}
|
||||
onClick={() => dashboardStore.toggleWidgetSelection(metric)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="py-4 border-t px-8 bg-white w-full flex items-center justify-between">
|
||||
<div>
|
||||
{'Selected '}
|
||||
<span className="font-semibold">{selectedWidgetIds.length}</span>
|
||||
{' out of '}
|
||||
<span className="font-semibold">{totalMetricCount}</span>
|
||||
</div>
|
||||
<Button variant="primary" disabled={selectedWidgetIds.length === 0} onClick={onSave}>
|
||||
Add Selected
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(observer(AddPredefinedMetric));
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { useStore } from 'App/mstore';
|
||||
import WidgetWrapper from '../WidgetWrapper';
|
||||
import { NoContent, Button, Loader } from 'UI';
|
||||
import { NoContent, Loader } from 'UI';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
import AddMetricContainer from './AddMetricContainer'
|
||||
|
||||
interface Props {
|
||||
siteId: string,
|
||||
|
|
@ -18,16 +19,14 @@ function DashboardWidgetGrid(props: Props) {
|
|||
const list: any = useObserver(() => dashboard?.widgets);
|
||||
|
||||
return useObserver(() => (
|
||||
// @ts-ignore
|
||||
<Loader loading={loading}>
|
||||
<NoContent
|
||||
show={list.length === 0}
|
||||
icon="no-metrics-chart"
|
||||
title="No metrics added to this dashboard"
|
||||
title="Build your dashboard"
|
||||
subtext={
|
||||
<div className="flex items-center justify-center flex-col">
|
||||
<p>Metrics helps you visualize trends from sessions captured by OpenReplay</p>
|
||||
<Button variant="primary" onClick={props.onEditHandler}>Add Metric</Button>
|
||||
</div>
|
||||
<AddMetricContainer siteId={siteId} />
|
||||
}
|
||||
>
|
||||
<div className="grid gap-4 grid-cols-4 items-start pb-10" id={props.id}>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,7 @@ interface Props{
|
|||
siteId: number;
|
||||
}
|
||||
function MetricsView(props: Props) {
|
||||
const { siteId } = props;
|
||||
const { metricStore } = useStore();
|
||||
const metricsCount = useObserver(() => metricStore.metrics.length);
|
||||
|
||||
React.useEffect(() => {
|
||||
metricStore.fetchList();
|
||||
|
|
@ -22,7 +20,6 @@ function MetricsView(props: Props) {
|
|||
<div className="flex items-center mb-4 justify-between px-4">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Metrics" className="" />
|
||||
<span className="text-2xl color-gray-medium ml-2">{metricsCount}</span>
|
||||
</div>
|
||||
<Link to={'/metrics/create'}><Button variant="primary">Create Metric</Button></Link>
|
||||
<div className="ml-auto w-1/4">
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ export default class SiteDropdown extends React.PureComponent {
|
|||
this.props.clearSearchLive();
|
||||
|
||||
mstore.initClient();
|
||||
};
|
||||
mstore.dashboardStore.selectDefaultDashboard();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ interface Props {
|
|||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
variant?: 'default' | 'primary' | 'text' | 'text-primary' | 'text-red' | 'outline'
|
||||
loading?: boolean;
|
||||
icon?: string;
|
||||
rounded?: boolean;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export interface IDashboardSotre {
|
|||
|
||||
selectWidgetsByCategory: (category: string) => void;
|
||||
toggleAllSelectedWidgets: (isSelected: boolean) => void;
|
||||
removeSelectedWidgetByCategory(category: string): void;
|
||||
removeSelectedWidgetByCategory(category: Record<string, any>): void;
|
||||
toggleWidgetSelection(widget: IWidget): void;
|
||||
|
||||
initDashboard(dashboard?: IDashboard): void;
|
||||
|
|
@ -72,6 +72,7 @@ export interface IDashboardSotre {
|
|||
getDashboardCount(): void;
|
||||
updateDashboard(dashboard: IDashboard): void;
|
||||
selectDashboardById(dashboardId: string): void;
|
||||
getDashboardById(dashboardId: string): boolean;
|
||||
setSiteId(siteId: any): void;
|
||||
selectDefaultDashboard(): Promise<IDashboard>;
|
||||
|
||||
|
|
@ -372,6 +373,18 @@ export default class DashboardStore implements IDashboardSotre {
|
|||
new Dashboard();
|
||||
};
|
||||
|
||||
getDashboardById = (dashboardId: string) => {
|
||||
const dashboard = this.dashboards.find((d) => d.dashboardId == dashboardId)
|
||||
|
||||
if (dashboard) {
|
||||
this.selectedDashboard = dashboard
|
||||
return true;
|
||||
} else {
|
||||
this.selectedDashboard = null
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
setSiteId = (siteId: any) => {
|
||||
this.siteId = siteId;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue