From 9b558d444a1a1e38ba38b218ce9e0309cb6e533f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 9 Jan 2023 16:13:06 +0100 Subject: [PATCH 01/15] feat(ui) - insights - wip --- .../InsightsCard/InsightItem.tsx | 25 ++++ .../InsightsCard/InsightsCard.tsx | 31 +++++ .../InsightsCard/index.ts | 1 + .../components/WidgetChart/WidgetChart.tsx | 7 +- .../components/WidgetForm/WidgetForm.tsx | 117 ++++++++++-------- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 1 + .../components/WidgetView/WidgetView.tsx | 16 ++- frontend/app/components/ui/SVG.tsx | 3 +- frontend/app/constants/card.ts | 7 ++ frontend/app/constants/filterOptions.js | 10 +- frontend/app/mstore/metricStore.ts | 89 ++++++++++--- .../app/svg/icons/arrow-counterclockwise.svg | 4 + frontend/app/types/filter/filterType.ts | 7 ++ 13 files changed, 244 insertions(+), 74 deletions(-) create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts create mode 100644 frontend/app/svg/icons/arrow-counterclockwise.svg diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx new file mode 100644 index 000000000..41ac503be --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Icon } from 'UI'; + +interface Props { + item: any; + onClick?: (e: React.MouseEvent) => void; +} +function InsightItem(props: Props) { + const { item, onClick = () => {} } = props; + return ( +
+ +
{item.ratio}
+
on
+
Update
+
increased by
+
{item.increase}
+
+ ); +} + +export default InsightItem; diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx new file mode 100644 index 000000000..a100158e6 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx @@ -0,0 +1,31 @@ +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import InsightItem from './InsightItem'; + +const data = [ + { icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' }, + { icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'yello' }, + { icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'green' }, + { icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'gray' }, + { icon: 'dizzy', ratio: 'Click Rage', increase: 10, iconColor: 'red' }, +]; +interface Props {} +function InsightsCard(props: Props) { + const { metricStore } = useStore(); + const metric = metricStore.instance; + + const clickHanddler = (e: React.MouseEvent) => { + console.log(e); + }; + + return ( +
+ {data.map((item) => ( + + ))} +
+ ); +} + +export default observer(InsightsCard); diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts new file mode 100644 index 000000000..bd85f2e4b --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/index.ts @@ -0,0 +1 @@ +export { default } from './InsightsCard' \ 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 3291c0006..d522c299a 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -13,12 +13,13 @@ import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted' import { FilterKey } from 'Types/filter/filterType'; -import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS } from 'App/constants/card'; +import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS, INSIGHTS } from 'App/constants/card'; import FunnelWidget from 'App/components/Funnels/FunnelWidget'; import SessionWidget from '../Sessions/SessionWidget'; import CustomMetricTableSessions from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableSessions'; import CustomMetricTableErrors from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricTableErrors'; import ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard' +import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard'; interface Props { metric: any; @@ -193,6 +194,10 @@ function WidgetChart(props: Props) { ) } + if (metricType === INSIGHTS) { + return + } + return
Unknown metric type
; } return ( diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index d9eaf0e65..64ebef507 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { metricOf, issueOptions } from 'App/constants/filterOptions'; +import React, { useEffect, useState } from 'react'; +import { metricOf, issueOptions, issueCategories } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; @@ -18,9 +18,11 @@ import { RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS, + INSIGHTS, } from 'App/constants/card'; -import { clickmapFilter, eventKeys } from 'App/types/filter/newFilter'; +import { eventKeys } from 'App/types/filter/newFilter'; import { renderClickmapThumbnail } from './renderMap'; +import Widget from 'App/mstore/types/widget'; interface Props { history: any; match: any; @@ -37,68 +39,45 @@ function WidgetForm(props: Props) { const { metricStore, dashboardStore } = useStore(); const isSaving = metricStore.isSaving; const metric: any = metricStore.instance; + const [initialInstance, setInitialInstance] = useState(); const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); const tableOptions = metricOf.filter((i) => i.type === 'table'); - const isTable = metric.metricType === 'table'; + const isTable = metric.metricType === TABLE; const isClickmap = metric.metricType === CLICKMAP; - const isFunnel = metric.metricType === 'funnel'; + const isFunnel = metric.metricType === FUNNEL; + const isInsights = metric.metricType === INSIGHTS; const canAddSeries = metric.series.length < 3; const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length; const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); + const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes( metric.metricType ); - const excludeFilterKeys = isClickmap ? eventKeys : [] + const excludeFilterKeys = isClickmap ? eventKeys : []; + + useEffect(() => { + if (!!metric && !initialInstance) { + setInitialInstance(metric.toJson()); + } + }, [metric]); const writeOption = ({ value, name }: { value: any; name: any }) => { value = Array.isArray(value) ? value : value.value; const obj: any = { [name]: value }; - if (name === 'metricValue') { - obj.metricValue = value; - - if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) { - obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all'); - } - } - if (name === 'metricType') { switch (value) { case TIMESERIES: obj.metricOf = timeseriesOptions[0].value; - obj.viewType = 'lineChart'; break; case TABLE: obj.metricOf = tableOptions[0].value; - obj.viewType = 'table'; - break; - case FUNNEL: - obj.metricOf = 'sessionCount'; - break; - case ERRORS: - case RESOURCE_MONITORING: - case PERFORMANCE: - case WEB_VITALS: - obj.viewType = 'chart'; - break; - case CLICKMAP: - obj.viewType = 'chart'; - - if (value !== CLICKMAP) { - metric.series[0].filter.removeFilter(0); - } - - if (metric.series[0].filter.filters.length < 1) { - metric.series[0].filter.addFilter({ - ...clickmapFilter, - value: [''], - }); - } break; } } + metricStore.merge(obj); }; @@ -112,10 +91,16 @@ function WidgetForm(props: Props) { } } const savedMetric = await metricStore.save(metric); + setInitialInstance(metric.toJson()) if (wasCreating) { if (parseInt(dashboardId, 10) > 0) { - history.replace(withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)); - dashboardStore.addWidgetToDashboard(dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [savedMetric.metricId]); + history.replace( + withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId) + ); + dashboardStore.addWidgetToDashboard( + dashboardStore.getDashboard(parseInt(dashboardId, 10))!, + [savedMetric.metricId] + ); } else { history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId)); } @@ -134,6 +119,11 @@ function WidgetForm(props: Props) { } }; + const undoChnages = () => { + const w = new Widget(); + metricStore.merge(w.fromJson(initialInstance), false); + }; + return (
@@ -142,7 +132,7 @@ function WidgetForm(props: Props) { - {metric.metricOf === FilterKey.ISSUE && ( + {metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && ( <> issue type + + )} + {metric.metricType === 'table' && !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( <> @@ -183,8 +187,8 @@ function WidgetForm(props: Props) { {!isPredefined && (
- {`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`} - {!isTable && !isFunnel && !isClickmap && ( + {`${isTable || isFunnel || isClickmap || isInsights ? 'Filter by' : 'Chart Series'}`} + {!isTable && !isFunnel && !isClickmap && !isInsights && ( +
+ + {metric.exists() && metric.hasChanged && ( + + )} +
{metric.exists() && ( diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index ac47765b8..f2faf0d5a 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -43,6 +43,7 @@ function MetricTypeDropdown(props: Props) { const onChange = (type: string) => { metricStore.changeType(type); }; + return ( metricStore.updateKey('sort', { by: value.value })} /> -
+
*/}
@@ -52,8 +55,81 @@ function MetricViewHeader() { Create custom Cards to capture key interactions and track KPIs.
+
+ + +
+ metricStore.updateKey('sort', { by: value.value })} + plain={true} + /> + + metricStore.updateKey('filter', { ...filter, dashboard: value})} + /> +
+
); } -export default MetricViewHeader; +export default observer(MetricViewHeader); + +function DashboardDropdown({ onChange, plain = false }: { plain?: boolean; onChange: any }) { + const { dashboardStore, metricStore } = useStore(); + const dashboardOptions = dashboardStore.dashboards.map((i: any) => ({ + key: i.id, + label: i.name, + value: i.dashboardId, + })); + + return ( + -
- )); + const write = ({ target: { value } }: any) => { + setQuery(value); + debounceUpdate('metricsSearch', value); + }; + + return useObserver(() => ( +
+ + +
+ )); } export default MetricsSearch; diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index f2faf0d5a..c85389529 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -1,7 +1,6 @@ -import React, { useMemo } from 'react'; -import { TYPES, LIBRARY } from 'App/constants/card'; +import React from 'react'; +import { DROPDOWN_OPTIONS, Option } from 'App/constants/card'; import Select from 'Shared/Select'; -import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem'; import { components } from 'react-select'; import CustomDropdownOption from 'Shared/CustomDropdownOption'; import { observer } from 'mobx-react-lite'; @@ -22,20 +21,11 @@ interface Props { function MetricTypeDropdown(props: Props) { const { metricStore } = useStore(); const metric: any = metricStore.instance; - const options: Options[] = useMemo(() => { - // TYPES.shift(); // remove "Add from library" item - return TYPES.filter((i: MetricType) => i.slug !== LIBRARY).map((i: MetricType) => ({ - label: i.title, - icon: i.icon, - value: i.slug, - description: i.description, - })); - }, []); React.useEffect(() => { const queryCardType = props.query.get('type'); - if (queryCardType && options.length > 0 && metric.metricType) { - const type = options.find((i) => i.value === queryCardType); + if (queryCardType && DROPDOWN_OPTIONS.length > 0 && metric.metricType) { + const type: Option = DROPDOWN_OPTIONS.find((i) => i.value === queryCardType) as Option; setTimeout(() => onChange(type.value), 0); } }, []); @@ -48,13 +38,16 @@ function MetricTypeDropdown(props: Props) { - + const { dashboardStore } = useStore(); + const [query, setQuery] = useState(dashboardStore.dashboardsSearch); + useEffect(() => { + debounceUpdate = debounce( + (key: string, value: any) => + dashboardStore.updateKey('filter', { ...dashboardStore.filter, query: value }), + 500 ); + }, []); + + // @ts-ignore + const write = ({ target: { value } }) => { + setQuery(value); + debounceUpdate('dashboardsSearch', value); + }; + + return ( +
+ + +
+ ); } export default observer(DashboardSearch); diff --git a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx index f77d3ae69..560c27c61 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx @@ -19,30 +19,49 @@ function Header({ history, siteId }: { history: any; siteId: string }) { }; return ( -
-
- -
-
- -
- + dashboardStore.updateKey('filter', { + ...dashboardStore.filter, + visibility: value.value, + }) + } + /> + + metricStore.updateKey('sort', { by: value.value })} - /> -
*/}
@@ -65,6 +39,7 @@ function MetricViewHeader() { defaultValue={filter.type} onChange={({ value }) => metricStore.updateKey('filter', { ...filter, type: value.value})} plain={true} + isSearchable={true} /> metricStore.updateKey('filter', { ...filter, type: value.value})} + onChange={({ value }) => + metricStore.updateKey('filter', { ...filter, type: value.value }) + } plain={true} isSearchable={true} /> @@ -55,7 +66,9 @@ function MetricViewHeader() { metricStore.updateKey('filter', { ...filter, dashboard: value})} + onChange={(value: any) => + metricStore.updateKey('filter', { ...filter, dashboard: value }) + } />
diff --git a/frontend/app/components/ui/Toggler/toggler.module.css b/frontend/app/components/ui/Toggler/toggler.module.css index 26d82880c..0b2d7ac68 100644 --- a/frontend/app/components/ui/Toggler/toggler.module.css +++ b/frontend/app/components/ui/Toggler/toggler.module.css @@ -13,7 +13,7 @@ & span { padding-left: 10px; - color: $gray-medium; + /* color: $gray-dark; */ } } .switch input { diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index af7050955..cfcb5f19f 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -6,7 +6,6 @@ import Error from './types/error'; import { TIMESERIES, TABLE, - FUNNEL, ERRORS, RESOURCE_MONITORING, @@ -61,6 +60,9 @@ export default class MetricStore { return this.metrics .filter( (card) => + (this.filter.showMine + ? card.owner === JSON.parse(localStorage.getItem('user')!).account.email + : true) && (this.filter.type === 'all' || card.metricType === this.filter.type) && (!dbIds.length || card.dashboards.map((i) => i.dashboardId).some((id) => dbIds.includes(id))) &&