From 8bcd63a879a8ea32f1b9e3f9e9840b5d1cd41997 Mon Sep 17 00:00:00 2001 From: Taha Yassine Kraiem Date: Thu, 2 Feb 2023 17:06:23 +0100 Subject: [PATCH 1/4] feat(chalice): ignore cards calls for /chart for clickMaps --- api/chalicelib/core/custom_metrics.py | 4 ++-- api/routers/subs/metrics.py | 5 +++-- ee/api/chalicelib/core/custom_metrics.py | 4 ++-- ee/api/routers/subs/metrics.py | 5 +++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api/chalicelib/core/custom_metrics.py b/api/chalicelib/core/custom_metrics.py index 602f1f7d2..24415a072 100644 --- a/api/chalicelib/core/custom_metrics.py +++ b/api/chalicelib/core/custom_metrics.py @@ -572,7 +572,7 @@ def get_funnel_sessions_by_issue(user_id, project_id, metric_id, issue_id, "issue": issue} -def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChartSchema, from_dashboard=False): +def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChartSchema, ignore_click_map=False): raw_metric: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, include_data=True) if raw_metric is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="card not found") @@ -581,7 +581,7 @@ def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChart return get_predefined_metric(key=metric.metric_of, project_id=project_id, data=data.dict()) elif __is_click_map(metric): # TODO: remove this when UI is able to stop this endpoint calls for clickMap - if from_dashboard: + if ignore_click_map: return None if raw_metric["data"]: keys = sessions_mobs. \ diff --git a/api/routers/subs/metrics.py b/api/routers/subs/metrics.py index 9bd92174d..2bf4e56ff 100644 --- a/api/routers/subs/metrics.py +++ b/api/routers/subs/metrics.py @@ -232,10 +232,11 @@ def get_card_chart(projectId: int, metric_id: int, request: Request, data: schem context: schemas.CurrentContext = Depends(OR_context)): # TODO: remove this when UI is able to stop this endpoint calls for clickMap import re - from_dashboard = re.match(r".*\/[0-9]+\/dashboard\/[0-9]+$", request.headers.get('referer')) is not None \ + ignore_click_map = re.match(r".*\/[0-9]+\/dashboard\/[0-9]+$", request.headers.get('referer')) is not None \ + or re.match(r".*\/[0-9]+\/metrics$", request.headers.get('referer')) is not None \ if request.headers.get('referer') else False data = custom_metrics.make_chart_from_card(project_id=projectId, user_id=context.user_id, metric_id=metric_id, - data=data, from_dashboard=from_dashboard) + data=data, ignore_click_map=ignore_click_map) return {"data": data} diff --git a/ee/api/chalicelib/core/custom_metrics.py b/ee/api/chalicelib/core/custom_metrics.py index 0eb0c3333..94b2289b7 100644 --- a/ee/api/chalicelib/core/custom_metrics.py +++ b/ee/api/chalicelib/core/custom_metrics.py @@ -628,7 +628,7 @@ def get_funnel_sessions_by_issue(user_id, project_id, metric_id, issue_id, "issue": issue} -def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChartSchema, from_dashboard=False): +def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChartSchema, ignore_click_map=False): raw_metric: dict = get_card(metric_id=metric_id, project_id=project_id, user_id=user_id, include_data=True) if raw_metric is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="card not found") @@ -637,7 +637,7 @@ def make_chart_from_card(project_id, user_id, metric_id, data: schemas.CardChart return get_predefined_metric(key=metric.metric_of, project_id=project_id, data=data.dict()) elif __is_click_map(metric): # TODO: remove this when UI is able to stop this endpoint calls for clickMap - if from_dashboard: + if ignore_click_map: return None if raw_metric["data"]: keys = sessions_mobs. \ diff --git a/ee/api/routers/subs/metrics.py b/ee/api/routers/subs/metrics.py index 540147a73..574763d65 100644 --- a/ee/api/routers/subs/metrics.py +++ b/ee/api/routers/subs/metrics.py @@ -234,10 +234,11 @@ def get_card_chart(projectId: int, metric_id: int, request: Request, data: schem context: schemas.CurrentContext = Depends(OR_context)): # TODO: remove this when UI is able to stop this endpoint calls for clickMap import re - from_dashboard = re.match(r".*\/[0-9]+\/dashboard\/[0-9]+$", request.headers.get('referer')) is not None \ + ignore_click_map = re.match(r".*\/[0-9]+\/dashboard\/[0-9]+$", request.headers.get('referer')) is not None \ + or re.match(r".*\/[0-9]+\/metrics$", request.headers.get('referer')) is not None \ if request.headers.get('referer') else False data = custom_metrics.make_chart_from_card(project_id=projectId, user_id=context.user_id, metric_id=metric_id, - data=data, from_dashboard=from_dashboard) + data=data, ignore_click_map=ignore_click_map) return {"data": data} From 10f2ff9864af65a5570741c96f4efd164d962aef Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 2 Feb 2023 16:47:28 +0100 Subject: [PATCH 2/4] change(ui) - dashboard cards limit --- .../DashboardHeader/DashboardHeader.tsx | 29 +++++++------ .../MetricsLibraryModal/FooterContent.tsx | 42 +++++++++++++++++++ .../MetricsLibraryModal.tsx | 42 +++---------------- .../components/MetricsList/ListView.tsx | 1 + .../components/MetricsList/MetricsList.tsx | 10 +++-- 5 files changed, 71 insertions(+), 53 deletions(-) create mode 100644 frontend/app/components/Dashboard/components/MetricsLibraryModal/FooterContent.tsx diff --git a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx index 90ecd4761..5f312d124 100644 --- a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx +++ b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Tooltip } from 'react-tippy'; import Breadcrumb from 'Shared/Breadcrumb'; import { withSiteId } from 'App/routes'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { Button, PageTitle, confirm } from 'UI'; +import { Button, PageTitle, confirm, Tooltip } from 'UI'; import SelectDateRange from 'Shared/SelectDateRange'; import { useStore } from 'App/mstore'; import { useModal } from 'App/components/Modal'; @@ -20,7 +19,7 @@ interface IProps { } type Props = IProps & RouteComponentProps; - +const MAX_CARDS = 30 function DashboardHeader(props: Props) { const { siteId, dashboardId } = props; const { dashboardStore } = useStore(); @@ -30,6 +29,7 @@ function DashboardHeader(props: Props) { const period = dashboardStore.period; const dashboard: any = dashboardStore.selectedDashboard; + const canAddMore: boolean = dashboard?.widgets?.length <= MAX_CARDS; const onEdit = (isTitle: boolean) => { dashboardStore.initDashboard(dashboard); @@ -80,16 +80,19 @@ function DashboardHeader(props: Props) { />
- + + +
dashboardStore.getDashboard(dashboardId), [dashboardId]); + + const existingCardIds = useMemo(() => dashboard?.widgets?.map(i => parseInt(i.metricId)), [dashboard]); + const total = useMemo(() => metricStore.filteredCards.filter(i => !existingCardIds?.includes(parseInt(i.metricId))).length, [metricStore.filteredCards]); + + const addSelectedToDashboard = () => { + if (!dashboard || !dashboard.dashboardId) return; + dashboardStore.addWidgetToDashboard(dashboard, selected).then(() => { + hideModal(); + dashboardStore.fetch(dashboard.dashboardId!); + }); + }; + + return ( +
+
+ Selected {selected.length} of{' '} + {total} +
+
+ + +
+
+ ); + } + + export default observer(FooterContent); + \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 6a998e0c8..0683dce60 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -1,10 +1,10 @@ import Modal from 'App/components/Modal/Modal'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import MetricsList from '../MetricsList'; -import { Button, Icon } from 'UI'; -import { useModal } from 'App/components/Modal'; +import { Icon } from 'UI'; import { useStore } from 'App/mstore'; -import { observer, useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; +import FooterContent from './FooterContent'; interface Props { dashboardId: number; @@ -50,7 +50,7 @@ function MetricsLibraryModal(props: Props) {
- + ); @@ -71,35 +71,3 @@ function MetricSearch({ onChange }: any) {
); } - -function SelectedContent({ dashboardId, selected }: any) { - const { hideModal } = useModal(); - const { metricStore, dashboardStore } = useStore(); - const total = useObserver(() => metricStore.metrics.length); - const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]); - - const addSelectedToDashboard = () => { - if (!dashboard || !dashboard.dashboardId) return; - dashboardStore.addWidgetToDashboard(dashboard, selected).then(() => { - hideModal(); - dashboardStore.fetch(dashboard.dashboardId); - }); - }; - - return ( -
-
- Selected {selected.length} of{' '} - {total} -
-
- - -
-
- ); -} diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index 1236487cc..446ffa431 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -10,6 +10,7 @@ interface Props { toggleAll?: (e: any) => void; disableSelection?: boolean; allSelected?: boolean + existingCardIds?: number[]; } function ListView(props: Props) { const { siteId, list, selectedList, toggleSelection, disableSelection = false, allSelected = false } = props; diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index d815d901d..f2639d37f 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -1,5 +1,5 @@ import { observer, useObserver } from 'mobx-react-lite'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { NoContent, Pagination } from 'UI'; import { useStore } from 'App/mstore'; import { sliceListPerPage } from 'App/utils'; @@ -14,11 +14,14 @@ function MetricsList({ siteId: string; onSelectionChange?: (selected: any[]) => void; }) { - const { metricStore } = useStore(); - const cards = metricStore.filteredCards; + const { metricStore, dashboardStore } = useStore(); const metricsSearch = metricStore.metricsSearch; const listView = useObserver(() => metricStore.listView); const [selectedMetrics, setSelectedMetrics] = useState([]); + + const dashboard = dashboardStore.selectedDashboard; + const existingCardIds = useMemo(() => dashboard?.widgets?.map(i => parseInt(i.metricId)), [dashboard]); + const cards = useMemo(() => metricStore.filteredCards.filter(i => !existingCardIds?.includes(parseInt(i.metricId))), [metricStore.filteredCards]); useEffect(() => { metricStore.fetchList(); @@ -63,6 +66,7 @@ function MetricsList({ siteId={siteId} list={sliceListPerPage(cards, metricStore.page - 1, metricStore.pageSize)} selectedList={selectedMetrics} + existingCardIds={existingCardIds} toggleSelection={toggleMetricSelection} allSelected={cards.length === selectedMetrics.length} toggleAll={({ target: { checked, name } }) => From da6883cc671c227e94db67093ca4f915629def40 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 2 Feb 2023 17:00:18 +0100 Subject: [PATCH 3/4] change(ui) - search url improvements --- .../SessionSearchQueryParamHandler.tsx | 99 ++++++++++++------- frontend/app/duck/search.js | 1 + 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/frontend/app/components/shared/SessionSearchQueryParamHandler/SessionSearchQueryParamHandler.tsx b/frontend/app/components/shared/SessionSearchQueryParamHandler/SessionSearchQueryParamHandler.tsx index f2624b460..1b14057a8 100644 --- a/frontend/app/components/shared/SessionSearchQueryParamHandler/SessionSearchQueryParamHandler.tsx +++ b/frontend/app/components/shared/SessionSearchQueryParamHandler/SessionSearchQueryParamHandler.tsx @@ -4,9 +4,12 @@ import { connect } from 'react-redux'; import { addFilterByKeyAndValue, addFilter } from 'Duck/search'; import { getFilterKeyTypeByKey, setQueryParamKeyFromFilterkey } from 'Types/filter/filterType'; import { filtersMap } from 'App/types/filter/newFilter'; +import Filter from 'Types/filter/filter'; +import { applyFilter } from 'Duck/search'; interface Props { appliedFilter: any; + applyFilter: any; addFilterByKeyAndValue: typeof addFilterByKeyAndValue; addFilter: typeof addFilter; } @@ -15,8 +18,9 @@ const SessionSearchQueryParamHandler = React.memo((props: Props) => { const history = useHistory(); const createUrlQuery = (filters: any) => { - const query: any = {}; + const query: any = []; filters.forEach((filter: any) => { + const item: any = {}; if (filter.value.length > 0) { const _key = setQueryParamKeyFromFilterkey(filter.key); if (_key) { @@ -24,55 +28,77 @@ const SessionSearchQueryParamHandler = React.memo((props: Props) => { if (filter.hasSource) { str = `${str}^${filter.sourceOperator}|${filter.source.join('|')}`; } - query[_key] = str; + item.key = _key + '[]'; + item.value = str; } else { let str = `${filter.operator}|${filter.value.join('|')}`; - query[filter.key] = str; + item.key = [filter.key] + '[]'; + item.value = str; } + + query.push(item); } + }); return query; }; - const addFilter = ([key, value]: [any, any]): void => { - if (value !== '') { - const filterKey = getFilterKeyTypeByKey(key); - const tmp = value.split('^'); - const valueArr = tmp[0].split('|'); - const operator = valueArr.shift(); - - const sourceArr = tmp[1] ? tmp[1].split('|') : []; - const sourceOperator = sourceArr.shift(); - // TODO validate operator - if (filterKey) { - props.addFilterByKeyAndValue(filterKey, valueArr, operator, sourceOperator, sourceArr); - } else { - const _filters: any = { ...filtersMap }; - const _filter = _filters[key]; - if (!!_filter) { - _filter.value = valueArr; - _filter.operator = operator; - _filter.source = sourceArr; - props.addFilter(_filter); - } - } - } - }; - const applyFilterFromQuery = () => { if (appliedFilter.filters.size > 0) { return; } const entires = getQueryObject(history.location.search); + const _filters: any = { ...filtersMap }; if (entires.length > 0) { - entires.forEach(addFilter); + const filters: any = []; + entires.forEach((item: any) => { + if (!item.key || !item.value) { return } + const filter: any = {} + const filterKey = getFilterKeyTypeByKey(item.key); + const tmp = item.value.split('^'); + const valueArr = tmp[0].split('|'); + const operator = valueArr.shift(); + const sourceArr = tmp[1] ? tmp[1].split('|') : []; + const sourceOperator = decodeURI(sourceArr.shift()); + + + if (filterKey) { + filter.type = filterKey; + filter.key = filterKey; + filter.value = valueArr; + filter.operator = operator; + filter.source = sourceArr; + filter.sourceOperator = sourceOperator; + filters.push(filter); + } else { + const _filter = _filters[item.key]; + + if (!!_filter) { + _filter.type = _filter.key; + _filter.key = _filter.key; + _filter.value = valueArr; + _filter.operator = operator; + _filter.source = sourceArr; + filter.sourceOperator = sourceOperator; + } + } + }); + const f = Filter({ filters }) + props.applyFilter(f); } }; const generateUrlQuery = () => { const query: any = createUrlQuery(appliedFilter.filters); - // const queryString = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&'); - const queryString = new URLSearchParams(query).toString(); + + let queryString = query.reduce((acc: any, curr: any, index: any) => { + acc += `${curr.key}=${curr.value}`; + if (index < query.length - 1) { + acc += '&'; + } + return acc; + }, ''); + history.replace({ search: queryString }); }; @@ -85,12 +111,13 @@ export default connect( (state: any) => ({ appliedFilter: state.getIn(['search', 'instance']), }), - { addFilterByKeyAndValue, addFilter } + { addFilterByKeyAndValue, addFilter, applyFilter } )(SessionSearchQueryParamHandler); function getQueryObject(search: any) { - const queryParams = Object.fromEntries( - Object.entries(Object.fromEntries(new URLSearchParams(search))) - ); - return Object.entries(queryParams); + let jsonArray = search.slice(1).split('&').map((item: any) => { + let [key, value] = item.split('='); + return {key: key.slice(0, -2), value}; + }); + return jsonArray; } diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index 9d26e19fe..ce4306cbb 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -367,6 +367,7 @@ export const addFilterByKeyAndValue = defaultFilter.sourceOperator = sourceOperator; defaultFilter.source = source; } + dispatch(addFilter(defaultFilter)); }; From 55a496d40509bfd9906ee000258c113e1f9fadac Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Thu, 2 Feb 2023 17:04:38 +0100 Subject: [PATCH 4/4] change(ui) - removed unused key on post --- frontend/app/mstore/types/dashboard.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/app/mstore/types/dashboard.ts b/frontend/app/mstore/types/dashboard.ts index 19faffe7b..098f28ee9 100644 --- a/frontend/app/mstore/types/dashboard.ts +++ b/frontend/app/mstore/types/dashboard.ts @@ -35,7 +35,6 @@ export default class Dashboard { dashboardId: this.dashboardId, name: this.name, isPublic: this.isPublic, - createdAt: this.createdAt, metrics: this.metrics, description: this.description, }