diff --git a/frontend/app/components/Dashboard/NewDashboard.tsx b/frontend/app/components/Dashboard/NewDashboard.tsx index 89af30897..af0e1ed88 100644 --- a/frontend/app/components/Dashboard/NewDashboard.tsx +++ b/frontend/app/components/Dashboard/NewDashboard.tsx @@ -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) { + 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(() => (
-
+
- +
diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx index 5ebe9e150..2564cea2b 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx @@ -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 (
- +
diff --git a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx index d612efe0b..cca7c51c1 100644 --- a/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx +++ b/frontend/app/components/Dashboard/components/DashboardMetricSelection/DashboardMetricSelection.tsx @@ -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 + onClick: (category: Record) => 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 (
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); diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx index a7e71ffbd..f18415f27 100644 --- a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx +++ b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx @@ -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 ( ) } -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 (
@@ -46,7 +46,7 @@ function DashboardRouter(props: Props) { - + diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index e3aa0c2f2..b81cef7b9 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -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(, { 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 ( - - + setShowEditModal(false)} + focusTitle={focusTitle} + /> + +
+
+ + {dashboard?.name} + + } + onDoubleClick={() => onEdit(true)} + className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer" + actionButton={ + + } /> - - Gather and analyze
important metrics in one - place. -
- } - size="small" - > -
- setShowEditModal(false)} - focusTitle={focusTitle} - /> - -
-
- - {dashboard?.name} - - } - onDoubleClick={() => onEdit(true)} - className="mr-3 select-none hover:border-dotted hover:border-b border-gray-medium cursor-pointer" - actionButton={ - +
+
+ + dashboardStore.setPeriod(period) } + right={true} />
-
-
- - dashboardStore.setPeriod(period) - } - right={true} - /> -
-
-
- -
+
+
+
-
-

- {dashboard?.description} -

-
- - - dashboardStore.updateKey("showAlertModal", false) - } - />
- +
+

onEdit(false)} + > + {dashboard?.description || "Describe the purpose of this dashboard"} +

+
+ + + dashboardStore.updateKey("showAlertModal", false) + } + /> +
); } diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx new file mode 100644 index 000000000..ccdc033fe --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetric.tsx @@ -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 ( +
+
+
+
+

{title}

+
{description}
+
+ {title.includes('Custom') ? ( +
+ + + Create new + +
+ ) : ( +
+ Don’t find the one you need? + + + Create custom metric + +
+ )} +
+ +
+ {metrics.map((metric: any) => ( + dashboardStore.toggleWidgetSelection(metric)} + /> + ))} +
+ +
+
+ {'Selected '} + {selectedWidgetIds.length} + {' out of '} + {metrics.length} +
+ +
+
+
+ ); +} + +export default withRouter(observer(AddMetric)); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx new file mode 100644 index 000000000..c796ecc5f --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddMetricContainer.tsx @@ -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 ( +
+
+ +
+
{title}
+
{description}
+
+ ); +} + +function AddMetricContainer({ siteId }: any) { + const { showModal } = useModal(); + const [categories, setCategories] = React.useState[]>([]); + const { dashboardStore } = useStore(); + + React.useEffect(() => { + dashboardStore?.fetchTemplates(true).then((cats) => setCategories(cats)); + }, []); + + const onAddCustomMetrics = () => { + dashboardStore.initDashboard(dashboardStore.selectedDashboard); + showModal( + category.name === 'custom')?.widgets} + />, + { right: true } + ); + }; + + const onAddPredefinedMetrics = () => { + dashboardStore.initDashboard(dashboardStore.selectedDashboard); + showModal( + category.name !== 'custom')} + />, + { right: true } + ); + }; + return ( +
+ + +
+ ); +} + +export default observer(AddMetricContainer); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx new file mode 100644 index 000000000..13cd1047b --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/AddPredefinedMetric.tsx @@ -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[]; + 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>(); + + const scrollContainer = React.useRef(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 ( +
+
+
+
+

{title}

+
{description}
+
+ {title.includes('Custom') ? ( +
+ + + Create new + +
+ ) : ( +
+ Don’t find the one you need? + + + Create custom metric + +
+ )} +
+ +
+
+
+ {activeCategory && + categories.map((category) => ( + + ))} +
+
+ +
+ {activeCategory && + activeCategory.widgets.map((metric: any) => ( + dashboardStore.toggleWidgetSelection(metric)} + /> + ))} +
+
+ +
+
+ {'Selected '} + {selectedWidgetIds.length} + {' out of '} + {totalMetricCount} +
+ +
+
+
+ ); +} + +export default withRouter(observer(AddPredefinedMetric)); diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index 442ee46e6..7750c8241 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -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 -

Metrics helps you visualize trends from sessions captured by OpenReplay

- -
+ } >
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 216d1369b..288b9e1f0 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -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) {
- {metricsCount}
diff --git a/frontend/app/components/Header/SiteDropdown.js b/frontend/app/components/Header/SiteDropdown.js index 87fe2a0c2..7f89eee82 100644 --- a/frontend/app/components/Header/SiteDropdown.js +++ b/frontend/app/components/Header/SiteDropdown.js @@ -54,7 +54,8 @@ export default class SiteDropdown extends React.PureComponent { this.props.clearSearchLive(); mstore.initClient(); - }; + mstore.dashboardStore.selectDefaultDashboard(); + } render() { const { diff --git a/frontend/app/components/ui/Button/Button.tsx b/frontend/app/components/ui/Button/Button.tsx index d3e3acd15..eb9396682 100644 --- a/frontend/app/components/ui/Button/Button.tsx +++ b/frontend/app/components/ui/Button/Button.tsx @@ -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; diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 232d4da4f..8abef7ea8 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -53,7 +53,7 @@ export interface IDashboardSotre { selectWidgetsByCategory: (category: string) => void; toggleAllSelectedWidgets: (isSelected: boolean) => void; - removeSelectedWidgetByCategory(category: string): void; + removeSelectedWidgetByCategory(category: Record): 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; @@ -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; };