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..3705ceaa7 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightItem.tsx @@ -0,0 +1,114 @@ +import { IssueCategory } from 'App/types/filter/filterType'; +import React from 'react'; +import { Icon } from 'UI'; +import cn from 'classnames'; + +interface Props { + item: any; + onClick?: (e: React.MouseEvent) => void; +} +function InsightItem(props: Props) { + const { item, onClick = () => {} } = props; + const className = + 'flex items-center py-4 hover:bg-active-blue -mx-4 px-4 border-b last:border-transparent cursor-pointer'; + + switch (item.category) { + case IssueCategory.RAGE: + return ; + case IssueCategory.RESOURCES: + return ; + case IssueCategory.ERRORS: + return ; + case IssueCategory.NETWORK: + return ; + default: + return null; + } +} + +export default InsightItem; + + +function Change({ change, isIncreased }: any) { + return ( +
+ + {change}% +
+ ); +} + +function ErrorItem({ item, className, onClick }: any) { + return ( +
+ + {item.isNew ? ( + <> +
{item.name}
+
error observed
+
{item.ratio}%
+
more than other new errors
+ + ) : ( + <> +
Increase
+
in
+
{item.name}
+ + + )} +
+ ); +} + +function NetworkItem({ item, className, onClick }: any) { + return ( +
+ +
Network request
+
{item.name}
+
{item.change > 0 ? 'increased' : 'decreased'}
+ +
+ ); +} + +function ResourcesItem({ item, className, onClick }: any) { + return ( +
+ +
{item.change > 0 ? 'Inrease' : 'Decrease'}
+
in
+
{item.name}
+ +
+ ); +} + +function RageItem({ item, className, onClick }: any) { + return ( +
+ +
{item.isNew ? item.name : 'Click Rage'}
+ {item.isNew &&
has
} + {!item.isNew &&
on
} + {item.isNew &&
{item.ratio}%
} + {item.isNew &&
more clickrage than other raged elements.
} + {!item.isNew && ( + <> +
increase by
+ + + )} +
+ ); +} 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..9ac804853 --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard/InsightsCard.tsx @@ -0,0 +1,80 @@ +import { NoContent } from 'UI'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import InsightItem from './InsightItem'; +import { NO_METRIC_DATA } from 'App/constants/messages'; +import { InishtIssue } from 'App/mstore/types/widget'; +import { FilterKey, IssueCategory, IssueType } from 'App/types/filter/filterType'; +import { filtersMap } from 'Types/filter/newFilter'; + +function InsightsCard() { + const { metricStore, dashboardStore } = useStore(); + const metric = metricStore.instance; + const drillDownFilter = dashboardStore.drillDownFilter; + const period = dashboardStore.period; + + const clickHanddler = (e: React.MouseEvent, item: InishtIssue) => { + let filter: any = {}; + switch (item.category) { + case IssueCategory.RESOURCES: + filter = { + ...filtersMap[ + item.name === IssueType.MEMORY ? FilterKey.AVG_MEMORY_USAGE : FilterKey.AVG_CPU + ], + }; + filter.source = [item.oldValue]; + filter.value = []; + break; + case IssueCategory.RAGE: + filter = { ...filtersMap[FilterKey.CLICK] }; + filter.value = [item.name]; + break; + case IssueCategory.NETWORK: + filter = { ...filtersMap[FilterKey.FETCH_URL] }; + filter.filters = [ + { ...filtersMap[FilterKey.FETCH_URL], value: [item.name] }, + { ...filtersMap[FilterKey.FETCH_DURATION], value: [item.oldValue] }, + ]; + filter.value = []; + break; + case IssueCategory.ERRORS: + filter = { ...filtersMap[FilterKey.ERROR] }; + break; + } + + filter.type = filter.key; + delete filter.key; + delete filter.operatorOptions; + delete filter.sourceOperatorOptions; + delete filter.placeholder; + delete filter.sourcePlaceholder; + delete filter.sourceType; + delete filter.sourceUnit; + delete filter.category; + delete filter.icon; + delete filter.label; + delete filter.options; + + drillDownFilter.merge({ + filters: [filter], + }); + }; + + return ( + +
+ {metric.data.issues && + metric.data.issues.map((item: any) => ( + clickHanddler(e, 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/DashboardList/DashboardList.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx index ace14e03e..2b60ad40a 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx @@ -2,22 +2,14 @@ import { observer } from 'mobx-react-lite'; import React from 'react'; import { NoContent, Pagination } from 'UI'; import { useStore } from 'App/mstore'; -import { filterList } from 'App/utils'; import { sliceListPerPage } from 'App/utils'; import DashboardListItem from './DashboardListItem'; import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; function DashboardList() { const { dashboardStore } = useStore(); - const [shownDashboards, setDashboards] = React.useState([]); - const dashboards = dashboardStore.sortedDashboards; + const list = dashboardStore.filteredList; const dashboardsSearch = dashboardStore.dashboardsSearch; - - React.useEffect(() => { - setDashboards(filterList(dashboards, dashboardsSearch, ['name', 'owner', 'description'])); - }, [dashboardsSearch]); - - const list = dashboardsSearch !== '' ? shownDashboards : dashboards; const lenth = list.length; return ( @@ -38,9 +30,6 @@ function DashboardList() { )} - {/*
- -
*/} } > diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx index a3cb6e436..e076dd6d5 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardListItem.tsx @@ -32,7 +32,6 @@ function DashboardListItem(props: Props) {
{dashboard.name}
- {/*
*/}
diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx index a3b13f1d3..d60a6886c 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardSearch.tsx @@ -4,33 +4,37 @@ import { useStore } from 'App/mstore'; import { Icon } from 'UI'; import { debounce } from 'App/utils'; -let debounceUpdate: any = () => {} +let debounceUpdate: any = () => {}; function DashboardSearch() { - const { dashboardStore } = useStore(); - const [query, setQuery] = useState(dashboardStore.dashboardsSearch); - useEffect(() => { - debounceUpdate = debounce((key: string, value: any) => dashboardStore.updateKey(key, value), 500); - }, []) - - // @ts-ignore - const write = ({ target: { value } }) => { - setQuery(value); - debounceUpdate('dashboardsSearch', value); - } - - return ( -
- - -
+ 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 })} - /> -
@@ -52,8 +29,95 @@ function MetricViewHeader() { Create custom Cards to capture key interactions and track KPIs.
+
+ + +
+ + metricStore.updateKey('filter', { ...filter, showMine: !filter.showMine }) + } + /> + 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/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 3291c0006..5d615815b 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; @@ -94,15 +95,7 @@ function WidgetChart(props: Props) { const renderChart = () => { const { metricType, viewType, metricOf } = metric; - const metricWithData = { ...metric, data }; - if (metricType === 'sessions') { - return - } - - // if (metricType === ERRORS) { - // return - // } if (metricType === FUNNEL) { return @@ -193,6 +186,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..591011214 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)); } @@ -127,13 +112,18 @@ function WidgetForm(props: Props) { await confirm({ header: 'Confirm', confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this metric?`, + confirmation: `Are you sure you want to permanently delete this card?`, }) ) { metricStore.delete(metric).then(props.onDelete); } }; + 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..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); } }, []); @@ -43,17 +33,21 @@ function MetricTypeDropdown(props: Props) { const onChange = (type: string) => { metricStore.changeType(type); }; + return (