diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 64357683b..c5fe4b36c 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -17,8 +17,9 @@ const AssistPure = lazy(() => import('Components/Assist')); const BugFinderPure = lazy(() => import('Components/BugFinder/BugFinder')); const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard')); const ErrorsPure = lazy(() => import('Components/Errors/Errors')); -const FunnelDetails = lazy(() => import('Components/Funnels/FunnelDetails')); +const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails')); const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDetails')); +const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage')); import WidgetViewPure from 'Components/Dashboard/components/WidgetView'; import Header from 'Components/Header/Header'; import { fetchList as fetchMetadata } from 'Duck/customField'; @@ -37,20 +38,21 @@ import { ModalProvider } from './components/Modal'; const BugFinder = withSiteIdUpdater(BugFinderPure); const Dashboard = withSiteIdUpdater(DashboardPure); -const WidgetView = withSiteIdUpdater(WidgetViewPure); const Session = withSiteIdUpdater(SessionPure); const LiveSession = withSiteIdUpdater(LiveSessionPure); const Assist = withSiteIdUpdater(AssistPure); const Client = withSiteIdUpdater(ClientPure); const Onboarding = withSiteIdUpdater(OnboardingPure); const Errors = withSiteIdUpdater(ErrorsPure); -const Funnels = withSiteIdUpdater(FunnelDetails); +const FunnelPage = withSiteIdUpdater(FunnelPagePure); +const FunnelsDetails = withSiteIdUpdater(FunnelDetailsPure); const FunnelIssue = withSiteIdUpdater(FunnelIssueDetails); const withSiteId = routes.withSiteId; // const withObTab = routes.withObTab; const METRICS_PATH = routes.metrics(); const METRICS_DETAILS = routes.metricDetails(); +const METRICS_DETAILS_SUB = routes.metricDetailsSub(); const DASHBOARD_PATH = routes.dashboard(); const DASHBOARD_SELECT_PATH = routes.dashboardSelected(); @@ -62,7 +64,8 @@ const SESSIONS_PATH = routes.sessions(); const ASSIST_PATH = routes.assist(); const ERRORS_PATH = routes.errors(); const ERROR_PATH = routes.error(); -const FUNNEL_PATH = routes.funnel(); +const FUNNEL_PATH = routes.funnels(); +const FUNNEL_CREATE_PATH = routes.funnelsCreate(); const FUNNEL_ISSUE_PATH = routes.funnelIssue(); const SESSION_PATH = routes.session(); const LIVE_SESSION_PATH = routes.liveSession(); @@ -199,6 +202,7 @@ class Router extends React.Component { {/* DASHBOARD and Metrics */} + @@ -207,7 +211,9 @@ class Router extends React.Component { - + + + {/* */} diff --git a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js index 167b448f5..7707424b0 100644 --- a/frontend/app/components/BugFinder/SessionList/SessionListHeader.js +++ b/frontend/app/components/BugFinder/SessionList/SessionListHeader.js @@ -60,4 +60,4 @@ export default connect(state => ({ activeTab: state.getIn([ 'search', 'activeTab' ]), period: state.getIn([ 'search', 'period' ]), filter: state.getIn([ 'search', 'instance' ]), -}), { applyFilter })(SessionListHeader); +}), { applyFilter })(SessionListHeader); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx index 6d1cd01e4..76b8697c1 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricPieChart/CustomMetricPieChart.tsx @@ -9,8 +9,6 @@ import { numberWithCommas } from 'App/utils'; interface Props { metric: any, data: any; - params: any; - // seriesMap: any; colors: any; onClick?: (filters) => void; } diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx new file mode 100644 index 000000000..4bd4cb983 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/CustomMetricTableSessions.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +interface Props { + data: any + metric?: any + isTemplate?: boolean; +} +function CustomMetricTableSessions(props: Props) { + return ( +
+ +
+ ); +} + +export default CustomMetricTableSessions; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts new file mode 100644 index 000000000..46889345c --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions/index.ts @@ -0,0 +1 @@ +export { default } from './CustomMetricTableSessions'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx index 5f90e01ac..f248fbc89 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricWidgetPreview/CustomMetricWidgetPreview.tsx @@ -1,28 +1,24 @@ import React, { useEffect, useState, useRef } from 'react'; import { connect } from 'react-redux'; -import { Loader, NoContent, SegmentSelection, Icon } from 'UI'; +import { Loader, NoContent, SegmentSelection } from 'UI'; import { Styles } from '../../common'; -// import { ResponsiveContainer, XAxis, YAxis, CartesianGrid, Tooltip, LineChart, Line, Legend } from 'recharts'; -import Period, { LAST_24_HOURS, LAST_30_MINUTES, YESTERDAY, LAST_7_DAYS } from 'Types/app/period'; +import Period from 'Types/app/period'; import stl from './CustomMetricWidgetPreview.module.css'; -import { getChartFormatter } from 'Types/dashboard/helper'; import { remove } from 'Duck/customMetrics'; import DateRange from 'Shared/DateRange'; import { edit } from 'Duck/customMetrics'; import CustomMetriLineChart from '../CustomMetriLineChart'; import CustomMetricPercentage from '../CustomMetricPercentage'; import CustomMetricTable from '../CustomMetricTable'; - -import APIClient from 'App/api_client'; import CustomMetricPieChart from '../CustomMetricPieChart'; -const customParams = rangeName => { +const customParams = (rangeName: string) => { const params = { density: 70 } - if (rangeName === LAST_24_HOURS) params.density = 70 - if (rangeName === LAST_30_MINUTES) params.density = 70 - if (rangeName === YESTERDAY) params.density = 70 - if (rangeName === LAST_7_DAYS) params.density = 70 + // if (rangeName === LAST_24_HOURS) params.density = 70 + // if (rangeName === LAST_30_MINUTES) params.density = 70 + // if (rangeName === YESTERDAY) params.density = 70 + // if (rangeName === LAST_7_DAYS) params.density = 70 return params } @@ -30,23 +26,18 @@ const customParams = rangeName => { interface Props { metric: any; data?: any; - showSync?: boolean; - // compare?: boolean; onClickEdit?: (e) => void; remove: (id) => void; edit: (metric) => void; } function CustomMetricWidget(props: Props) { - const { metric, showSync } = props; + const { metric } = props; const [loading, setLoading] = useState(false) const [data, setData] = useState({ chart: [{}] }) - const [seriesMap, setSeriesMap] = useState([]); const [period, setPeriod] = useState(Period({ rangeName: metric.rangeName, startDate: metric.startDate, endDate: metric.endDate })); const colors = Styles.customMetricColors; const params = customParams(period.rangeName) - const gradientDef = Styles.gradientDef(); - const metricParams = { ...params, metricId: metric.metricId, viewType: 'lineChart' } const prevMetricRef = useRef(); const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; @@ -59,29 +50,6 @@ function CustomMetricWidget(props: Props) { }; prevMetricRef.current = metric; setLoading(true); - - // fetch new data for the widget preview - // new APIClient()['post']('/custom_metrics/try', { ...metricParams, ...metric.toSaveData() }) - // .then(response => response.json()) - // .then(({ errors, data }) => { - // if (errors) { - // console.log('err', errors) - // } else { - // const namesMap = data - // .map(i => Object.keys(i)) - // .flat() - // .filter(i => i !== 'time' && i !== 'timestamp') - // .reduce((unique: any, item: any) => { - // if (!unique.includes(item)) { - // unique.push(item); - // } - // return unique; - // }, []); - - // setSeriesMap(namesMap); - // setData(getChartFormatter(period)(data)); - // } - // }).finally(() => setLoading(false)); }, [metric]) const onDateChange = (changedDates) => { @@ -185,7 +153,6 @@ function CustomMetricWidget(props: Props) { metric={metric} data={data[0]} colors={colors} - params={params} /> )} diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx index ad8663390..6b155364d 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/SessionsPerBrowser/SessionsPerBrowser.tsx @@ -13,7 +13,7 @@ function SessionsPerBrowser(props: Props) { const getVersions = item => { return Object.keys(item) - .filter(i => i !== 'browser' && i !== 'count') + .filter(i => i !== 'browser' && i !== 'count' && i !== 'time' && i !== 'timestamp') .map(i => ({ key: 'v' +i, value: item[i]})) } return ( diff --git a/frontend/app/components/Dashboard/Widgets/common/Styles.js b/frontend/app/components/Dashboard/Widgets/common/Styles.js index 6da043a28..1804b6dbc 100644 --- a/frontend/app/components/Dashboard/Widgets/common/Styles.js +++ b/frontend/app/components/Dashboard/Widgets/common/Styles.js @@ -1,7 +1,6 @@ import React from 'react'; import { numberWithCommas } from 'App/utils'; - -const colors = ['#3EAAAF', '#5FBABF', '#7BCBCF', '#96DCDF', '#ADDCDF']; +const colors = ['#1E889A', '#239DB2', '#28B2C9', '#36C0D7', '#65CFE1']; const colorsx = ['#256669', '#38999e', '#3eaaaf', '#51b3b7', '#78c4c7', '#9fd5d7', '#c5e6e7'].reverse(); const compareColors = ['#394EFF', '#4D5FFF', '#808DFF', '#B3BBFF', '#E5E8FF']; const compareColorsx = ["#222F99", "#2E3ECC", "#394EFF", "#6171FF", "#8895FF", "#B0B8FF", "#D7DCFF"].reverse(); diff --git a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx index 1add670ec..0a89c0072 100644 --- a/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx +++ b/frontend/app/components/Dashboard/components/DashboardRouter/DashboardRouter.tsx @@ -5,6 +5,7 @@ import { withRouter } from 'react-router-dom'; import { metrics, metricDetails, + metricDetailsSub, dashboardSelected, dashboardMetricCreate, dashboardMetricDetails, @@ -14,6 +15,7 @@ import { import DashboardView from '../DashboardView'; import MetricsView from '../MetricsView'; import WidgetView from '../WidgetView'; +import WidgetSubDetailsView from '../WidgetSubDetailsView'; function DashboardViewSelected({ siteId, dashboardId }) { return ( @@ -37,6 +39,10 @@ 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 eed6bbaf9..fd6bd2ee3 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -123,15 +123,6 @@ function DashboardView(props: RouteComponentProps) {
- {/* Time Range */} - {/* dashboardStore.setPeriod(period)} - customRangeRight - direction="left" - /> */} + Errors List Item +
+ ); +} + +export default ErrorListItem; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts new file mode 100644 index 000000000..b37bfbbca --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorListItem/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorListItem' \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorsList/ErrorsList.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorsList/ErrorsList.tsx new file mode 100644 index 000000000..a6a9bca4c --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorsList/ErrorsList.tsx @@ -0,0 +1,21 @@ +import React, { useEffect } from 'react'; +import ErrorListItem from '../ErrorListItem'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +function ErrorsList(props) { + const { errorStore, metricStore } = useStore(); + const metric = useObserver(() => metricStore.instance); + + useEffect(() => { + errorStore.fetchErrors(); + }, []); + return ( +
+ Errors List + +
+ ); +} + +export default ErrorsList; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorsList/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorsList/index.ts new file mode 100644 index 000000000..7dd28915c --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorsList/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorsList'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/ErrorsWidget.tsx b/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/ErrorsWidget.tsx new file mode 100644 index 000000000..b2862d040 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/ErrorsWidget.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ErrorsList from '../ErrorsList'; + +function ErrorsWidget(props) { + return ( +
+ +
+ ); +} + +export default ErrorsWidget; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/index.ts b/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/index.ts new file mode 100644 index 000000000..8f62f38a4 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Errors/ErrorsWidget/index.ts @@ -0,0 +1 @@ +export { default } from './ErrorsWidget'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx new file mode 100644 index 000000000..6a0a02897 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/FunnelIssueDetails.tsx @@ -0,0 +1,46 @@ +import React, { useEffect, useState } from 'react'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; +import { Loader } from 'UI'; +import FunnelIssuesListItem from '../FunnelIssuesListItem'; +import SessionItem from 'App/components/shared/SessionItem/SessionItem'; + +interface Props { + issueId: string; +} +function FunnelIssueDetails(props: Props) { + const { dashboardStore, metricStore } = useStore(); + const { issueId } = props; + const filter = useObserver(() => dashboardStore.drillDownFilter); + const widget = useObserver(() => metricStore.instance); + const [loading, setLoading] = useState(false); + const [funnelIssue, setFunnelIssue] = useState(null); + const [sessions, setSessions] = useState([]); + + useEffect(() => { + setLoading(true); + widget.fetchIssue(widget.metricId, issueId, filter).then((resp: any) => { + setFunnelIssue(resp.issue); + setSessions(resp.sessions); + }).finally(() => { + setLoading(false); + }); + }, []); + + return ( + + { funnelIssue && } + +
+ {sessions.map((session: any) => ( + + ))} +
+
+ ); +} + +export default FunnelIssueDetails; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts new file mode 100644 index 000000000..486b120d5 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueDetails/index.ts @@ -0,0 +1 @@ +export { default } from './FunnelIssueDetails'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx new file mode 100644 index 000000000..4a69dc280 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/FunnelIssueGraph.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Popup } from 'UI'; + +const MIN_WIDTH = '20px'; +interface Props { + issue: any +} +function FunnelIssueGraph(props: Props) { + const { issue } = props; + return ( +
+ +
+
{issue.unaffectedSessions}
+
+ } + content={ `Unaffected sessions` } + size="tiny" + inverted + position="top center" + /> + +
+
{issue.affectedSessions}
+ {/*
{issue.affectedSessionsPer}
*/} +
+ } + content={ `Affected sessions` } + size="tiny" + inverted + position="top center" + /> + +
+
{issue.lostConversions}
+
+ } + content={ `Conversion lost` } + size="tiny" + inverted + position="top center" + /> +
+ ); +} + +export default FunnelIssueGraph; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/index.ts new file mode 100644 index 000000000..11aed8599 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueGraph/index.ts @@ -0,0 +1 @@ +export { default } from './FunnelIssueGraph'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/FunnelIssueModal.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/FunnelIssueModal.tsx new file mode 100644 index 000000000..bf9b6acf8 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/FunnelIssueModal.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import { useStore } from 'App/mstore'; +import { useModal } from 'App/components/Modal'; +import FunnelIssueDetails from '../FunnelIssueDetails'; + +interface Props { + issueId: string; +} +function FunnelIssueModal(props: Props) { + const { issueId } = props; + const { hideModal } = useModal(); + return ( +
+
+ +
+
+ ); +} + +export default FunnelIssueModal; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts new file mode 100644 index 000000000..0261453c0 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssueModal/index.ts @@ -0,0 +1 @@ +export { default } from './FunnelIssueModal'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx new file mode 100644 index 000000000..d1f817a21 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/FunnelIssues.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; +import { NoContent, Loader } from 'UI'; +import FunnelIssuesDropdown from '../FunnelIssuesDropdown'; +import FunnelIssuesSort from '../FunnelIssuesSort'; +import FunnelIssuesList from '../FunnelIssuesList'; +import { DateTime } from 'luxon'; +import { debounce } from 'App/utils'; +import useIsMounted from 'App/hooks/useIsMounted' +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; + +function FunnelIssues() { + const { metricStore, dashboardStore } = useStore(); + const [data, setData] = useState({ issues: [] }); + const [loading, setLoading] = useState(false); + const isMounted = useIsMounted() + // const funnel = useObserver(() => funnelStore.instance); + // const funnel = useObserver(() => metricStore.instance); + // const issues = useObserver(() => funnelStore.issues); + // const loading = useObserver(() => funnelStore.isLoadingIssues); + + const fetchIssues = (filter: any) => { + if (!isMounted()) return; + setLoading(true) + widget.fetchIssues(filter).then((res: any) => { + setData(res) + }).finally(() => { + setLoading(false) + }); + } + + const filter = useObserver(() => dashboardStore.drillDownFilter); + const widget: any = useObserver(() => metricStore.instance); + const startTime = DateTime.fromMillis(filter.startTimestamp).toFormat('LLL dd, yyyy HH:mm a'); + const endTime = DateTime.fromMillis(filter.endTimestamp).toFormat('LLL dd, yyyy HH:mm a'); + const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []); + + + const depsString = JSON.stringify(widget.series); + useEffect(() => { + debounceRequest({ ...filter, series: widget.toJsonDrilldown(), page: metricStore.sessionsPage, limit: metricStore.sessionsPageSize }); + }, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage]); + + return useObserver(() => ( +
+
+

Most significant issues identified in this funnel

+
+
+ +
+ +
+
+ + + +
No issues found
+
+ } + > + + + +
+ )); +} + +export default FunnelIssues; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssues/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx new file mode 100644 index 000000000..547e896ed --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesDropdown/FunnelIssuesDropdown.tsx @@ -0,0 +1,96 @@ +import React, { Component, ReactNode, FunctionComponent, useEffect } from 'react'; +import Select from 'Shared/Select' +import { components } from 'react-select'; +import { Icon } from 'UI'; +import FunnelIssuesSelectedFilters from '../FunnelIssuesSelectedFilters'; +import { useStore } from 'App/mstore'; + +const options = [ + { value: "click_rage", label: "Click Rage" }, + { value: "dead_click", label: "Dead Click" }, + { value: "excessive_scrolling", label: "Excessive Scrolling" }, + { value: "bad_request", label: "Bad Request" }, + { value: "missing_resource", label: "Missing Image" }, + { value: "memory", label: "High Memory Usage" }, + { value: "cpu", label: "High CPU" }, + { value: "slow_resource", label: "Slow Resource" }, + { value: "slow_page_load", label: "Slow Page" }, + { value: "crash", label: "Crash" }, + { value: "custom_event_error", label: "Custom Error" }, + { value: "js_error", label: "Error" } +] + +function FunnelIssuesDropdown(props) { + const { funnelStore } = useStore(); + const [isOpen, setIsOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + const filteredOptions = options.filter((option: any) => { + return !selectedValues.includes(option.value); + }); + + const selectedOptions = options.filter((option: any) => { + return selectedValues.includes(option.value); + }); + + useEffect(() => { + funnelStore.updateKey('issuesFilter', selectedOptions); + }, [selectedOptions]); + + const handleChange = ({ value }: any) => { + toggleSelectedValue(value.value); + } + + const toggleSelectedValue = (value: string) => { + if (selectedValues.includes(value)) { + setSelectedValues(selectedValues.filter(v => v !== value)); + } else { + setSelectedValues([...selectedValues, value]); + } + } + + return ( +
+ +
+ ); +} + +export default FunnelIssuesSort; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts new file mode 100644 index 000000000..895b1eddd --- /dev/null +++ b/frontend/app/components/Dashboard/components/Funnels/FunnelIssuesSort/index.ts @@ -0,0 +1 @@ +export { default } from './FunnelIssuesSort'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionList/SessionList.tsx b/frontend/app/components/Dashboard/components/Sessions/SessionList/SessionList.tsx new file mode 100644 index 000000000..f85110edb --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionList/SessionList.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import SessionListItem from '../SessionListItem'; + +function SessionList(props) { + return ( +
+ Session list + +
+ ); +} + +export default SessionList; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionList/index.ts b/frontend/app/components/Dashboard/components/Sessions/SessionList/index.ts new file mode 100644 index 000000000..779c9df2a --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionList/index.ts @@ -0,0 +1 @@ +export { default } from './SessionList'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionListItem/SessionListItem.tsx b/frontend/app/components/Dashboard/components/Sessions/SessionListItem/SessionListItem.tsx new file mode 100644 index 000000000..898d2d341 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionListItem/SessionListItem.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +interface Props { + session: any; +} +function SessionListItem(props: Props) { + return ( +
+ Session list item +
+ ); +} + +export default SessionListItem; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionListItem/index.ts b/frontend/app/components/Dashboard/components/Sessions/SessionListItem/index.ts new file mode 100644 index 000000000..1c2791143 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionListItem/index.ts @@ -0,0 +1 @@ +export { default } from './SessionListItem'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionWidget/SessionWidget.tsx b/frontend/app/components/Dashboard/components/Sessions/SessionWidget/SessionWidget.tsx new file mode 100644 index 000000000..0eb5ca1a5 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionWidget/SessionWidget.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SessionList from '../SessionList'; + +function SessionWidget(props) { + return ( +
+ +
+ ); +} + +export default SessionWidget; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/Sessions/SessionWidget/index.ts b/frontend/app/components/Dashboard/components/Sessions/SessionWidget/index.ts new file mode 100644 index 000000000..64b9563f5 --- /dev/null +++ b/frontend/app/components/Dashboard/components/Sessions/SessionWidget/index.ts @@ -0,0 +1 @@ +export { default } from './SessionWidget'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index bdd9d220f..62ab68a1d 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -13,6 +13,9 @@ import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted' +import FunnelWidget from 'App/components/Funnels/FunnelWidget'; +import ErrorsWidget from '../Errors/ErrorsWidget'; +import SessionWidget from '../Sessions/SessionWidget'; interface Props { metric: any; isWidget?: boolean; @@ -35,6 +38,7 @@ function WidgetChart(props: Props) { const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table'; const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart'; + const isFunnel = metric.metricType === 'funnel'; const onChartClick = (event: any) => { if (event) { @@ -60,7 +64,7 @@ function WidgetChart(props: Props) { const depsString = JSON.stringify(_metric.series); - const fetchMetricChartData = (metric, payload, isWidget) => { + const fetchMetricChartData = (metric: any, payload: any, isWidget: any) => { if (!isMounted()) return; setLoading(true) dashboardStore.fetchMetricChartData(metric, payload, isWidget).then((res: any) => { @@ -84,6 +88,18 @@ function WidgetChart(props: Props) { const renderChart = () => { const { metricType, viewType } = metric; + if (metricType === 'sessions') { + return + } + + if (metricType === 'errors') { + return + } + + if (metricType === 'funnel') { + return + } + if (metricType === 'predefined') { if (isOverviewWidget) { return @@ -135,7 +151,7 @@ function WidgetChart(props: Props) { return
Unknown
; } return useObserver(() => ( - + {renderChart()} )); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 83dab5b7d..9d95fc503 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import DropdownPlain from 'Shared/DropdownPlain'; import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; @@ -7,6 +6,7 @@ import { useObserver } from 'mobx-react-lite'; import { Button, Icon } from 'UI' import FilterSeries from '../FilterSeries'; import { confirm } from 'UI'; +import Select from 'Shared/Select' import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes' import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal'; @@ -22,27 +22,36 @@ function WidgetForm(props: Props) { const { metricStore, dashboardStore } = useStore(); const dashboards = dashboardStore.dashboards; const isSaving = useObserver(() => metricStore.isSaving); - const metric: any = useObserver(() => metricStore.instance); + const metric: any = useObserver(() => metricStore.instance) const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); const tableOptions = metricOf.filter(i => i.type === 'table'); const isTable = metric.metricType === 'table'; - const _issueOptions = [{ text: 'All', value: 'all' }].concat(issueOptions); + const isFunnel = metric.metricType === 'funnel'; + const isErrors = metric.metricType === 'errors'; + const isSessions = metric.metricType === 'sessions'; + const _issueOptions = [{ label: 'All', value: 'all' }].concat(issueOptions); const canAddToDashboard = metric.exists() && dashboards.length > 0; const canAddSeries = metric.series.length < 3; - const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value }); - const writeOption = (e, { value, name }) => { - const obj = { [ name ]: value }; + // const write = ({ target: { value, name } }) => metricStore.merge({ [ name ]: value }); + const writeOption = ({ value, name }: any) => { + value = Array.isArray(value) ? value : value.value + const obj: any = { [ name ]: value }; if (name === 'metricValue') { - obj['metricValue'] = [value]; + obj['metricValue'] = value; + + // handle issues (remove all when other option is selected) + if (Array.isArray(obj['metricValue']) && obj['metricValue'].length > 1) { + obj['metricValue'] = obj['metricValue'].filter(i => i.value !== 'all'); + } } if (name === 'metricOf') { - if (value === FilterKey.ISSUE) { - obj['metricValue'] = ['all']; - } + // if (value === FilterKey.ISSUE) { + // obj['metricValue'] = [{ value: 'all', label: 'All' }]; + // } } if (name === 'metricType') { @@ -60,14 +69,13 @@ function WidgetForm(props: Props) { const onSave = () => { const wasCreating = !metric.exists() - metricStore.save(metric, dashboardId).then((metric) => { + metricStore.save(metric, dashboardId).then((metric: any) => { if (wasCreating) { if (parseInt(dashboardId) > 0) { history.replace(withSiteId(dashboardMetricDetails(parseInt(dashboardId), metric.metricId), siteId)); } else { history.replace(withSiteId(metricDetails(metric.metricId), siteId)); } - } }); } @@ -87,24 +95,24 @@ function WidgetForm(props: Props) { } return useObserver(() => ( -
+
- i.value === metric.metricType) || metricTypes[0]} onChange={ writeOption } /> {metric.metricType === 'timeseries' && ( <> of - @@ -113,10 +121,10 @@ function WidgetForm(props: Props) { {metric.metricType === 'table' && ( <> of - @@ -125,11 +133,13 @@ function WidgetForm(props: Props) { {metric.metricOf === FilterKey.ISSUE && ( <> issue type - )} @@ -137,12 +147,12 @@ function WidgetForm(props: Props) { {metric.metricType === 'table' && ( <> showing - @@ -151,9 +161,9 @@ function WidgetForm(props: Props) {
-
- {`${isTable ? 'Filter by' : 'Chart Series'}`} - {!isTable && ( +
+ {`${(isTable || isFunnel) ? 'Filter by' : 'Chart Series'}`} + {!isTable && !isFunnel && ( +
+
+ ))} +
+
+
+ Lost conversions +
+ {funnel.lostConversions} + (12%) +
+
+
+
+ Total conversions +
+ 20 + (12%) +
+
+
+
+ Affected users +
+ {funnel.affectedUsers} + {/* (12%) */} +
+
+
+ + )); +} + +export default FunnelWidget; \ No newline at end of file diff --git a/frontend/app/components/Funnels/FunnelWidget/index.ts b/frontend/app/components/Funnels/FunnelWidget/index.ts new file mode 100644 index 000000000..e2e5d1797 --- /dev/null +++ b/frontend/app/components/Funnels/FunnelWidget/index.ts @@ -0,0 +1 @@ +export { default } from './FunnelWidget'; \ No newline at end of file diff --git a/frontend/app/components/Header/Header.js b/frontend/app/components/Header/Header.js index 5b4c14b27..9803a06f2 100644 --- a/frontend/app/components/Header/Header.js +++ b/frontend/app/components/Header/Header.js @@ -7,6 +7,7 @@ import { assist, client, errors, + funnels, dashboard, withSiteId, CLIENT_DEFAULT_TAB, @@ -31,6 +32,7 @@ const DASHBOARD_PATH = dashboard(); const SESSIONS_PATH = sessions(); const ASSIST_PATH = assist(); const ERRORS_PATH = errors(); +const FUNNELS_PATH = funnels(); const CLIENT_PATH = client(CLIENT_DEFAULT_TAB); const AUTOREFRESH_INTERVAL = 30 * 1000; @@ -105,6 +107,13 @@ const Header = (props) => { > { 'Errors' } + + { 'Funnels' } + { var initOpts = { projectKey: "PROJECT_KEY", ingestPoint: "https://${window.location.hostname}/ingest", - defaultInputMode: ${inputModeOptionsMap[gdpr.defaultInputMode]}, + defaultInputMode: ${gdpr.defaultInputMode}, obscureTextNumbers: ${gdpr.maskNumbers}, obscureTextEmails: ${gdpr.maskEmails}, }; @@ -65,18 +66,13 @@ const ProjectCodeSnippet = props => { } const onChangeSelect = ({ name, value }) => { - // console.log(name, value) - // const { gdpr } = site; const _gdpr = { ...gdpr.toData() }; - props.editGDPR({ [ name ]: value }); _gdpr[name] = value; props.editGDPR({ [ name ]: value }); saveGDPR(_gdpr) }; const onChangeOption = ({ target: { name, checked } }) => { - // const { gdpr } = site; - console.log(name, checked) const _gdpr = { ...gdpr.toData() }; _gdpr[name] = checked; props.editGDPR({ [ name ]: checked }); @@ -121,7 +117,7 @@ const ProjectCodeSnippet = props => {