diff --git a/frontend/app/components/Client/Modules/index.ts b/frontend/app/components/Client/Modules/index.ts index 9aa3173f9..291affc45 100644 --- a/frontend/app/components/Client/Modules/index.ts +++ b/frontend/app/components/Client/Modules/index.ts @@ -18,8 +18,8 @@ export interface Module { export const modules = [ { - label: 'Assist', - description: 'Record and replay user sessions to see a video of what users did on your website.', + label: 'Cobrowse', + description: 'Enable live session playing, interaction, screen sharing, and annotations over video call.', key: MODULES.ASSIST, icon: 'broadcast' }, diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.module.css b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.module.css new file mode 100644 index 000000000..fdbe4d1fe --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.module.css @@ -0,0 +1,37 @@ +.cohortTableContainer { + display: flex; + overflow-x: hidden; + width: 100%; +} + +.fixedTableWrapper { + flex-shrink: 0; +} + +.scrollableTableWrapper { + overflow-x: auto; + width: 100%; +} + +.cohortTable { + border-collapse: separate; + width: max-content; + border-spacing: 6px; +} + +.cell { + border: transparent; + border-radius: 3px; + padding: 4px 8px; + text-align: center; + min-width: 80px; + cursor: pointer; +} + +.header { + height: 29px; +} + +.header .bg { + background-color: #f2f2f2; +} \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx new file mode 100644 index 000000000..cdbbb295c --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/CohortCard.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import styles from './CohortCard.module.css'; + + +interface Props { + data: any +} +function CohortCard(props: Props) { + // const { data } = props; + const data = [ + { + cohort: '2022-01-01', + users: 100, + data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5], + }, + { + cohort: '2022-01-08', + users: 100, + data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15], + }, + { + cohort: '2022-01-08', + users: 100, + data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15], + }, + { + cohort: '2022-01-08', + users: 100, + data: [100, 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + { + cohort: '2022-01-08', + users: 100, + data: [90, 70, 50, 30], + }, + // ... more rows + ]; + + const getCellColor = (value: number) => { + const maxValue = 100; // Adjust this based on the maximum value in your data + const maxOpacity = 0.5; + const opacity = (value / maxValue) * maxOpacity; + return `rgba(62, 170, 175, ${opacity})`; + }; + + return ( +
+
+ + + + + + + + + + + + + {data.map((row, rowIndex) => ( + + + + + ))} + +
DateUsers
{row.cohort}{row.users}
+
+
+ + + + + + + {data[0].data.map((_, index) => ( + + ))} + + + + {data.map((row, rowIndex) => ( + + {row.data.map((cell, cellIndex) => ( + + ))} + + ))} + +
Weeks later users retained
{`${index + 1}`}
{cell}%
+
+
+ ); +} + +export default CohortCard; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts new file mode 100644 index 000000000..a570376dd --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/CohortCard/index.ts @@ -0,0 +1 @@ +export { default } from './CohortCard'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx b/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx new file mode 100644 index 000000000..94526d904 --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardIssues/CardIssueItem.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Icon } from 'UI'; +import Issue from 'App/mstore/types/issue'; + +interface Props { + issue: Issue; +} + +function CardIssueItem(props: Props) { + const { issue } = props; + return ( +
+
+
+ +
+
+ {issue.name} + {issue.source} +
+
+
{issue.sessionCount}
+
+ ); +} + +export default CardIssueItem; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx new file mode 100644 index 000000000..d24344456 --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardIssues/CardIssues.tsx @@ -0,0 +1,107 @@ +import React, { useEffect, useState } from 'react'; +import { useStore } from 'App/mstore'; +import { observer, useObserver } from 'mobx-react-lite'; +import { Loader, Pagination, Button, NoContent } from 'UI'; + +import { debounce } from 'App/utils'; +import useIsMounted from 'App/hooks/useIsMounted'; +import CardIssueItem from './CardIssueItem'; +import SessionsModal from '../SessionsModal'; +import { useModal } from 'App/components/Modal'; +import Issue from 'App/mstore/types/issue'; + +function CardIssues() { + const { metricStore, dashboardStore } = useStore(); + const [data, setData] = useState<{ + issues: Issue[]; + total: number; + }>({ issues: [], total: 0 }); + const [loading, setLoading] = useState(false); + const widget: any = useObserver(() => metricStore.instance); + const isMounted = useIsMounted(); + const { showModal } = useModal(); + + const fetchIssues = (filter: any) => { + if (!isMounted()) return; + setLoading(true); + + const newFilter = { + ...filter, + series: filter.series.map((item: any) => { + return { + ...item, + filter: { + ...item.filter, + filters: item.filter.filters.filter((filter: any, index: any) => { + const stage = widget.data.funnel.stages[index]; + return stage && stage.isActive; + }).map((f: any) => f.toJson()) + } + }; + }) + }; + widget.fetchIssues(newFilter).then((res: any) => { + setData(res); + }).finally(() => { + setLoading(false); + }); + }; + + const handleClick = (issue: any) => { + showModal(, { right: true, width: 900 }); + }; + + const filter = useObserver(() => dashboardStore.drillDownFilter); + const drillDownPeriod = useObserver(() => dashboardStore.drillDownPeriod); + const debounceRequest: any = React.useCallback(debounce(fetchIssues, 1000), []); + const depsString = JSON.stringify(widget.series); + + useEffect(() => { + const newPayload = { + ...widget, + page: metricStore.sessionsPage, + limit: metricStore.sessionsPageSize, + filters: filter.filters + }; + console.log('drillDownPeriod', newPayload); + debounceRequest(newPayload); + }, [drillDownPeriod, filter.filters, depsString, metricStore.sessionsPage]); + + return useObserver(() => ( +
+
+

Issues

+ {/*
*/} + {/* */} + {/*
*/} +
+ + + + {data.issues.map((item: any, index: any) => ( +
handleClick(item)} key={index}> + +
+ ))} +
+
+ +
+
+ Showing {Math.min(data.issues.length, metricStore.sessionsPageSize)} out of{' '} + {data.total} Issues +
+ metricStore.updateKey('sessionsPage', page)} + limit={metricStore.sessionsPageSize} + debounceRequest={500} + /> +
+
+ )); +} + +export default observer(CardIssues); diff --git a/frontend/app/components/Dashboard/components/CardIssues/index.ts b/frontend/app/components/Dashboard/components/CardIssues/index.ts new file mode 100644 index 000000000..9970a8e05 --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardIssues/index.ts @@ -0,0 +1 @@ +export { default } from './CardIssues' \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/CardUserList/CardUserItem.tsx b/frontend/app/components/Dashboard/components/CardUserList/CardUserItem.tsx new file mode 100644 index 000000000..8c195b081 --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardUserList/CardUserItem.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Icon } from 'UI'; +interface Props { + user: any +} +function CardUserItem(props: Props) { + const { user } = props; + return ( +
+
+
+
+ +
+
+
+ {user.name} + {/* some-button */} +
+
+
+
{user.sessions}
+
+
+
+ ); +} + +export default CardUserItem; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/CardUserList/CardUserList.tsx b/frontend/app/components/Dashboard/components/CardUserList/CardUserList.tsx new file mode 100644 index 000000000..60814047b --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardUserList/CardUserList.tsx @@ -0,0 +1,77 @@ +import { useModal } from 'App/components/Modal'; +import { observer } from 'mobx-react-lite'; +import React, { useEffect, useState } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; +import { Loader, Pagination, Button } from 'UI'; +import SessionsModal from './SessionsModal'; +import CardUserItem from './CardUserItem'; +import { useStore } from 'App/mstore'; + +interface Props { + history: any; + location: any; +} +function CardUserList(props: RouteComponentProps) { + const [loading, setLoading] = useState(false); + const { showModal } = useModal(); + const userId = new URLSearchParams(props.location.search).get("userId"); + const { metricStore, dashboardStore } = useStore(); + + const [data, setData] = useState([ + { name: 'user@domain.com', sessions: 29 }, + { name: 'user@domain.com', sessions: 29 }, + { name: 'user@domain.com', sessions: 29 }, + { name: 'user@domain.com', sessions: 29 }, + ]); + const pageSize = data.length; + + const handleClick = (issue: any) => { + props.history.replace({search: (new URLSearchParams({userId : '123'})).toString()}); + // showModal(, { right: true, width: 450 }) + } + + useEffect(() => { + if (!userId) return; + + showModal(, { right: true, width: 600, onClose: () => { + if (props.history.location.pathname.includes("/metric")) { + props.history.replace({search: ""}); + } + }}); + }, [userId]); + + return ( +
+
+

Returning users between

+
+ +
+
+ + + {data.map((item: any, index: any) => ( +
handleClick(item)}> + +
+ ))} +
+ +
+
+ Showing {Math.min(data.length, pageSize)} out of{' '} + {data.length} Issues +
+ metricStore.updateKey('sessionsPage', page)} + limit={metricStore.sessionsPageSize} + debounceRequest={500} + /> +
+
+ ); +} + +export default withRouter(observer(CardUserList)); \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/SessionsModal.tsx b/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/SessionsModal.tsx new file mode 100644 index 000000000..6e4381e3f --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/SessionsModal.tsx @@ -0,0 +1,99 @@ +import React, { useEffect } from 'react'; +import { useStore } from 'App/mstore'; +import { FilterKey } from 'App/types/filter/filterType'; +import { NoContent, Pagination, Loader, Avatar } from 'UI'; +import SessionItem from 'Shared/SessionItem'; +import SelectDateRange from 'Shared/SelectDateRange'; +import { useObserver, observer } from 'mobx-react-lite'; +import { useModal } from 'App/components/Modal'; +import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; + +const PER_PAGE = 10; +interface Props { + userId: string; + hash: string; + name: string; +} +function SessionsModal(props: Props) { + const { userId, hash, name } = props; + const { sessionStore } = useStore(); + const { hideModal } = useModal(); + const [loading, setLoading] = React.useState(false); + const [data, setData] = React.useState({ sessions: [], total: 0 }); + const filter = useObserver(() => sessionStore.userFilter); + + const onDateChange = (period: any) => { + filter.update('period', period); + }; + + const fetchData = () => { + setLoading(true); + sessionStore + .getSessions(filter) + .then(setData) + .catch(() => { + console.log('error'); + }) + .finally(() => { + setLoading(false); + }); + }; + + useEffect(() => { + const userFilter = { key: FilterKey.USERID, value: [userId], operator: 'is', isEvent: false }; + filter.update('filters', [userFilter]); + }, []); + useEffect(fetchData, [filter.page, filter.startDate, filter.endDate]); + + return ( +
+
+
+ +
+ {name}'s Sessions +
+
+
+ +
+
+ + + +
+
No recordings found.
+
+ }> +
+ + {data.sessions.map((session: any) => ( +
+ +
+ ))} +
+ +
+
+ {/* showing x to x of total sessions */} + Showing {(filter.page - 1) * PER_PAGE + 1} to{' '} + {(filter.page - 1) * PER_PAGE + data.sessions.length} of{' '} + {data.total} sessions. +
+ filter.update('page', page)} + limit={PER_PAGE} + debounceRequest={1000} + /> +
+
+
+
+ ); +} + +export default observer(SessionsModal); diff --git a/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/index.ts b/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/index.ts new file mode 100644 index 000000000..914a0b55b --- /dev/null +++ b/frontend/app/components/Dashboard/components/CardUserList/SessionsModal/index.ts @@ -0,0 +1 @@ +export { default } from './SessionsModal'; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx new file mode 100644 index 000000000..b19e4936b --- /dev/null +++ b/frontend/app/components/Dashboard/components/FilterSeries/ExcludeFilters.tsx @@ -0,0 +1,58 @@ +import filters from 'App/duck/filters'; +import Filter from 'App/mstore/types/filter'; +import { FilterKey } from 'App/types/filter/filterType'; +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import FilterItem from 'Shared/Filters/FilterItem'; +import cn from 'classnames'; + +import { Button } from 'UI'; + +interface Props { + filter: Filter; +} +function ExcludeFilters(props: Props) { + const { filter } = props; + const hasExcludes = filter.excludes.length > 0; + + const addPageFilter = () => { + const filterItem = filter.createFilterBykey(FilterKey.LOCATION); + filter.addExcludeFilter(filterItem); + }; + + const onUpdateFilter = (filterIndex: any, filterItem: any) => { + filter.updateExcludeFilter(filterIndex, filterItem); + }; + + const onRemoveFilter = (filterIndex: any) => { + filter.removeExcludeFilter(filterIndex); + }; + + return ( +
+ {filter.excludes.length > 0 ? ( +
+
EXCLUDES
+ {filter.excludes.map((f: any, index: number) => ( + onUpdateFilter(index, f)} + onRemoveFilter={() => onRemoveFilter(index)} + // saveRequestPayloads={saveRequestPayloads} + disableDelete={false} + // excludeFilterKeys={excludeFilterKeys} + /> + ))} +
+ ) : ( + + )} +
+ ); +} + +export default observer(ExcludeFilters); diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx index f1756e299..b2f1d08d7 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -5,6 +5,7 @@ import FilterSelection from 'Shared/Filters/FilterSelection'; import SeriesName from './SeriesName'; import cn from 'classnames'; import { observer } from 'mobx-react-lite'; +import ExcludeFilters from './ExcludeFilters'; interface Props { seriesIndex: number; @@ -15,52 +16,57 @@ interface Props { hideHeader?: boolean; emptyMessage?: any; observeChanges?: () => void; - excludeFilterKeys?: Array + excludeFilterKeys?: Array; + canExclude?: boolean; } function FilterSeries(props: Props) { const { - observeChanges = () => { - }, + observeChanges = () => {}, canDelete, hideHeader = false, emptyMessage = 'Add user event or filter to define the series by clicking Add Step.', supportsEmpty = true, - excludeFilterKeys = [] + excludeFilterKeys = [], + canExclude = false, } = props; - const [expanded, setExpanded] = useState(true) + const [expanded, setExpanded] = useState(true); const { series, seriesIndex } = props; const onAddFilter = (filter: any) => { - series.filter.addFilter(filter) - observeChanges() - } + series.filter.addFilter(filter); + observeChanges(); + }; const onUpdateFilter = (filterIndex: any, filter: any) => { - series.filter.updateFilter(filterIndex, filter) - observeChanges() - } + series.filter.updateFilter(filterIndex, filter); + observeChanges(); + }; const onChangeEventsOrder = (_: any, { name, value }: any) => { - series.filter.updateKey(name, value) - observeChanges() - } + series.filter.updateKey(name, value); + observeChanges(); + }; const onRemoveFilter = (filterIndex: any) => { - series.filter.removeFilter(filterIndex) - observeChanges() - } + series.filter.removeFilter(filterIndex); + observeChanges(); + }; - console.log(series.filter) return (
-
+ {canExclude && } +
- series.update('name', name)} /> + series.update('name', name)} + />
-
+
@@ -92,7 +98,9 @@ function FilterSeries(props: Props) { onFilterClick={onAddFilter} excludeFilterKeys={excludeFilterKeys} > - +
@@ -102,4 +110,4 @@ function FilterSeries(props: Props) { ); } -export default observer(FilterSeries); \ No newline at end of file +export default observer(FilterSeries); diff --git a/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx new file mode 100644 index 000000000..f6ddbb486 --- /dev/null +++ b/frontend/app/components/Dashboard/components/SessionsModal/SessionsModal.tsx @@ -0,0 +1,82 @@ +import React, { useEffect } from 'react'; +import { useStore } from 'App/mstore'; + +import { dashboardService, metricService } from 'App/services'; +import { Loader, Modal, NoContent, Pagination } from 'UI'; +import SessionItem from 'Shared/SessionItem'; +import Session from 'App/mstore/types/session'; +import { useModal } from 'Components/Modal'; + +interface Props { + list: any, + issue: any +} + +function SessionsModal(props: Props) { + const { issue } = props; + const { metricStore, dashboardStore } = useStore(); + const [loading, setLoading] = React.useState(false); + const [page, setPage] = React.useState(1); + const [total, setTotal] = React.useState(0); + const [list, setList] = React.useState([]); + const { hideModal } = useModal(); + + const length = list.length; + + const fetchSessions = async (filter: any) => { + setLoading(true); + filter.filters = [ + { + type: 'issue', + operator: 'is', + value: [issue.type] + } + ]; + const res = await metricService.fetchSessions(null, filter); + console.log('res', res); + setList(res[0].sessions.map((item: any) => new Session().fromJson(item))); + setTotal(res[0].total); + setLoading(false); + }; + + useEffect(() => { + fetchSessions({ ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page: page }); + }, [page]); + + useEffect(() => { + fetchSessions({ ...dashboardStore.drillDownFilter, ...metricStore.instance.toJson(), limit: 10, page: 1 }); + }, [props.issue]); + + + return ( +
+ + Sessions with selected issue + + + + {list.map((item: any) => ( + + ))} + + + +
+
+ Showing {Math.min(length, 10)} out of{' '} + {total} Issues +
+ setPage(page)} + limit={10} + debounceRequest={500} + /> +
+
+ ); +} + +export default SessionsModal; \ No newline at end of file diff --git a/frontend/app/components/Dashboard/components/SessionsModal/index.ts b/frontend/app/components/Dashboard/components/SessionsModal/index.ts new file mode 100644 index 000000000..914a0b55b --- /dev/null +++ b/frontend/app/components/Dashboard/components/SessionsModal/index.ts @@ -0,0 +1 @@ +export { default } from './SessionsModal'; \ 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 cf44102d4..f62adfed3 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -11,203 +11,245 @@ import WidgetPredefinedChart from '../WidgetPredefinedChart'; import CustomMetricOverviewChart from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/CustomMetricOverviewChart'; import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; -import useIsMounted from 'App/hooks/useIsMounted' +import useIsMounted from 'App/hooks/useIsMounted'; import { FilterKey } from 'Types/filter/filterType'; -import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS, INSIGHTS } from 'App/constants/card'; +import { + TIMESERIES, + TABLE, + CLICKMAP, + FUNNEL, + ERRORS, + PERFORMANCE, + RESOURCE_MONITORING, + WEB_VITALS, + INSIGHTS, + USER_PATH, + RETENTION +} 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 ClickMapCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard'; import InsightsCard from 'App/components/Dashboard/Widgets/CustomMetricsWidgets/InsightsCard'; +import SankeyChart from 'Shared/Insights/SankeyChart'; +import CohortCard from '../../Widgets/CustomMetricsWidgets/CohortCard'; interface Props { - metric: any; - isWidget?: boolean; - isTemplate?: boolean; - isPreview?: boolean; + metric: any; + isWidget?: boolean; + isTemplate?: boolean; + isPreview?: boolean; } function WidgetChart(props: Props) { - const { isWidget = false, metric, isTemplate } = props; - const { dashboardStore, metricStore, sessionStore } = useStore(); - const _metric: any = metricStore.instance; - const period = dashboardStore.period; - const drillDownPeriod = dashboardStore.drillDownPeriod; - const drillDownFilter = dashboardStore.drillDownFilter; - const colors = Styles.customMetricColors; - const [loading, setLoading] = useState(true) - const isOverviewWidget = metric.metricType === WEB_VITALS; - const params = { density: isOverviewWidget ? 7 : 70 } - const metricParams = { ...params } - const prevMetricRef = useRef(); - const isMounted = useIsMounted(); - const [data, setData] = useState(metric.data); + const { isWidget = false, metric, isTemplate } = props; + const { dashboardStore, metricStore, sessionStore } = useStore(); + const _metric: any = metricStore.instance; + const period = dashboardStore.period; + const drillDownPeriod = dashboardStore.drillDownPeriod; + const drillDownFilter = dashboardStore.drillDownFilter; + const colors = Styles.customMetricColors; + const [loading, setLoading] = useState(true); + const isOverviewWidget = metric.metricType === WEB_VITALS; + const params = { density: isOverviewWidget ? 7 : 70 }; + const metricParams = { ...params }; + const prevMetricRef = useRef(); + const isMounted = useIsMounted(); + const [data, setData] = useState(metric.data); - const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table'; - const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart'; + const isTableWidget = metric.metricType === 'table' && metric.viewType === 'table'; + const isPieChart = metric.metricType === 'table' && metric.viewType === 'pieChart'; - useEffect(() => { - return () => { - dashboardStore.resetDrillDownFilter(); - } - }, []) + useEffect(() => { + return () => { + dashboardStore.resetDrillDownFilter(); + }; + }, []); - const onChartClick = (event: any) => { - if (event) { - if (isTableWidget || isPieChart) { // get the filter of clicked row - const periodTimestamps = drillDownPeriod.toTimestamps() - drillDownFilter.merge({ - filters: event, - startTimestamp: periodTimestamps.startTimestamp, - endTimestamp: periodTimestamps.endTimestamp, - }); - } else { // get the filter of clicked chart point - const payload = event.activePayload[0].payload; - const timestamp = payload.timestamp; - const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density); - - drillDownFilter.merge({ - startTimestamp: periodTimestamps.startTimestamp, - endTimestamp: periodTimestamps.endTimestamp, - }); - } - } - } - - const depsString = JSON.stringify(_metric.series); - const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => { - if (!isMounted()) return; - setLoading(true) - dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => { - if (isMounted()) setData(res); - }).finally(() => { - setLoading(false); + const onChartClick = (event: any) => { + if (event) { + if (isTableWidget || isPieChart) { // get the filter of clicked row + const periodTimestamps = drillDownPeriod.toTimestamps(); + drillDownFilter.merge({ + filters: event, + startTimestamp: periodTimestamps.startTimestamp, + endTimestamp: periodTimestamps.endTimestamp }); + } else { // get the filter of clicked chart point + const payload = event.activePayload[0].payload; + const timestamp = payload.timestamp; + const periodTimestamps = getStartAndEndTimestampsByDensity(timestamp, drillDownPeriod.start, drillDownPeriod.end, params.density); + + drillDownFilter.merge({ + startTimestamp: periodTimestamps.startTimestamp, + endTimestamp: periodTimestamps.endTimestamp + }); + } + } + }; + + const depsString = JSON.stringify({ + ..._metric.series, ..._metric.excludes, ..._metric.startPoint, + hideExcess: _metric.hideExcess + }); + const fetchMetricChartData = (metric: any, payload: any, isWidget: any, period: any) => { + if (!isMounted()) return; + setLoading(true); + dashboardStore.fetchMetricChartData(metric, payload, isWidget, period).then((res: any) => { + if (isMounted()) setData(res); + }).finally(() => { + setLoading(false); + }); + }; + + const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []); + const loadPage = () => { + if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) { + prevMetricRef.current = metric; + return; + } + prevMetricRef.current = metric; + const timestmaps = drillDownPeriod.toTimestamps(); + const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() }; + debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period); + }; + useEffect(() => { + _metric.updateKey('page', 1); + loadPage(); + }, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue, metric.startType]); + useEffect(loadPage, [_metric.page]); + + + const renderChart = () => { + const { metricType, viewType, metricOf } = metric; + const metricWithData = { ...metric, data }; + + if (metricType === FUNNEL) { + return ; } - const debounceRequest: any = React.useCallback(debounce(fetchMetricChartData, 500), []); - const loadPage = () => { - if (prevMetricRef.current && prevMetricRef.current.name !== metric.name) { - prevMetricRef.current = metric; - return - } - prevMetricRef.current = metric; - const timestmaps = drillDownPeriod.toTimestamps(); - const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() }; - debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period); + if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) { + const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric; + if (isOverviewWidget) { + return ; + } + return ; } - useEffect(() => { - _metric.updateKey('page', 1) - loadPage(); - }, [drillDownPeriod, period, depsString, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue]); - useEffect(loadPage, [_metric.page]); - - const renderChart = () => { - const { metricType, viewType, metricOf } = metric; - const metricWithData = { ...metric, data }; - - if (metricType === FUNNEL) { - return - } - - if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) { - const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric - if (isOverviewWidget) { - return - } - return - } - - // TODO add USER_PATH, RETENTION, FEATUER_ADOPTION - - if (metricType === TIMESERIES) { - if (viewType === 'lineChart') { - return ( - - ) - } else if (viewType === 'progress') { - return ( - - ) - } - } - - if (metricType === TABLE) { - if (metricOf === FilterKey.SESSIONS) { - return ( - - ) - } - if (metricOf === FilterKey.ERRORS) { - return ( - - ) - } - if (viewType === TABLE) { - return ( - - ) - } else if (viewType === 'pieChart') { - return ( - - ) - } - } - if (metricType === CLICKMAP) { - if (!props.isPreview) { - return ( -
- clickmap thumbnail -
- ) - } - return ( - - ) - } - - if (metricType === INSIGHTS) { - return - } - - return
Unknown metric type
; + if (metricType === TIMESERIES) { + if (viewType === 'lineChart') { + return ( + + ); + } else if (viewType === 'progress') { + return ( + + ); + } } - return ( - -
{renderChart()}
-
- ); + + if (metricType === TABLE) { + if (metricOf === FilterKey.SESSIONS) { + return ( + + ); + } + if (metricOf === FilterKey.ERRORS) { + return ( + + ); + } + if (viewType === TABLE) { + return ( + + ); + } else if (viewType === 'pieChart') { + return ( + + ); + } + } + if (metricType === CLICKMAP) { + if (!props.isPreview) { + return ( +
+ clickmap thumbnail +
+ ); + } + return ( + + ); + } + + if (metricType === INSIGHTS) { + return ; + } + + if (metricType === USER_PATH && data && data.links) { + return { + dashboardStore.drillDownFilter.merge({ filters }); + }} />; + } + + if (metricType === RETENTION) { + if (viewType === 'trend') { + return ( + + ); + } else if (viewType === 'cohort') { + return ( + + ); + } + } + + return
Unknown metric type
; + }; + return ( + +
{renderChart()}
+
+ ); } export default observer(WidgetChart); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 59a1b619a..adc08635e 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -19,10 +19,14 @@ import { PERFORMANCE, WEB_VITALS, INSIGHTS, + USER_PATH, + RETENTION } from 'App/constants/card'; -import { eventKeys } from 'App/types/filter/newFilter'; +import { eventKeys, filtersMap } from 'App/types/filter/newFilter'; import { renderClickmapThumbnail } from './renderMap'; import Widget from 'App/mstore/types/widget'; +import FilterItem from 'Shared/Filters/FilterItem'; + interface Props { history: any; match: any; @@ -33,8 +37,8 @@ function WidgetForm(props: Props) { const { history, match: { - params: { siteId, dashboardId }, - }, + params: { siteId, dashboardId } + } } = props; const { metricStore, dashboardStore } = useStore(); const isSaving = metricStore.isSaving; @@ -47,6 +51,8 @@ function WidgetForm(props: Props) { const isClickmap = metric.metricType === CLICKMAP; const isFunnel = metric.metricType === FUNNEL; const isInsights = metric.metricType === INSIGHTS; + const isPathAnalysis = metric.metricType === USER_PATH; + const isRetention = metric.metricType === RETENTION; 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); @@ -55,7 +61,7 @@ function WidgetForm(props: Props) { metric.metricType ); - const excludeFilterKeys = isClickmap ? eventKeys : []; + const excludeFilterKeys = isClickmap || isPathAnalysis ? eventKeys : []; useEffect(() => { if (!!metric && !initialInstance) { @@ -91,7 +97,7 @@ function WidgetForm(props: Props) { } } const savedMetric = await metricStore.save(metric); - setInitialInstance(metric.toJson()) + setInitialInstance(metric.toJson()); if (wasCreating) { if (parseInt(dashboardId, 10) > 0) { history.replace( @@ -112,50 +118,83 @@ function WidgetForm(props: Props) { await confirm({ header: 'Confirm', confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this card?`, + confirmation: `Are you sure you want to permanently delete this card?` }) ) { metricStore.delete(metric).then(props.onDelete); } }; - const undoChnages = () => { + const undoChanges = () => { const w = new Widget(); metricStore.merge(w.fromJson(initialInstance), false); }; return ( -
-
-
- Card showing +
+
+
+ Card showing + {isPathAnalysis && ( + <> + + + + )} + {metric.metricOf === FilterKey.ISSUE && metric.metricType === TABLE && ( <> - issue type + issue type )} @@ -163,9 +202,9 @@ function WidgetForm(props: Props) { {metric.metricType === 'table' && !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( <> - showing + showing + Jack + Lucy + + ); +} + +export default NodeDropdown; diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx index 511ca12e8..bf7bd9025 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx @@ -1,134 +1,100 @@ -import React from 'react'; -import { Sankey, Tooltip, Rectangle, Layer, ResponsiveContainer } from 'recharts'; +import React, { useEffect } from 'react'; +import { Sankey, ResponsiveContainer } from 'recharts'; +import CustomLink from './CustomLink'; +import CustomNode from './CustomNode'; +import { NoContent } from 'UI'; -type Node = { +interface Node { name: string; + eventType: string; + avgTimeFromPrevious: number | null; } -type Link = { +interface Link { + eventType: string; + value: number; source: number; target: number; - value: number; } -export interface SankeyChartData { - links: Link[]; +interface Data { nodes: Node[]; + links: Link[]; } + interface Props { - data: SankeyChartData; + data: Data; nodePadding?: number; nodeWidth?: number; + onChartClick?: (data: any) => void; + height?: number; } + + function SankeyChart(props: Props) { - const { data, nodePadding = 50, nodeWidth = 10 } = props; + const { data, nodeWidth = 10, height = 240 } = props; + const [activeLink, setActiveLink] = React.useState(null); + + data.nodes = data.nodes.map((node: any) => { + return { + ...node, + avgTimeFromPrevious: 200 + }; + }); + + useEffect(() => { + if (!activeLink) return; + const { source, target } = activeLink.payload; + const filters = []; + if (source) { + filters.push({ + operator: 'is', + type: source.eventType, + value: [source.name], + isEvent: true + }); + } + + if (target) { + filters.push({ + operator: 'is', + type: target.eventType, + value: [target.name], + isEvent: true + }); + } + + props.onChartClick?.(filters); + }, [activeLink]); + return ( -
-
Sankey Chart
-
- - } - nodePadding={nodePadding} - nodeWidth={nodeWidth} - margin={{ - left: 10, - right: 100, - top: 10, - bottom: 10, - }} - link={} - > - - - - - - - } /> - - -
-
+ + + } + nodeWidth={nodeWidth} + sort={false} + // linkCurvature={0.5} + // iterations={128} + margin={{ + left: 0, + right: 200, + top: 0, + bottom: 10 + }} + link={ setActiveLink(props)} activeLink={activeLink} />} + > + + + + + + + + + ); } export default SankeyChart; - -const CustomTooltip = (props: any) => { - return
test
; - // if (active && payload && payload.length) { - // return ( - //
- //

{`${label} : ${payload[0].value}`}

- //

{getIntroOfPage(label)}

- //

Anything you want can be displayed here.

- //
- // ); - // } - - return null; -}; - -function CustomNodeComponent({ x, y, width, height, index, payload, containerWidth }: any) { - const isOut = x + width + 6 > containerWidth; - return ( - - - - {payload.name} - - - {payload.value + 'k'} - - - ); -} - -const CustomLinkComponent = (props: any) => { - const [fill, setFill] = React.useState('url(#linkGradient)'); - const { sourceX, targetX, sourceY, targetY, sourceControlX, targetControlX, linkWidth, index } = - props; - return ( - - { - setFill('rgba(0, 136, 254, 0.5)'); - }} - onMouseLeave={() => { - setFill('url(#linkGradient)'); - }} - /> - - ); -}; diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx index 5385e14df..8650c3ada 100644 --- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx +++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx @@ -88,7 +88,7 @@ function LiveSessionList(props: Props) {

- Live Sessions + Cobrowse {/* {numberWithCommas(total)} */}

@@ -126,7 +126,7 @@ function LiveSessionList(props: Props) { subtext={
- Support users with live sessions, co-browsing, and video calls. + Support users with live sessions, cobrowsing, and video calls. { - options: Value[]; - isSearchable?: boolean; - defaultValue?: string | number; - plain?: boolean; - components?: any; - styles?: Record; - controlStyle?: Record; - onChange: (newValue: { name: string, value: Value }) => void; - name?: string; - placeholder?: string; - [x:string]: any; + options: Value[]; + isSearchable?: boolean; + defaultValue?: string | number; + plain?: boolean; + components?: any; + styles?: Record; + controlStyle?: Record; + onChange: (newValue: { name: string, value: Value }) => void; + name?: string; + placeholder?: string; + + [x: string]: any; } -export default function({ placeholder='Select', name = '', onChange, right = false, plain = false, options, isSearchable = false, components = {}, styles = {}, defaultValue = '', controlStyle = {}, ...rest }: Props) { - const defaultSelected = defaultValue ? (options.find(o => o.value === defaultValue) || options[0]): null; - const customStyles = { - option: (provided: any, state: any) => ({ - ...provided, - whiteSpace: 'nowrap', - transition: 'all 0.3s', - backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent', - color: state.isFocused ? colors.teal : 'black', - fontSize: '14px', - '&:hover': { - transition: 'all 0.2s', - backgroundColor: colors['active-blue'], - }, - '&:focus': { - transition: 'all 0.2s', - backgroundColor: colors['active-blue'], - } - }), - menu: (provided: any, state: any) => ({ - ...provided, - top: 31, - borderRadius: '3px', - right: right ? 0 : undefined, - border: `1px solid ${colors['gray-light']}`, - // borderRadius: '3px', - backgroundColor: '#fff', - boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)', - position: 'absolute', - minWidth: 'fit-content', - // zIndex: 99, - overflow: 'hidden', - zIndex: 100, - ...(right && { right: 0 }) - }), - menuList: (provided: any, state: any) => ({ - ...provided, - padding: 0, - }), - control: (provided: any) => { - const obj = { - ...provided, - border: 'solid thin #ddd', - cursor: 'pointer', - minHeight: '36px', - transition: 'all 0.5s', - ['&:hover']: { - backgroundColor: colors['gray-lightest'], - transition: 'all 0.2s ease-in-out' - }, - ...controlStyle, - } - if (plain) { - obj['backgroundColor'] = 'transparent'; - obj['border'] = '1px solid transparent' - obj['backgroundColor'] = 'transparent' - obj['&:hover'] = { - borderColor: 'transparent', - backgroundColor: colors['gray-light'], - transition: 'all 0.2s ease-in-out' - } - obj['&:focus'] = { - borderColor: 'transparent' - } - obj['&:active'] = { - borderColor: 'transparent' - } - } - return obj; +export default function ({ + placeholder = 'Select', + name = '', + onChange, + right = false, + plain = false, + options, + isSearchable = false, + components = {}, + styles = {}, + defaultValue = '', + controlStyle = {}, + ...rest + }: Props) { + + const defaultSelected = Array.isArray(defaultValue) ? + defaultValue.map((value) => options.find((option) => option.value === value)) : + options.find((option) => option.value === defaultValue + ) || null; + if (Array.isArray(defaultSelected) && defaultSelected.length === 0) { + console.log('defaultSelected', defaultSelected); + } + const customStyles = { + option: (provided: any, state: any) => ({ + ...provided, + whiteSpace: 'nowrap', + transition: 'all 0.3s', + backgroundColor: state.isFocused ? colors['active-blue'] : 'transparent', + color: state.isFocused ? colors.teal : 'black', + fontSize: '14px', + '&:hover': { + transition: 'all 0.2s', + backgroundColor: colors['active-blue'] + }, + '&:focus': { + transition: 'all 0.2s', + backgroundColor: colors['active-blue'] + } + }), + menu: (provided: any, state: any) => ({ + ...provided, + top: 31, + borderRadius: '3px', + right: right ? 0 : undefined, + border: `1px solid ${colors['gray-light']}`, + // borderRadius: '3px', + backgroundColor: '#fff', + boxShadow: '1px 1px 1px rgba(0, 0, 0, 0.1)', + position: 'absolute', + minWidth: 'fit-content', + // zIndex: 99, + overflow: 'hidden', + zIndex: 100, + ...(right && { right: 0 }) + }), + menuList: (provided: any, state: any) => ({ + ...provided, + padding: 0 + }), + control: (provided: any) => { + const obj = { + ...provided, + border: 'solid thin #ddd', + cursor: 'pointer', + minHeight: '36px', + transition: 'all 0.5s', + ['&:hover']: { + backgroundColor: colors['gray-lightest'], + transition: 'all 0.2s ease-in-out' }, - indicatorsContainer: (provided: any) => ({ - ...provided, - maxHeight: '34px', - padding: 0, - }), - valueContainer: (provided: any) => ({ - ...provided, - paddingRight: '0px', - }), - singleValue: (provided: any, state: { isDisabled: any; }) => { - const opacity = state.isDisabled ? 0.5 : 1; - const transition = 'opacity 300ms'; + ...controlStyle + }; + if (plain) { + obj['backgroundColor'] = 'transparent'; + obj['border'] = '1px solid transparent'; + obj['backgroundColor'] = 'transparent'; + obj['&:hover'] = { + borderColor: 'transparent', + backgroundColor: colors['gray-light'], + transition: 'all 0.2s ease-in-out' + }; + obj['&:focus'] = { + borderColor: 'transparent' + }; + obj['&:active'] = { + borderColor: 'transparent' + }; + } + return obj; + }, + indicatorsContainer: (provided: any) => ({ + ...provided, + maxHeight: '34px', + padding: 0 + }), + valueContainer: (provided: any) => ({ + ...provided, + paddingRight: '0px' + }), + singleValue: (provided: any, state: { isDisabled: any; }) => { + const opacity = state.isDisabled ? 0.5 : 1; + const transition = 'opacity 300ms'; - return { ...provided, opacity, transition, fontWeight: '500' }; - }, - input: (provided: any) => ({ - ...provided, - '& input:focus': { - border: 'none !important', - } - }), - noOptionsMessage: (provided: any) => ({ - ...provided, - whiteSpace: 'nowrap !important', - // minWidth: 'fit-content', - }), - } + return { ...provided, opacity, transition, fontWeight: '500' }; + }, + input: (provided: any) => ({ + ...provided, + '& input:focus': { + border: 'none !important' + } + }), + noOptionsMessage: (provided: any) => ({ + ...provided, + whiteSpace: 'nowrap !important' + // minWidth: 'fit-content', + }) + }; - return ( - null, + DropdownIndicator, + ValueContainer: CustomValueContainer, + ...components + }} + onChange={(value) => onChange({ name, value: value })} + styles={{ ...customStyles, ...styles }} + theme={(theme) => ({ + ...theme, + colors: { + ...theme.colors, + primary: '#394EFF' + } + })} + blurInputOnSelect={true} + placeholder={placeholder} + {...rest} + /> + ); } const DropdownIndicator = ( - props: DropdownIndicatorProps - ) => { - return ( - - - - ); - }; + props: DropdownIndicatorProps +) => { + return ( + + + + ); +}; const CustomValueContainer = ({ children, ...rest }: any) => { - const selectedCount = rest.getValue().length - const conditional = (selectedCount < 3) + const selectedCount = rest.getValue().length; + const conditional = (selectedCount < 3); - let firstChild: any = [] + let firstChild: any = []; - if (!conditional) { - firstChild = [children[0].shift(), children[1]] - } - - return ( - - {conditional ? children : firstChild} - {!conditional && ` and ${selectedCount - 1} others`} - - ) + if (!conditional) { + firstChild = [children[0].shift(), children[1]]; } + + return ( + + {conditional ? children : firstChild} + {!conditional && ` and ${selectedCount - 1} others`} + + ); +}; diff --git a/frontend/app/components/ui/SVG.tsx b/frontend/app/components/ui/SVG.tsx index 1bf9d0dec..c3e3f4caf 100644 --- a/frontend/app/components/ui/SVG.tsx +++ b/frontend/app/components/ui/SVG.tsx @@ -1,7 +1,7 @@ import React from 'react'; -export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down-up' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_avatar1' | 'avatar/icn_avatar10' | 'avatar/icn_avatar11' | 'avatar/icn_avatar12' | 'avatar/icn_avatar13' | 'avatar/icn_avatar14' | 'avatar/icn_avatar15' | 'avatar/icn_avatar16' | 'avatar/icn_avatar17' | 'avatar/icn_avatar18' | 'avatar/icn_avatar19' | 'avatar/icn_avatar2' | 'avatar/icn_avatar20' | 'avatar/icn_avatar21' | 'avatar/icn_avatar22' | 'avatar/icn_avatar23' | 'avatar/icn_avatar3' | 'avatar/icn_avatar4' | 'avatar/icn_avatar5' | 'avatar/icn_avatar6' | 'avatar/icn_avatar7' | 'avatar/icn_avatar8' | 'avatar/icn_avatar9' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'bookmark' | 'broadcast' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-list' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-left-text' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection-play' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'db-icons/icn-card-clickMap' | 'db-icons/icn-card-errors' | 'db-icons/icn-card-funnel' | 'db-icons/icn-card-funnels' | 'db-icons/icn-card-insights' | 'db-icons/icn-card-library' | 'db-icons/icn-card-mapchart' | 'db-icons/icn-card-performance' | 'db-icons/icn-card-resources' | 'db-icons/icn-card-table' | 'db-icons/icn-card-timeseries' | 'db-icons/icn-card-userPath' | 'db-icons/icn-card-webVitals' | 'desktop' | 'device' | 'diagram-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'exclamation-triangle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'fflag-multi' | 'fflag-single' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filetype-js' | 'filetype-pdf' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'integrations/zustand' | 'journal-code' | 'key' | 'layer-group' | 'layers-half' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'record2' | 'redo-back' | 'redo' | 'redux' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'side_menu_closed' | 'side_menu_open' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'terminal' | 'text-paragraph' | 'toggles' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; +export type IconNames = 'activity' | 'alarm-clock' | 'alarm-plus' | 'all-sessions' | 'analytics' | 'anchor' | 'arrow-alt-square-right' | 'arrow-bar-left' | 'arrow-clockwise' | 'arrow-counterclockwise' | 'arrow-down-short' | 'arrow-down-up' | 'arrow-down' | 'arrow-repeat' | 'arrow-right-short' | 'arrow-square-left' | 'arrow-square-right' | 'arrow-up-short' | 'arrow-up' | 'arrows-angle-extend' | 'avatar/icn_avatar1' | 'avatar/icn_avatar10' | 'avatar/icn_avatar11' | 'avatar/icn_avatar12' | 'avatar/icn_avatar13' | 'avatar/icn_avatar14' | 'avatar/icn_avatar15' | 'avatar/icn_avatar16' | 'avatar/icn_avatar17' | 'avatar/icn_avatar18' | 'avatar/icn_avatar19' | 'avatar/icn_avatar2' | 'avatar/icn_avatar20' | 'avatar/icn_avatar21' | 'avatar/icn_avatar22' | 'avatar/icn_avatar23' | 'avatar/icn_avatar3' | 'avatar/icn_avatar4' | 'avatar/icn_avatar5' | 'avatar/icn_avatar6' | 'avatar/icn_avatar7' | 'avatar/icn_avatar8' | 'avatar/icn_avatar9' | 'ban' | 'bar-chart-line' | 'bar-pencil' | 'bell-fill' | 'bell-plus' | 'bell-slash' | 'bell' | 'binoculars' | 'book-doc' | 'book' | 'bookmark' | 'broadcast' | 'browser/browser' | 'browser/chrome' | 'browser/edge' | 'browser/electron' | 'browser/facebook' | 'browser/firefox' | 'browser/ie' | 'browser/opera' | 'browser/safari' | 'buildings' | 'bullhorn' | 'business-time' | 'calendar-alt' | 'calendar-check' | 'calendar-day' | 'calendar' | 'call' | 'camera-alt' | 'camera-video-off' | 'camera-video' | 'camera' | 'card-checklist' | 'card-list' | 'card-text' | 'caret-down-fill' | 'caret-left-fill' | 'caret-right-fill' | 'caret-up-fill' | 'chat-dots' | 'chat-left-text' | 'chat-right-text' | 'chat-square-quote' | 'check-circle-fill' | 'check-circle' | 'check' | 'chevron-double-left' | 'chevron-double-right' | 'chevron-down' | 'chevron-left' | 'chevron-right' | 'chevron-up' | 'circle-fill' | 'circle' | 'click-hesitation' | 'click-rage' | 'clipboard-list-check' | 'clock-history' | 'clock' | 'close' | 'cloud-fog2-fill' | 'code' | 'cog' | 'cogs' | 'collection-play' | 'collection' | 'columns-gap-filled' | 'columns-gap' | 'console/error' | 'console/exception' | 'console/info' | 'console/warning' | 'console' | 'controller' | 'cookies' | 'copy' | 'credit-card-front' | 'cross' | 'cubes' | 'cursor-trash' | 'dash' | 'dashboard-icn' | 'db-icons/icn-card-clickMap' | 'db-icons/icn-card-errors' | 'db-icons/icn-card-funnel' | 'db-icons/icn-card-funnels' | 'db-icons/icn-card-insights' | 'db-icons/icn-card-library' | 'db-icons/icn-card-mapchart' | 'db-icons/icn-card-pathAnalysis' | 'db-icons/icn-card-performance' | 'db-icons/icn-card-resources' | 'db-icons/icn-card-table' | 'db-icons/icn-card-timeseries' | 'db-icons/icn-card-webVitals' | 'desktop' | 'device' | 'diagram-3' | 'dice-3' | 'dizzy' | 'door-closed' | 'doublecheck' | 'download' | 'drag' | 'edit' | 'ellipsis-v' | 'enter' | 'envelope-check' | 'envelope-x' | 'envelope' | 'errors-icon' | 'event/click' | 'event/click_hesitation' | 'event/clickrage' | 'event/code' | 'event/i-cursor' | 'event/input' | 'event/input_hesitation' | 'event/link' | 'event/location' | 'event/mouse_thrashing' | 'event/resize' | 'event/view' | 'exclamation-circle-fill' | 'exclamation-circle' | 'exclamation-triangle' | 'expand-wide' | 'explosion' | 'external-link-alt' | 'eye-slash-fill' | 'eye-slash' | 'eye' | 'fetch' | 'fflag-multi' | 'fflag-single' | 'file-code' | 'file-medical-alt' | 'file-pdf' | 'file' | 'files' | 'filetype-js' | 'filetype-pdf' | 'filter' | 'filters/arrow-return-right' | 'filters/browser' | 'filters/click' | 'filters/clickrage' | 'filters/code' | 'filters/console' | 'filters/country' | 'filters/cpu-load' | 'filters/custom' | 'filters/device' | 'filters/dom-complete' | 'filters/duration' | 'filters/error' | 'filters/fetch-failed' | 'filters/fetch' | 'filters/file-code' | 'filters/graphql' | 'filters/i-cursor' | 'filters/input' | 'filters/lcpt' | 'filters/link' | 'filters/location' | 'filters/memory-load' | 'filters/metadata' | 'filters/os' | 'filters/perfromance-network-request' | 'filters/platform' | 'filters/referrer' | 'filters/resize' | 'filters/rev-id' | 'filters/state-action' | 'filters/ttfb' | 'filters/user-alt' | 'filters/userid' | 'filters/view' | 'flag-na' | 'folder-plus' | 'folder2' | 'fullscreen' | 'funnel/cpu-fill' | 'funnel/cpu' | 'funnel/dizzy' | 'funnel/emoji-angry-fill' | 'funnel/emoji-angry' | 'funnel/emoji-dizzy-fill' | 'funnel/exclamation-circle-fill' | 'funnel/exclamation-circle' | 'funnel/file-earmark-break-fill' | 'funnel/file-earmark-break' | 'funnel/file-earmark-minus-fill' | 'funnel/file-earmark-minus' | 'funnel/file-medical-alt' | 'funnel/file-x' | 'funnel/hdd-fill' | 'funnel/hourglass-top' | 'funnel/image-fill' | 'funnel/image' | 'funnel/microchip' | 'funnel/mouse' | 'funnel/patch-exclamation-fill' | 'funnel/sd-card' | 'funnel-fill' | 'funnel-new' | 'funnel' | 'gear-fill' | 'gear' | 'geo-alt-fill-custom' | 'github' | 'graph-up-arrow' | 'graph-up' | 'grid-1x2' | 'grid-3x3' | 'grid-check' | 'grid-horizontal' | 'grid' | 'grip-horizontal' | 'hash' | 'hdd-stack' | 'headset' | 'heart-rate' | 'high-engagement' | 'history' | 'hourglass-start' | 'ic-errors' | 'ic-network' | 'ic-rage' | 'ic-resources' | 'id-card' | 'image' | 'info-circle-fill' | 'info-circle' | 'info-square' | 'info' | 'input-hesitation' | 'inspect' | 'integrations/assist' | 'integrations/bugsnag-text' | 'integrations/bugsnag' | 'integrations/cloudwatch-text' | 'integrations/cloudwatch' | 'integrations/datadog' | 'integrations/elasticsearch-text' | 'integrations/elasticsearch' | 'integrations/github' | 'integrations/graphql' | 'integrations/jira-text' | 'integrations/jira' | 'integrations/mobx' | 'integrations/newrelic-text' | 'integrations/newrelic' | 'integrations/ngrx' | 'integrations/openreplay-text' | 'integrations/openreplay' | 'integrations/redux' | 'integrations/rollbar-text' | 'integrations/rollbar' | 'integrations/segment' | 'integrations/sentry-text' | 'integrations/sentry' | 'integrations/slack-bw' | 'integrations/slack' | 'integrations/stackdriver' | 'integrations/sumologic-text' | 'integrations/sumologic' | 'integrations/teams-white' | 'integrations/teams' | 'integrations/vuejs' | 'integrations/zustand' | 'journal-code' | 'key' | 'layer-group' | 'layers-half' | 'lightbulb-on' | 'lightbulb' | 'link-45deg' | 'list-alt' | 'list-arrow' | 'list-ul' | 'list' | 'lock-alt' | 'magic' | 'map-marker-alt' | 'memory' | 'mic-mute' | 'mic' | 'minus' | 'mobile' | 'mouse-alt' | 'network' | 'next1' | 'no-dashboard' | 'no-metrics-chart' | 'no-metrics' | 'no-recordings' | 'os/android' | 'os/chrome_os' | 'os/fedora' | 'os/ios' | 'os/linux' | 'os/mac_os_x' | 'os/other' | 'os/ubuntu' | 'os/windows' | 'os' | 'pause-fill' | 'pause' | 'pdf-download' | 'pencil-stop' | 'pencil' | 'people' | 'percent' | 'performance-icon' | 'person-border' | 'person-fill' | 'person' | 'pie-chart-fill' | 'pin-fill' | 'play-circle-bold' | 'play-circle-light' | 'play-circle' | 'play-fill-new' | 'play-fill' | 'play-hover' | 'play' | 'plug' | 'plus-circle' | 'plus-lg' | 'plus' | 'pointer-sessions-search' | 'prev1' | 'pulse' | 'puzzle-piece' | 'puzzle' | 'question-circle' | 'question-lg' | 'quote-left' | 'quote-right' | 'quotes' | 'record-btn' | 'record-circle' | 'record2' | 'redo-back' | 'redo' | 'redux' | 'remote-control' | 'replay-10' | 'resources-icon' | 'safe-fill' | 'safe' | 'sandglass' | 'search' | 'search_notification' | 'server' | 'share-alt' | 'shield-lock' | 'side_menu_closed' | 'side_menu_open' | 'signpost-split' | 'signup' | 'skip-forward-fill' | 'skip-forward' | 'slack' | 'slash-circle' | 'sleep' | 'sliders' | 'social/slack' | 'social/trello' | 'speedometer2' | 'spinner' | 'star-solid' | 'star' | 'step-forward' | 'stickies' | 'stop-record-circle' | 'stopwatch' | 'store' | 'sync-alt' | 'table-new' | 'table' | 'tablet-android' | 'tachometer-slow' | 'tachometer-slowest' | 'tags' | 'team-funnel' | 'telephone-fill' | 'telephone' | 'terminal' | 'text-paragraph' | 'toggles' | 'tools' | 'trash' | 'turtle' | 'user-alt' | 'user-circle' | 'user-friends' | 'users' | 'vendors/graphql' | 'vendors/mobx' | 'vendors/ngrx' | 'vendors/redux' | 'vendors/vuex' | 'web-vitals' | 'wifi' | 'window-alt' | 'window-restore' | 'window-x' | 'window' | 'zoom-in'; interface Props { name: IconNames; @@ -116,6 +116,7 @@ const SVG = (props: Props) => { case 'click-hesitation': return ; case 'click-rage': return ; case 'clipboard-list-check': return ; + case 'clock-history': return ; case 'clock': return ; case 'close': return ; case 'cloud-fog2-fill': return ; @@ -147,15 +148,16 @@ const SVG = (props: Props) => { case 'db-icons/icn-card-insights': return ; case 'db-icons/icn-card-library': return ; case 'db-icons/icn-card-mapchart': return ; + case 'db-icons/icn-card-pathAnalysis': return ; case 'db-icons/icn-card-performance': return ; case 'db-icons/icn-card-resources': return ; case 'db-icons/icn-card-table': return ; case 'db-icons/icn-card-timeseries': return ; - case 'db-icons/icn-card-userPath': return ; case 'db-icons/icn-card-webVitals': return ; case 'desktop': return ; case 'device': return ; case 'diagram-3': return ; + case 'dice-3': return ; case 'dizzy': return ; case 'door-closed': return ; case 'doublecheck': return ; diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 004b5839a..b2a4da493 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -20,7 +20,7 @@ export const ERRORS = 'errors'; export const PERFORMANCE = 'performance'; export const RESOURCE_MONITORING = 'resources'; export const WEB_VITALS = 'webVitals'; -export const USER_PATH = 'userPath'; +export const USER_PATH = 'pathAnalysis'; export const RETENTION = 'retention'; export const FEATURE_ADOPTION = 'featureAdoption'; export const INSIGHTS = 'insights'; @@ -185,18 +185,19 @@ export const TYPES: CardType[] = [ { title: 'Captured Sessions', slug: FilterKey.COUNT_SESSIONS, description: '' }, ], }, - // { - // title: 'Path Analysis', - // icon: 'signpost-split', - // description: 'See where users are flowing and explore their journeys.', - // slug: USER_PATH, - // }, - // { - // title: 'Retention', - // icon: 'arrow-repeat', - // description: 'Get an understanding of how many users are returning.', - // slug: RETENTION, - // }, + { + title: 'Path Analysis', + icon: 'signpost-split', + description: 'See where users are flowing and explore their journeys.', + slug: USER_PATH, + }, + { + title: 'Retention', + icon: 'arrow-repeat', + description: 'Get an understanding of how many users are returning.', + slug: RETENTION, + disabled: true, + }, // { // title: 'Feature Adoption', // icon: 'card-checklist', diff --git a/frontend/app/initialize.tsx b/frontend/app/initialize.tsx index 6b799eab4..5b7b1ebea 100644 --- a/frontend/app/initialize.tsx +++ b/frontend/app/initialize.tsx @@ -21,7 +21,6 @@ const customTheme: ThemeConfig = { bodyBg: colors['gray-lightest'], headerBg: colors['gray-lightest'], siderBg: colors['gray-lightest'], - }, Menu: { colorPrimary: colors.teal, diff --git a/frontend/app/layout/SideMenu.tsx b/frontend/app/layout/SideMenu.tsx index ff1b793cd..bc5384292 100644 --- a/frontend/app/layout/SideMenu.tsx +++ b/frontend/app/layout/SideMenu.tsx @@ -132,7 +132,7 @@ function SideMenu(props: Props) { <> {isPreferencesActive && ( @@ -151,7 +151,7 @@ function SideMenu(props: Props) { {index > 0 && } {category.title}
} > {category.items.filter((item: any) => !item.hidden).map((item: any) => { const isActive = isMenuItemActive(item.key); @@ -160,6 +160,7 @@ function SideMenu(props: Props) { key={item.key} title={{item.label}} icon={}> + {/*style={{ paddingLeft: '30px' }}*/} {item.children.map((child: any) => {child.label})} @@ -168,7 +169,7 @@ function SideMenu(props: Props) { } - // style={{ color: '#333', height: '32px' }} + style={{ paddingLeft: '20px' }} className={cn('!rounded')} itemIcon={item.leading ? : null}> diff --git a/frontend/app/layout/TopHeader.tsx b/frontend/app/layout/TopHeader.tsx index 75958c803..102bfbf97 100644 --- a/frontend/app/layout/TopHeader.tsx +++ b/frontend/app/layout/TopHeader.tsx @@ -18,7 +18,7 @@ function TopHeader() { position: 'sticky', top: 0, zIndex: 1, - padding: '0 15px', + padding: '0 20px', display: 'flex', alignItems: 'center', height: '60px' @@ -30,6 +30,7 @@ function TopHeader() { onClick={() => { settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed); }} + style={{ paddingTop: '4px' }} className='cursor-pointer' > diff --git a/frontend/app/layout/data.ts b/frontend/app/layout/data.ts index 8e8ae4c49..d2007d98e 100644 --- a/frontend/app/layout/data.ts +++ b/frontend/app/layout/data.ts @@ -70,7 +70,7 @@ export const categories: Category[] = [ title: 'Assist', key: 'assist', items: [ - { label: 'Live Sessions', key: MENU.LIVE_SESSIONS, icon: 'broadcast' }, + { label: 'Cobrowse', key: MENU.LIVE_SESSIONS, icon: 'broadcast' }, { label: 'Recordings', key: MENU.RECORDINGS, icon: 'record-btn', isEnterprise: true } ] }, @@ -112,7 +112,7 @@ export const preferences: Category[] = [ { label: 'Integrations', key: PREFERENCES_MENU.INTEGRATIONS, icon: 'plug' }, { label: 'Metadata', key: PREFERENCES_MENU.METADATA, icon: 'tags' }, { label: 'Webhooks', key: PREFERENCES_MENU.WEBHOOKS, icon: 'link-45deg' }, - { label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'people' }, + { label: 'Modules', key: PREFERENCES_MENU.MODULES, icon: 'puzzle' }, { label: 'Projects', key: PREFERENCES_MENU.PROJECTS, icon: 'folder2' }, { label: 'Roles & Access', diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index ad9aaa8f7..379ce94cf 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -423,21 +423,19 @@ export default class DashboardStore { params['limit'] = metric.limit; } - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { this.pendingRequests += 1; - return metricService - .getMetricChartData(metric, params, isWidget) - .then((data: any) => { - resolve(metric.setData(data, period)); - }) - .catch((err: any) => { - reject(err); - }) - .finally(() => { - setTimeout(() => { - this.pendingRequests = this.pendingRequests - 1; - }, 100); - }); + + try { + const data = await metricService.getMetricChartData(metric, params, isWidget); + resolve(metric.setData(data, period)); + } catch (error) { + reject(error); + } finally { + setTimeout(() => { + this.pendingRequests -= 1; + }, 100); + } }); } } diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index dfdeaf0c9..5fcd0e974 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -12,7 +12,9 @@ import { PERFORMANCE, WEB_VITALS, INSIGHTS, - CLICKMAP + CLICKMAP, + USER_PATH, + RETENTION } from 'App/constants/card'; import { clickmapFilter } from 'App/types/filter/newFilter'; import { getRE } from 'App/utils'; @@ -109,8 +111,14 @@ export default class MetricStore { this.changeType(type); } - if (obj.hasOwnProperty('metricOf') && obj.metricOf !== this.instance.metricOf && (obj.metricOf === 'sessions' || obj.metricOf === 'jsErrors')) { - obj.viewType = 'table' + if (obj.hasOwnProperty('metricOf') && obj.metricOf !== this.instance.metricOf) { + if (obj.metricOf === 'sessions' || obj.metricOf === 'jsErrors') { + obj.viewType = 'table' + } + + if (this.instance.metricType === USER_PATH) { + this.instance.series[0].filter.eventsHeader = obj.metricOf === 'start-point' ? 'START POINT' : 'END POINT'; + } } // handle metricValue change @@ -140,6 +148,9 @@ export default class MetricStore { if (value === TIMESERIES) { obj['viewType'] = 'lineChart'; } + if (value === RETENTION) { + obj['viewType'] = 'cohort'; + } if ( value === ERRORS || value === RESOURCE_MONITORING || @@ -156,11 +167,21 @@ export default class MetricStore { obj.series[0].filter.eventsOrderSupport = ['then'] } + if (value === USER_PATH) { + obj.series[0].filter.eventsHeader = 'START POINT'; + } else { + obj.series[0].filter.eventsHeader = 'EVENTS' + } + if (value === INSIGHTS) { obj['metricOf'] = 'issueCategories'; obj['viewType'] = 'list'; } + if (value === USER_PATH) { + // obj['startType'] = 'start'; + } + if (value === CLICKMAP) { obj.series = obj.series.slice(0, 1); if (this.instance.metricType !== CLICKMAP) { @@ -174,6 +195,8 @@ export default class MetricStore { }); } } + + console.log('obj', obj); this.instance.update(obj); } diff --git a/frontend/app/mstore/types/filter.ts b/frontend/app/mstore/types/filter.ts index 82d087072..1bd5d9401 100644 --- a/frontend/app/mstore/types/filter.ts +++ b/frontend/app/mstore/types/filter.ts @@ -1,15 +1,18 @@ import { makeAutoObservable, runInAction, observable, action } from "mobx" import FilterItem from "./filterItem" +import { filtersMap } from 'Types/filter/newFilter'; export default class Filter { public static get ID_KEY():string { return "filterId" } filterId: string = '' name: string = '' filters: FilterItem[] = [] + excludes: FilterItem[] = [] eventsOrder: string = 'then' eventsOrderSupport: string[] = ['then', 'or', 'and'] startTimestamp: number = 0 endTimestamp: number = 0 + eventsHeader: string = "EVENTS" constructor() { makeAutoObservable(this, { @@ -22,6 +25,7 @@ export default class Filter { removeFilter: action, updateKey: action, merge: action, + addExcludeFilter: action, }) } @@ -73,6 +77,10 @@ export default class Filter { return json } + createFilterBykey(key: string) { + return filtersMap[key] ? new FilterItem(filtersMap[key]) : new FilterItem() + } + toJson() { const json = { name: this.name, @@ -81,4 +89,16 @@ export default class Filter { } return json } + + addExcludeFilter(filter: FilterItem) { + this.excludes.push(filter) + } + + updateExcludeFilter(index: number, filter: FilterItem) { + this.excludes[index] = new FilterItem(filter) + } + + removeExcludeFilter(index: number) { + this.excludes.splice(index, 1) + } } diff --git a/frontend/app/mstore/types/issue.ts b/frontend/app/mstore/types/issue.ts new file mode 100644 index 000000000..0b22f318e --- /dev/null +++ b/frontend/app/mstore/types/issue.ts @@ -0,0 +1,41 @@ +const ISSUE_MAP: any = { + dead_click: { name: 'Dead Click', icon: 'funnel/emoji-dizzy-fill', color: '#9C001F' }, + rage_click: { name: 'Rage Click', icon: 'funnel/emoji-angry-fill', color: '#CC0000' }, + click_rage: { name: 'Click Rage', icon: 'funnel/emoji-angry-fill', color: '#CC0000' }, + excessive_scrolling: { name: 'Excessive Scrolling', icon: 'funnel/mouse', color: '#D3545F' }, + bad_request: { name: 'Bad Request', icon: 'funnel/patch-exclamation-fill', color: '#D70072' }, + missing_resource: { name: 'Missing Resource', icon: 'funnel/image-fill', color: '#B89C50' }, + memory: { name: 'Memory', icon: 'funnel/cpu-fill', color: '#8A5A83' }, + cpu: { name: 'CPU', icon: 'funnel/hdd-fill', color: '#8A5A83' }, + slow_resource: { name: 'Slow Resource', icon: 'funnel/hourglass-top', color: '#8B006D' }, + slow_page_load: { name: 'Slow Page Load', icon: 'funnel/hourglass-top', color: '#8B006D' }, + custom_event_error: { name: 'Custom Event Error', icon: 'funnel/exclamation-circle-fill', color: '#BF6C00' }, + custom: { name: 'Custom', icon: 'funnel/exclamation-circle-fill', color: '#BF6C00' }, + crash: { name: 'Crash', icon: 'funnel/file-x', color: '#BF2D00' }, + js_exception: { name: 'JS Exception', icon: 'funnel/exclamation-circle', color: '#BF2D00' } +}; + +export default class Issue { + type: string = ''; + name: string = ''; + sessionCount: number = 0; + icon: string = ''; + source: string = ''; + + constructor() { + this.type = ''; + this.name = ''; + this.sessionCount = 0; + this.icon = ''; + this.source = ''; + } + + fromJSON(json: any) { + this.type = json.name; + this.name = ISSUE_MAP[json.name].name || ''; + this.sessionCount = json.sessionCount; + this.icon = ISSUE_MAP[json.name].icon || ''; + this.source = json.source; + return this; + } +} \ No newline at end of file diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index ecf7249ce..3598540ed 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -6,13 +6,16 @@ import Funnelissue from 'App/mstore/types/funnelIssue'; import { issueOptions, issueCategories, issueCategoriesMap } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import Period, { LAST_24_HOURS } from 'Types/app/period'; -import Funnel from "../types/funnel"; +import Funnel from '../types/funnel'; import { metricService } from 'App/services'; -import { FUNNEL, INSIGHTS, TABLE, WEB_VITALS } from 'App/constants/card'; +import { FUNNEL, INSIGHTS, TABLE, USER_PATH, WEB_VITALS } from 'App/constants/card'; import Error from '../types/error'; import { getChartFormatter } from 'Types/dashboard/helper'; +import FilterItem from './filterItem'; +import { filtersMap } from 'Types/filter/newFilter'; +import Issue from '../types/issue'; -export class InishtIssue { +export class InsightIssue { icon: string; iconColor: string; change: number; @@ -46,10 +49,20 @@ export class InishtIssue { } } +function cleanFilter(filter: any) { + delete filter['operatorOptions']; + delete filter['placeholder']; + delete filter['category']; + delete filter['label']; + delete filter['icon']; + delete filter['key']; +} + export default class Widget { public static get ID_KEY(): string { return 'metricId'; } + metricId: any = undefined; widgetId: any = undefined; category?: string = undefined; @@ -63,7 +76,7 @@ export default class Widget { sessions: [] = []; isPublic: boolean = true; owner: string = ''; - lastModified: number = new Date().getTime(); + lastModified: DateTime | null = new Date().getTime(); dashboards: any[] = []; dashboardIds: any[] = []; config: any = {}; @@ -71,6 +84,11 @@ export default class Widget { limit: number = 5; thumbnail?: string; params: any = { density: 70 }; + startType: string = 'start'; + // startPoint: FilterItem = filtersMap[FilterKey.LOCATION]; + startPoint: FilterItem = new FilterItem(filtersMap[FilterKey.LOCATION]); + excludes: FilterItem[] = []; + hideExcess?: boolean = false; period: Record = Period({ rangeName: LAST_24_HOURS }); // temp value in detail view hasChanged: boolean = false; @@ -83,7 +101,7 @@ export default class Widget { chart: [], namesMap: {}, avg: 0, - percentiles: [], + percentiles: [] }; isLoading: boolean = false; isValid: boolean = false; @@ -156,13 +174,13 @@ export default class Widget { config: { position: this.position, col: this.config.col, - row: this.config.row, - }, + row: this.config.row + } }; } toJson() { - return { + const data: any = { metricId: this.metricId, widgetId: this.widgetId, metricOf: this.metricOf, @@ -184,10 +202,25 @@ export default class Widget { this.metricOf === FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION ? 4 : this.metricType === WEB_VITALS - ? 1 - : 2, - }, + ? 1 + : 2 + } }; + + if (this.metricType === USER_PATH) { + data.hideExcess = this.hideExcess; + data.startType = this.startType; + data.startPoint = [this.startPoint.toJson()]; + console.log('excludes', this.excludes); + data.excludes = this.series[0].filter.excludes.map((i: any) => i.toJson()); + } + return data; + } + + updateStartPoint(startPoint: any) { + runInAction(() => { + this.startPoint = new FilterItem(startPoint); + }); } validate() { @@ -214,10 +247,10 @@ export default class Widget { .filter((i: any) => i.change > 0 || i.change < 0) .map( (i: any) => - new InishtIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew) + new InsightIssue(i.category, i.name, i.ratio, i.oldValue, i.value, i.change, i.isNew) ); } else if (this.metricType === FUNNEL) { - _data.funnel = new Funnel().fromJSON(_data); + _data.funnel = new Funnel().fromJSON(_data); } else { if (data.hasOwnProperty('chart')) { _data['value'] = data.value; @@ -237,20 +270,20 @@ export default class Widget { _data['chart'] = getChartFormatter(period)(Array.isArray(data) ? data : []); _data['namesMap'] = Array.isArray(data) ? 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; - }, []) + .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; + }, []) : []; } } - Object.assign(this.data, _data) + Object.assign(this.data, _data); return _data; } @@ -261,7 +294,7 @@ export default class Widget { response.map((cat: { sessions: any[] }) => { return { ...cat, - sessions: cat.sessions.map((s: any) => new Session().fromJson(s)), + sessions: cat.sessions.map((s: any) => new Session().fromJson(s)) }; }) ); @@ -269,17 +302,30 @@ export default class Widget { }); } - fetchIssues(filter: any): Promise { + + fetchIssues(card: any): Promise { return new Promise((resolve) => { - metricService.fetchIssues(filter).then((response: any) => { - const significantIssues = response.issues.significant - ? response.issues.significant.map((issue: any) => new Funnelissue().fromJSON(issue)) - : []; - const insignificantIssues = response.issues.insignificant - ? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue)) - : []; + metricService.fetchIssues(card) + .then((response: any) => { + if (card.metricType === USER_PATH) { + resolve({ + total: response.count, + issues: response.values.map((issue: any) => new Issue().fromJSON(issue)) + }); + } else { + const significantIssues = response.issues.significant + ? response.issues.significant.map((issue: any) => new Funnelissue().fromJSON(issue)) + : []; + const insignificantIssues = response.issues.insignificant + ? response.issues.insignificant.map((issue: any) => new Funnelissue().fromJSON(issue)) + : []; + resolve({ + issues: significantIssues.length > 0 ? significantIssues : insignificantIssues + }); + } + }).finally(() => { resolve({ - issues: significantIssues.length > 0 ? significantIssues : insignificantIssues, + issues: [] }); }); }); @@ -292,7 +338,7 @@ export default class Widget { .then((response: any) => { resolve({ issue: new Funnelissue().fromJSON(response.issue), - sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)), + sessions: response.sessions.sessions.map((s: any) => new Session().fromJson(s)) }); }) .catch((error: any) => { diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 5b97ec4ec..2ab32a9eb 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -1,113 +1,127 @@ -import Widget from "App/mstore/types/widget"; +import Widget from 'App/mstore/types/widget'; import APIClient from 'App/api_client'; -import { CLICKMAP } from "App/constants/card"; +import { CLICKMAP, USER_PATH } from 'App/constants/card'; export default class MetricService { - private client: APIClient; + private client: APIClient; - constructor(client?: APIClient) { - this.client = client ? client : new APIClient(); + constructor(client?: APIClient) { + this.client = client ? client : new APIClient(); + } + + initClient(client?: APIClient) { + this.client = client || new APIClient(); + } + + /** + * Get all metrics. + * @returns {Promise} + */ + getMetrics(): Promise { + return this.client.get('/cards') + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data || []); + } + + /** + * Get a metric by metricId. + * @param metricId + * @returns {Promise} + */ + getMetric(metricId: string): Promise { + return this.client.get('/cards/' + metricId) + .then(r => r.json()) + .then((response: { data: any; }) => response.data || {}) + .catch(e => Promise.reject(e)); + } + + /** + * Save a metric. + * @param metric + * @returns + */ + saveMetric(metric: Widget): Promise { + const data = metric.toJson(); + const isCreating = !data[Widget.ID_KEY]; + const url = isCreating ? '/cards' : '/cards/' + data[Widget.ID_KEY]; + return this.client.post(url, data) + .then(r => r.json()) + .then((response: { data: any; }) => response.data || {}) + .catch(e => Promise.reject(e)); + } + + /** + * Delete a metric. + * @param metricId + * @returns {Promise} + */ + deleteMetric(metricId: string): Promise { + return this.client.delete('/cards/' + metricId) + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data); + } + + + /** + * Get all templates. + * @returns {Promise} + */ + getTemplates(): Promise { + return this.client.get('/cards/templates') + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data || []); + } + + async getMetricChartData(metric: Widget, data: any, isWidget: boolean = false): Promise { + if ( + metric.metricType === CLICKMAP + && document.location.pathname.split('/').pop() === 'metrics' + && (document.location.pathname.indexOf('dashboard') !== -1 && document.location.pathname.indexOf('metric') === -1) + ) { + return Promise.resolve({}); + } + const path = isWidget ? `/cards/${metric.metricId}/chart` : `/cards/try`; + if (metric.metricType === USER_PATH) { + data.density = 4; + data.metricOf = 'sessionCount'; + } + try { + const r = await this.client.post(path, data); + const response = await r.json(); + return response.data || {}; + } catch (e) { + return await Promise.reject(e); + } + } + + /** + * Fetch sessions from the server. + * @param metricId {String} + * @param filter + * @returns + */ + fetchSessions(metricId: string | null, filter: any): Promise { + return this.client.post(metricId ? `/cards/${metricId}/sessions` : '/cards/try/sessions', filter) + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data || []); + } + + async fetchIssues(filter: any): Promise { + if (filter.metricType === USER_PATH) { + const widget = new Widget().fromJson(filter); + const drillDownFilter = filter.filters; + filter = widget.toJson(); + filter.filters = drillDownFilter; } - initClient(client?: APIClient) { - this.client = client || new APIClient(); - } + let resp: Response = await this.client.post(`/cards/try/issues`, filter); + const json: any = await resp.json(); + return await json.data || {}; + } - /** - * Get all metrics. - * @returns {Promise} - */ - getMetrics(): Promise { - return this.client.get('/cards') - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || []); - } - - /** - * Get a metric by metricId. - * @param metricId - * @returns {Promise} - */ - getMetric(metricId: string): Promise { - return this.client.get('/cards/' + metricId) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) - .catch(e => Promise.reject(e)) - } - - /** - * Save a metric. - * @param metric - * @returns - */ - saveMetric(metric: Widget): Promise { - const data = metric.toJson() - const isCreating = !data[Widget.ID_KEY]; - const url = isCreating ? '/cards' : '/cards/' + data[Widget.ID_KEY]; - return this.client.post(url, data) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) - .catch(e => Promise.reject(e)) - } - - /** - * Delete a metric. - * @param metricId - * @returns {Promise} - */ - deleteMetric(metricId: string): Promise { - return this.client.delete('/cards/' + metricId) - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data); - } - - - /** - * Get all templates. - * @returns {Promise} - */ - getTemplates(): Promise { - return this.client.get('/cards/templates') - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || []); - } - - getMetricChartData(metric: Widget, data: any, isWidget: boolean = false): Promise { - if ( - metric.metricType === CLICKMAP - && document.location.pathname.split('/').pop() === 'metrics' - && (document.location.pathname.indexOf('dashboard') !== -1 && document.location.pathname.indexOf('metric') === -1) - ) { - return Promise.resolve({}) - } - const path = isWidget ? `/cards/${metric.metricId}/chart` : `/cards/try`; - return this.client.post(path, data) - .then(r => r.json()) - .then((response: { data: any; }) => response.data || {}) - .catch(e => Promise.reject(e)) - } - - /** - * Fetch sessions from the server. - * @param metricId {String} - * @param filter - * @returns - */ - fetchSessions(metricId: string, filter: any): Promise { - return this.client.post(metricId ? `/cards/${metricId}/sessions` : '/cards/try/sessions', filter) - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || []); - } - - fetchIssues(filter: string): Promise { - return this.client.post(`/cards/try/issues`, filter) - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || {}); - } - - fetchIssue(metricId: string, issueId: string, params: any): Promise { - return this.client.post(`/cards/${metricId}/issues/${issueId}/sessions`, params) - .then((response: { json: () => any; }) => response.json()) - .then((response: { data: any; }) => response.data || {}); - } + fetchIssue(metricId: string, issueId: string, params: any): Promise { + return this.client.post(`/cards/${metricId}/issues/${issueId}/sessions`, params) + .then((response: { json: () => any; }) => response.json()) + .then((response: { data: any; }) => response.data || {}); + } } diff --git a/frontend/app/svg/icons/clock-history.svg b/frontend/app/svg/icons/clock-history.svg new file mode 100644 index 000000000..6f553fd05 --- /dev/null +++ b/frontend/app/svg/icons/clock-history.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/app/svg/icons/db-icons/icn-card-userPath.svg b/frontend/app/svg/icons/db-icons/icn-card-pathAnalysis.svg similarity index 100% rename from frontend/app/svg/icons/db-icons/icn-card-userPath.svg rename to frontend/app/svg/icons/db-icons/icn-card-pathAnalysis.svg diff --git a/frontend/app/svg/icons/dice-3.svg b/frontend/app/svg/icons/dice-3.svg new file mode 100644 index 000000000..5398d14d8 --- /dev/null +++ b/frontend/app/svg/icons/dice-3.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 866d73062..5457cafc9 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -1,75 +1,531 @@ import { KEYS } from 'Types/filter/customFilter'; import Record from 'Types/Record'; -import { FilterType, FilterKey, FilterCategory } from './filterType' +import { FilterType, FilterKey, FilterCategory } from './filterType'; import filterOptions, { countries, platformOptions } from 'App/constants'; import { capitalize } from 'App/utils'; const countryOptions = Object.keys(countries).map(i => ({ label: countries[i], value: i })); -const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }] +const containsFilters = [{ key: 'contains', label: 'contains', text: 'contains', value: 'contains' }]; export const filters = [ - { key: FilterKey.CLICK, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Click', operator: 'on', operatorOptions: filterOptions.targetOperators, icon: 'filters/click', isEvent: true }, - { key: FilterKey.INPUT, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Text Input', placeholder: 'Enter input label name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/input', isEvent: true }, - { key: FilterKey.LOCATION, type: FilterType.MULTIPLE, category: FilterCategory.INTERACTIONS, label: 'Visited URL', placeholder: 'Enter path', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/location', isEvent: true }, - { key: FilterKey.CUSTOM, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Custom Events', placeholder: 'Enter event key', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/custom', isEvent: true }, + { + key: FilterKey.CLICK, + type: FilterType.MULTIPLE, + category: FilterCategory.INTERACTIONS, + label: 'Click', + operator: 'on', + operatorOptions: filterOptions.targetOperators, + icon: 'filters/click', + isEvent: true + }, + { + key: FilterKey.INPUT, + type: FilterType.MULTIPLE, + category: FilterCategory.INTERACTIONS, + label: 'Text Input', + placeholder: 'Enter input label name', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/input', + isEvent: true + }, + { + key: FilterKey.LOCATION, + type: FilterType.MULTIPLE, + category: FilterCategory.INTERACTIONS, + label: 'Visited URL', + placeholder: 'Enter path', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/location', + isEvent: true + }, + { + key: FilterKey.CUSTOM, + type: FilterType.MULTIPLE, + category: FilterCategory.JAVASCRIPT, + label: 'Custom Events', + placeholder: 'Enter event key', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/custom', + isEvent: true + }, // { key: FilterKey.REQUEST, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Fetch', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch', isEvent: true }, - { key: FilterKey.FETCH, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, operator: 'is', label: 'Network Request', filters: [ - { key: FilterKey.FETCH_URL, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with URL', placeholder: 'Enter path or URL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - { key: FilterKey.FETCH_STATUS_CODE, type: FilterType.NUMBER_MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with status code', placeholder: 'Enter status code', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' }, - { key: FilterKey.FETCH_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', placeholder: 'Select method type', operatorOptions: filterOptions.stringOperatorsLimited, icon: 'filters/fetch', options: filterOptions.methodOptions }, - { key: FilterKey.FETCH_DURATION, type: FilterType.NUMBER, category: FilterCategory.PERFORMANCE, label: 'with duration (ms)', placeholder: 'E.g. 12', operator: '=', operatorOptions: filterOptions.customOperators, icon: 'filters/fetch' }, - { key: FilterKey.FETCH_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - { key: FilterKey.FETCH_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - ], icon: 'filters/fetch', isEvent: true }, - { key: FilterKey.GRAPHQL, type: FilterType.SUB_FILTERS, category: FilterCategory.JAVASCRIPT, label: 'GraphQL', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/graphql', isEvent: true, filters: [ - { key: FilterKey.GRAPHQL_NAME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'with name', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - { key: FilterKey.GRAPHQL_METHOD, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.PERFORMANCE, label: 'with method', operator: 'is', operatorOptions: filterOptions.stringOperatorsLimited, icon: 'filters/fetch', options: filterOptions.methodOptions }, - { key: FilterKey.GRAPHQL_REQUEST_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with request body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - { key: FilterKey.GRAPHQL_RESPONSE_BODY, type: FilterType.STRING, category: FilterCategory.PERFORMANCE, label: 'with response body', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/fetch' }, - ]}, - { key: FilterKey.STATEACTION, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'State Action', placeholder: 'E.g. 12', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/state-action', isEvent: true }, - { key: FilterKey.ERROR, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Error Message', placeholder: 'E.g. Uncaught SyntaxError', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/error', isEvent: true }, + { + key: FilterKey.FETCH, + type: FilterType.SUB_FILTERS, + category: FilterCategory.JAVASCRIPT, + operator: 'is', + label: 'Network Request', + filters: [ + { + key: FilterKey.FETCH_URL, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'with URL', + placeholder: 'Enter path or URL', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.FETCH_STATUS_CODE, + type: FilterType.NUMBER_MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'with status code', + placeholder: 'Enter status code', + operator: '=', + operatorOptions: filterOptions.customOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.FETCH_METHOD, + type: FilterType.MULTIPLE_DROPDOWN, + category: FilterCategory.PERFORMANCE, + label: 'with method', + operator: 'is', + placeholder: 'Select method type', + operatorOptions: filterOptions.stringOperatorsLimited, + icon: 'filters/fetch', + options: filterOptions.methodOptions + }, + { + key: FilterKey.FETCH_DURATION, + type: FilterType.NUMBER, + category: FilterCategory.PERFORMANCE, + label: 'with duration (ms)', + placeholder: 'E.g. 12', + operator: '=', + operatorOptions: filterOptions.customOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.FETCH_REQUEST_BODY, + type: FilterType.STRING, + category: FilterCategory.PERFORMANCE, + label: 'with request body', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.FETCH_RESPONSE_BODY, + type: FilterType.STRING, + category: FilterCategory.PERFORMANCE, + label: 'with response body', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + } + ], + icon: 'filters/fetch', + isEvent: true + }, + { + key: FilterKey.GRAPHQL, + type: FilterType.SUB_FILTERS, + category: FilterCategory.JAVASCRIPT, + label: 'GraphQL', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/graphql', + isEvent: true, + filters: [ + { + key: FilterKey.GRAPHQL_NAME, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'with name', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.GRAPHQL_METHOD, + type: FilterType.MULTIPLE_DROPDOWN, + category: FilterCategory.PERFORMANCE, + label: 'with method', + operator: 'is', + operatorOptions: filterOptions.stringOperatorsLimited, + icon: 'filters/fetch', + options: filterOptions.methodOptions + }, + { + key: FilterKey.GRAPHQL_REQUEST_BODY, + type: FilterType.STRING, + category: FilterCategory.PERFORMANCE, + label: 'with request body', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + }, + { + key: FilterKey.GRAPHQL_RESPONSE_BODY, + type: FilterType.STRING, + category: FilterCategory.PERFORMANCE, + label: 'with response body', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/fetch' + } + ] + }, + { + key: FilterKey.STATEACTION, + type: FilterType.MULTIPLE, + category: FilterCategory.JAVASCRIPT, + label: 'State Action', + placeholder: 'E.g. 12', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/state-action', + isEvent: true + }, + { + key: FilterKey.ERROR, + type: FilterType.MULTIPLE, + category: FilterCategory.JAVASCRIPT, + label: 'Error Message', + placeholder: 'E.g. Uncaught SyntaxError', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/error', + isEvent: true + }, // { key: FilterKey.METADATA, type: FilterType.MULTIPLE, category: FilterCategory.METADATA, label: 'Metadata', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/metadata', isEvent: true }, - // FILTERS - { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' }, - { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' }, - { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' }, - { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions }, - { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'Version ID', placeholder: 'E.g. v1.0.8', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'collection' }, - { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' }, - { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' }, - { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - { key: FilterKey.USER_CITY, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User City', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - { key: FilterKey.USER_STATE, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User State', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - // { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' }, - { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', operator: 'is', operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined'}]), icon: 'filters/userid' }, - { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' }, + // FILTERS + { + key: FilterKey.USER_OS, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User OS', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/os' + }, + { + key: FilterKey.USER_BROWSER, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User Browser', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/browser' + }, + { + key: FilterKey.USER_DEVICE, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User Device', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/device' + }, + { + key: FilterKey.PLATFORM, + type: FilterType.MULTIPLE_DROPDOWN, + category: FilterCategory.GEAR, + label: 'Platform', + operator: 'is', + operatorOptions: filterOptions.baseOperators, + icon: 'filters/platform', + options: platformOptions + }, + { + key: FilterKey.REVID, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'Version ID', + placeholder: 'E.g. v1.0.8', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'collection' + }, + { + key: FilterKey.REFERRER, + type: FilterType.MULTIPLE, + category: FilterCategory.RECORDING_ATTRIBUTES, + label: 'Referrer', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/arrow-return-right' + }, + { + key: FilterKey.DURATION, + type: FilterType.DURATION, + category: FilterCategory.RECORDING_ATTRIBUTES, + label: 'Duration', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is']), + icon: 'filters/duration' + }, + { + key: FilterKey.USER_COUNTRY, + type: FilterType.MULTIPLE_DROPDOWN, + category: FilterCategory.USER, + label: 'User Country', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + { + key: FilterKey.USER_CITY, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User City', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + { + key: FilterKey.USER_STATE, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User State', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + // { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' }, + { + key: FilterKey.USERID, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User Id', + placeholder: 'E.g. Alex, or alex@domain.com, or EMP123', + operator: 'is', + operatorOptions: filterOptions.stringOperators.concat([{ label: 'is undefined', value: 'isUndefined' }]), + icon: 'filters/userid' + }, + { + key: FilterKey.USERANONYMOUSID, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User AnonymousId', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/userid' + }, - // PERFORMANCE - { key: FilterKey.DOM_COMPLETE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'DOM Complete', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/dom-complete', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, - { key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Largest Contentful Paint', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/lcpt', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, - { key: FilterKey.TTFB, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Time to First Byte', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/ttfb', isEvent: true, hasSource: true, sourceOperator: '>=', sourceUnit: 'ms', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators, sourcePlaceholder: 'E.g. 12', }, - { key: FilterKey.AVG_CPU_LOAD, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg CPU Load', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/cpu-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: '%', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, - { key: FilterKey.AVG_MEMORY_USAGE, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Avg Memory Usage', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, source: [], icon: 'filters/memory-load', isEvent: true, hasSource: true, sourceOperator: '>=', sourcePlaceholder: 'E.g. 12', sourceUnit: 'mb', sourceType: FilterType.NUMBER, sourceOperatorOptions: filterOptions.customOperators }, - { key: FilterKey.FETCH_FAILED, type: FilterType.MULTIPLE, category: FilterCategory.PERFORMANCE, label: 'Failed Request', placeholder: 'Enter path', operator: 'isAny', operatorOptions: filterOptions.stringOperatorsPerformance, icon: 'filters/fetch-failed', isEvent: true }, - { key: FilterKey.ISSUE, type: FilterType.ISSUE, category: FilterCategory.JAVASCRIPT, label: 'Issue', placeholder: 'Select an issue', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/click', options: filterOptions.issueOptions }, + // PERFORMANCE + { + key: FilterKey.DOM_COMPLETE, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'DOM Complete', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + source: [], + icon: 'filters/dom-complete', + isEvent: true, + hasSource: true, + sourceOperator: '>=', + sourcePlaceholder: 'E.g. 12', + sourceUnit: 'ms', + sourceType: FilterType.NUMBER, + sourceOperatorOptions: filterOptions.customOperators + }, + { + key: FilterKey.LARGEST_CONTENTFUL_PAINT_TIME, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'Largest Contentful Paint', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + source: [], + icon: 'filters/lcpt', + isEvent: true, + hasSource: true, + sourceOperator: '>=', + sourcePlaceholder: 'E.g. 12', + sourceUnit: 'ms', + sourceType: FilterType.NUMBER, + sourceOperatorOptions: filterOptions.customOperators + }, + { + key: FilterKey.TTFB, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'Time to First Byte', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + source: [], + icon: 'filters/ttfb', + isEvent: true, + hasSource: true, + sourceOperator: '>=', + sourceUnit: 'ms', + sourceType: FilterType.NUMBER, + sourceOperatorOptions: filterOptions.customOperators, + sourcePlaceholder: 'E.g. 12' + }, + { + key: FilterKey.AVG_CPU_LOAD, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'Avg CPU Load', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + source: [], + icon: 'filters/cpu-load', + isEvent: true, + hasSource: true, + sourceOperator: '>=', + sourcePlaceholder: 'E.g. 12', + sourceUnit: '%', + sourceType: FilterType.NUMBER, + sourceOperatorOptions: filterOptions.customOperators + }, + { + key: FilterKey.AVG_MEMORY_USAGE, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'Avg Memory Usage', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + source: [], + icon: 'filters/memory-load', + isEvent: true, + hasSource: true, + sourceOperator: '>=', + sourcePlaceholder: 'E.g. 12', + sourceUnit: 'mb', + sourceType: FilterType.NUMBER, + sourceOperatorOptions: filterOptions.customOperators + }, + { + key: FilterKey.FETCH_FAILED, + type: FilterType.MULTIPLE, + category: FilterCategory.PERFORMANCE, + label: 'Failed Request', + placeholder: 'Enter path', + operator: 'isAny', + operatorOptions: filterOptions.stringOperatorsPerformance, + icon: 'filters/fetch-failed', + isEvent: true + }, + { + key: FilterKey.ISSUE, + type: FilterType.ISSUE, + category: FilterCategory.JAVASCRIPT, + label: 'Issue', + placeholder: 'Select an issue', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/click', + options: filterOptions.issueOptions + } ]; export const flagConditionFilters = [ - { key: FilterKey.USER_OS, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User OS', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/os' }, - { key: FilterKey.USER_BROWSER, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Browser', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/browser' }, - { key: FilterKey.USER_DEVICE, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'User Device', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/device' }, - { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/arrow-return-right' }, - { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.USER, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - { key: FilterKey.USER_CITY, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User City', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - { key: FilterKey.USER_STATE, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User State', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, - { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'isUndefined', operatorOptions: [{ label: 'is undefined', value: 'isUndefined'}, { key: 'isAny', label: 'is any', value: 'isAny' }], icon: 'filters/userid' }, -] + { + key: FilterKey.USER_OS, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User OS', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/os' + }, + { + key: FilterKey.USER_BROWSER, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User Browser', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/browser' + }, + { + key: FilterKey.USER_DEVICE, + type: FilterType.MULTIPLE, + category: FilterCategory.GEAR, + label: 'User Device', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/device' + }, + { + key: FilterKey.REFERRER, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'Referrer', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/arrow-return-right' + }, + { + key: FilterKey.USER_COUNTRY, + type: FilterType.MULTIPLE_DROPDOWN, + category: FilterCategory.USER, + label: 'User Country', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + { + key: FilterKey.USER_CITY, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User City', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + { + key: FilterKey.USER_STATE, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User State', + operator: 'is', + operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), + icon: 'filters/country', + options: countryOptions + }, + { + key: FilterKey.USERID, + type: FilterType.MULTIPLE, + category: FilterCategory.USER, + label: 'User Id', + operator: 'isUndefined', + operatorOptions: [{ label: 'is undefined', value: 'isUndefined' }, { + key: 'isAny', + label: 'is any', + value: 'isAny' + }], + icon: 'filters/userid' + } +]; + +const pathAnalysisStartPoint = [ + { + key: FilterKey.LOCATION, + type: FilterType.MULTIPLE, + category: FilterCategory.INTERACTIONS, + label: 'Visited URL', + placeholder: 'Enter path', + operator: 'is', + operatorOptions: filterOptions.stringOperators, + icon: 'filters/location', + isEvent: true + } +]; export const eventKeys = filters.filter((i) => i.isEvent).map(i => i.key); export const nonFlagFilters = filters.filter(i => { - return flagConditionFilters.findIndex(f => f.key === i.key) === -1 + return flagConditionFilters.findIndex(f => f.key === i.key) === -1; }).map(i => i.key); export const clickmapFilter = { @@ -80,15 +536,16 @@ export const clickmapFilter = { operator: filterOptions.pageUrlOperators[0].value, operatorOptions: filterOptions.pageUrlOperators, icon: 'filters/location', - isEvent: true, -} + isEvent: true +}; const mapFilters = (list) => { return list.reduce((acc, filter) => { + filter.value = ['']; acc[filter.key] = filter; return acc; }, {}); -} +}; const liveFilterSupportedOperators = ['is', 'contains']; const mapLiveFilters = (list) => { @@ -101,25 +558,25 @@ const mapLiveFilters = (list) => { filter.key !== FilterKey.DURATION && filter.key !== FilterKey.REFERRER ) { - obj[filter.key] = {...filter}; + obj[filter.key] = { ...filter }; obj[filter.key].operatorOptions = filter.operatorOptions.filter(operator => liveFilterSupportedOperators.includes(operator.value)); if (filter.key === FilterKey.PLATFORM) { obj[filter.key].operator = 'is'; } } - }) + }); return obj; -} +}; export const filterLabelMap = filters.reduce((acc, filter) => { - acc[filter.key] = filter.label - return acc -}, {}) + acc[filter.key] = filter.label; + return acc; +}, {}); -export let filtersMap = mapFilters(filters) -export let liveFiltersMap = mapLiveFilters(filters) -export let fflagsConditionsMap = mapFilters(flagConditionFilters) +export let filtersMap = mapFilters(filters); +export let liveFiltersMap = mapLiveFilters(filters); +export let fflagsConditionsMap = mapFilters(flagConditionFilters); export const clearMetaFilters = () => { filtersMap = mapFilters(filters); @@ -143,19 +600,37 @@ export const addElementToFiltersMap = ( operatorOptions = filterOptions.stringOperators, icon = 'filters/metadata' ) => { - filtersMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true } -} + filtersMap[key] = { + key, + type, + category, + label: capitalize(key), + operator: operator, + operatorOptions, + icon, + isLive: true + }; +}; export const addElementToFlagConditionsMap = ( - category = FilterCategory.METADATA, - key, - type = FilterType.MULTIPLE, - operator = 'is', - operatorOptions = filterOptions.stringOperators, - icon = 'filters/metadata' + category = FilterCategory.METADATA, + key, + type = FilterType.MULTIPLE, + operator = 'is', + operatorOptions = filterOptions.stringOperators, + icon = 'filters/metadata' ) => { - fflagsConditionsMap[key] = { key, type, category, label: capitalize(key), operator: operator, operatorOptions, icon, isLive: true } -} + fflagsConditionsMap[key] = { + key, + type, + category, + label: capitalize(key), + operator: operator, + operatorOptions, + icon, + isLive: true + }; +}; export const addElementToLiveFiltersMap = ( category = FilterCategory.METADATA, @@ -166,14 +641,14 @@ export const addElementToLiveFiltersMap = ( icon = 'filters/metadata' ) => { liveFiltersMap[key] = { - key, type, category, label: capitalize(key), - operator: operator, - operatorOptions, - icon, - operatorDisabled: true, - isLive: true - } -} + key, type, category, label: capitalize(key), + operator: operator, + operatorOptions, + icon, + operatorDisabled: true, + isLive: true + }; +}; export default Record({ timestamp: 0, @@ -182,8 +657,8 @@ export default Record({ placeholder: '', icon: '', type: '', - value: [""], - source: [""], + value: [''], + source: [''], category: '', custom: '', @@ -195,7 +670,7 @@ export default Record({ actualValue: '', hasSource: false, - source: [""], + source: [''], sourceType: '', sourceOperator: '=', sourcePlaceholder: '', @@ -208,20 +683,19 @@ export default Record({ isEvent: false, index: 0, options: [], - filters: [], - + excludes: [] }, { - keyKey: "_key", + keyKey: '_key', fromJS: ({ value, type, subFilter = false, ...filter }) => { let _filter = {}; if (subFilter) { const mainFilter = filtersMap[subFilter]; - const subFilterMap = {} + const subFilterMap = {}; mainFilter.filters.forEach(option => { - subFilterMap[option.key] = option - }) - _filter = subFilterMap[type] + subFilterMap[option.key] = option; + }); + _filter = subFilterMap[type]; } else { if (type === FilterKey.METADATA) { _filter = filtersMap[filter.source]; @@ -233,8 +707,8 @@ export default Record({ if (!_filter) { _filter = { key: filter.key, - type: "MULTIPLE", - } + type: 'MULTIPLE' + }; } return { @@ -242,10 +716,10 @@ export default Record({ ...filter, key: _filter.key, type: _filter.type, // camelCased(filter.type.toLowerCase()), - value: value.length === 0 || !value ? [""] : value, - } - }, -}) + value: value.length === 0 || !value ? [''] : value + }; + } +}); /** * Group filters by category @@ -263,7 +737,7 @@ export const generateFilterOptions = (map) => { } }); return filterSection; -} +}; export const generateFlagConditionOptions = (map) => { const filterSection = {}; @@ -276,8 +750,7 @@ export const generateFlagConditionOptions = (map) => { } }); return filterSection; -} - +}; export const generateLiveFilterOptions = (map) => { @@ -294,4 +767,4 @@ export const generateLiveFilterOptions = (map) => { } }); return filterSection; -} +}; diff --git a/frontend/package.json b/frontend/package.json index 035061524..f2a79a6f4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -70,7 +70,7 @@ "react-tippy": "^1.4.0", "react-toastify": "^9.1.1", "react-virtualized": "^9.22.3", - "recharts": "^2.1.13", + "recharts": "^2.8.0", "redux": "^4.0.5", "redux-immutable": "^4.0.0", "redux-thunk": "^2.3.0", diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 721f20357..51616c57d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -11270,10 +11270,10 @@ __metadata: languageName: node linkType: hard -"fast-equals@npm:^2.0.0": - version: 2.0.4 - resolution: "fast-equals@npm:2.0.4" - checksum: 2867aa148c995e4ea921242ab605b157e5cbdc44793ebddf83684a5e6673be5016eb790ec4d8329317b92887e1108fb67ed3d4334529f2a7650c1338e6aa2c5f +"fast-equals@npm:^5.0.0": + version: 5.0.1 + resolution: "fast-equals@npm:5.0.1" + checksum: d7077b8b681036c2840ed9860a3048e44fc268fad2b525b8f25b43458be0c8ad976152eb4b475de9617170423c5b802121ebb61ed6641c3ac035fadaf805c8c0 languageName: node linkType: hard @@ -17733,7 +17733,7 @@ __metadata: react-tippy: ^1.4.0 react-toastify: ^9.1.1 react-virtualized: ^9.22.3 - recharts: ^2.1.13 + recharts: ^2.8.0 redux: ^4.0.5 redux-immutable: ^4.0.0 redux-thunk: ^2.3.0 @@ -20589,15 +20589,15 @@ __metadata: languageName: node linkType: hard -"react-resize-detector@npm:^7.1.2": - version: 7.1.2 - resolution: "react-resize-detector@npm:7.1.2" +"react-resize-detector@npm:^8.0.4": + version: 8.1.0 + resolution: "react-resize-detector@npm:8.1.0" dependencies: lodash: ^4.17.21 peerDependencies: react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 2285b0024bcc736c7d5e80279e819835a5e8bef0899778100d2434b1c4b10971e6ae253df073ca96a20cacc47b3c249bc675479e5fc4ec1f6652fcca7f48ec22 + checksum: 2ae9927c6e53de460a1f216e008acd30d84d11297bbb2c38687206b998309dfc3928992141e2176a00af642ef8b1c7f56e97681cc7d1c3a4f389e29d46a443af languageName: node linkType: hard @@ -20657,17 +20657,17 @@ __metadata: languageName: node linkType: hard -"react-smooth@npm:^2.0.1": - version: 2.0.1 - resolution: "react-smooth@npm:2.0.1" +"react-smooth@npm:^2.0.2": + version: 2.0.4 + resolution: "react-smooth@npm:2.0.4" dependencies: - fast-equals: ^2.0.0 + fast-equals: ^5.0.0 react-transition-group: 2.9.0 peerDependencies: prop-types: ^15.6.0 react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 3227aecfb2c2783a53045e0b5931432fed3376c98893d1190c259ed4281182dc0b25f200f50b55733e01d9d6d1950641d1b5177d899277e3d4fe8264b6bddcc2 + checksum: a67103136ef7f7378183dce3baecb28f1828ac71ce9901f1dfd92a4ec3a637014a7a6787f79552bb5cd2b779d27afd978d0f5eb33693b6a1210f774ab6e131cf languageName: node linkType: hard @@ -20964,16 +20964,16 @@ __metadata: languageName: node linkType: hard -"recharts@npm:^2.1.13": - version: 2.3.2 - resolution: "recharts@npm:2.3.2" +"recharts@npm:^2.8.0": + version: 2.8.0 + resolution: "recharts@npm:2.8.0" dependencies: classnames: ^2.2.5 eventemitter3: ^4.0.1 lodash: ^4.17.19 react-is: ^16.10.2 - react-resize-detector: ^7.1.2 - react-smooth: ^2.0.1 + react-resize-detector: ^8.0.4 + react-smooth: ^2.0.2 recharts-scale: ^0.4.4 reduce-css-calc: ^2.1.8 victory-vendor: ^36.6.8 @@ -20981,7 +20981,7 @@ __metadata: prop-types: ^15.6.0 react: ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 88e86118388df94c07dc9b39d4e0a0c47defb1a6348fdbc0e2ad457f1329e5fbc4525fbd67bb94485a7ef0e8b23320e4e9066640b18241b3e0fad80b5eabfd28 + checksum: 9f78bdf67fd5394f472a37679d2f9c9194f1d572c5bc086caae578409fceb7bc87a4df07dd096820c103d2002d9714c8db838c99b2722950ab9edab38be2daf9 languageName: node linkType: hard