From 827766699db14457a41ca0ea0e95abc29f6d08c0 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 12 Dec 2022 18:54:46 +0530 Subject: [PATCH 01/56] rebase dev --- .../DashboardList/DashboardList.tsx | 19 +- .../DashboardList/DashboardsView.tsx | 76 ++++---- .../DashboardWidgetGrid.tsx | 174 +++++++++++++++--- frontend/app/components/ui/SVG.tsx | 32 ++-- frontend/app/mstore/dashboardStore.ts | 2 +- frontend/app/svg/icons/activity.svg | 3 + frontend/app/svg/icons/card-checklist.svg | 4 + frontend/app/svg/icons/files.svg | 3 + frontend/app/svg/icons/grid-1x2.svg | 3 + frontend/app/svg/icons/grid.svg | 3 + frontend/app/svg/icons/signpost-split.svg | 3 + frontend/app/svg/icons/speedometer2.svg | 4 + 12 files changed, 242 insertions(+), 84 deletions(-) create mode 100644 frontend/app/svg/icons/activity.svg create mode 100644 frontend/app/svg/icons/card-checklist.svg create mode 100644 frontend/app/svg/icons/files.svg create mode 100644 frontend/app/svg/icons/grid-1x2.svg create mode 100644 frontend/app/svg/icons/grid.svg create mode 100644 frontend/app/svg/icons/signpost-split.svg create mode 100644 frontend/app/svg/icons/speedometer2.svg diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx index 7bb716733..efbbd9b1d 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardList.tsx @@ -24,11 +24,20 @@ function DashboardList() { show={lenth === 0} title={
- -
- {dashboardsSearch !== '' - ? 'No matching results' - : "You haven't created any dashboards yet"} +
+ {dashboardsSearch !== '' ? ( + 'No matching results' + ) : ( +
+
Create your first Dashboard
+
+ A dashboard lets you visualize trends and insights of data captured by OpenReplay. +
+
+ )} +
+
+
} diff --git a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx index 7378e88f8..d54b34dc3 100644 --- a/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardList/DashboardsView.tsx @@ -3,43 +3,57 @@ import { Button, PageTitle, Icon } from 'UI'; import withPageTitle from 'HOCs/withPageTitle'; import { useStore } from 'App/mstore'; import { withSiteId } from 'App/routes'; - +import Select from 'Shared/Select'; import DashboardList from './DashboardList'; import DashboardSearch from './DashboardSearch'; +import { sort } from 'App/duck/sessions'; +import { useObserver } from 'mobx-react-lite'; -function DashboardsView({ history, siteId }: { history: any, siteId: string }) { - const { dashboardStore } = useStore(); +function DashboardsView({ history, siteId }: { history: any; siteId: string }) { + const { dashboardStore } = useStore(); + const sort = useObserver(() => dashboardStore.sort); - const onAddDashboardClick = () => { - dashboardStore.initDashboard(); - dashboardStore - .save(dashboardStore.dashboardInstance) - .then(async (syncedDashboard) => { - dashboardStore.selectDashboardById(syncedDashboard.dashboardId); - history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId)) - }) - } + const onAddDashboardClick = () => { + dashboardStore.initDashboard(); + dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => { + dashboardStore.selectDashboardById(syncedDashboard.dashboardId); + history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId)); + }); + }; - return ( -
-
-
- -
-
- -
- -
-
-
-
- - A Dashboard is a collection of Metrics that can be shared across teams. -
- + return ( +
+
+
+
- ); +
+ +
+ dashboardStore.updateKey('sort', { by: value.value })} - /> -
-
- -
-
-
+
); diff --git a/frontend/app/components/Dashboard/components/DashboardList/Header.tsx b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx new file mode 100644 index 000000000..f77d3ae69 --- /dev/null +++ b/frontend/app/components/Dashboard/components/DashboardList/Header.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Button, PageTitle } from 'UI'; +import Select from 'Shared/Select'; +import DashboardSearch from './DashboardSearch'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; +import { withSiteId } from 'App/routes'; + +function Header({ history, siteId }: { history: any; siteId: string }) { + const { dashboardStore } = useStore(); + const sort = useObserver(() => dashboardStore.sort); + + const onAddDashboardClick = () => { + dashboardStore.initDashboard(); + dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => { + dashboardStore.selectDashboardById(syncedDashboard.dashboardId); + history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId)); + }); + }; + + return ( +
+
+ +
+
+ +
+ metricStore.updateKey('sort', { by: value.value })} + /> +
+
+ +
+
+
+
+ + Create custom Metrics to capture key interactions and track KPIs. +
+
+ ); +} + +export default MetricViewHeader; diff --git a/frontend/app/components/Dashboard/components/MetricViewHeader/index.ts b/frontend/app/components/Dashboard/components/MetricViewHeader/index.ts new file mode 100644 index 000000000..fd6048fc9 --- /dev/null +++ b/frontend/app/components/Dashboard/components/MetricViewHeader/index.ts @@ -0,0 +1 @@ +export { default } from './MetricViewHeader'; diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx new file mode 100644 index 000000000..f6df7a81f --- /dev/null +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -0,0 +1,13 @@ +import Modal from 'App/components/Modal/Modal'; +import React from 'react'; + +function MetricsLibraryModal() { + return ( + <> + + Hello + + ); +} + +export default MetricsLibraryModal; diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/index.ts b/frontend/app/components/Dashboard/components/MetricsLibraryModal/index.ts new file mode 100644 index 000000000..f217fc7c3 --- /dev/null +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/index.ts @@ -0,0 +1 @@ +export { default } from './MetricsLibraryModal'; diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 2869f5240..b3880dbe2 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -1,6 +1,6 @@ import { observer } from 'mobx-react-lite'; -import React, { useEffect } from 'react'; -import { NoContent, Pagination, Icon } from 'UI'; +import React, { useEffect, useState } from 'react'; +import { NoContent, Pagination, Icon, Checkbox } from 'UI'; import { useStore } from 'App/mstore'; import { filterList } from 'App/utils'; import MetricListItem from '../MetricListItem'; @@ -11,6 +11,16 @@ function MetricsList({ siteId }: { siteId: string }) { const { metricStore } = useStore(); const metrics = metricStore.sortedWidgets; const metricsSearch = metricStore.metricsSearch; + const [selectedMetrics, setSelectedMetrics] = useState([]); + + const toggleMetricSelection = (id: any) => { + console.log('id', id); + if (selectedMetrics.includes(id)) { + selectedMetrics.splice(selectedMetrics.indexOf(id), 1); + } else { + selectedMetrics.push(id); + } + }; const filterByDashboard = (item: Widget, searchRE: RegExp) => { const dashboardsStr = item.dashboards.map((d: any) => d.name).join(' '); @@ -40,7 +50,16 @@ function MetricsList({ siteId }: { siteId: string }) { >
-
Title
+
+ setSelectedMetrics(list.map((i: any) => i.metricId))} + /> + Title +
Owner
Visibility
Last Modified
@@ -48,7 +67,15 @@ function MetricsList({ siteId }: { siteId: string }) { {sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => ( - + { + e.stopPropagation(); + toggleMetricSelection(parseInt(metric.metricId)); + }} + /> ))}
diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 6c39114cd..929868550 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -1,40 +1,26 @@ import React from 'react'; -import { Button, PageTitle, Icon, Link } from 'UI'; import withPageTitle from 'HOCs/withPageTitle'; import MetricsList from '../MetricsList'; -import MetricsSearch from '../MetricsSearch'; import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; +import MetricViewHeader from '../MetricViewHeader'; interface Props { - siteId: string; + siteId: string; } function MetricsView({ siteId }: Props) { - const { metricStore } = useStore(); + const { metricStore } = useStore(); - React.useEffect(() => { - metricStore.fetchList(); - }, []); - return useObserver(() => ( -
-
-
- -
-
- -
- -
-
-
-
- - Create custom Metrics to capture user frustrations, monitor your app's performance and track other KPIs. -
- -
- )); + React.useEffect(() => { + metricStore.fetchList(); + }, []); + + return useObserver(() => ( +
+ + +
+ )); } -export default withPageTitle('Metrics - OpenReplay')(MetricsView); +export default withPageTitle('Cards - OpenReplay')(MetricsView); diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index cc1e79e71..52f3d3cfe 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -1,9 +1,18 @@ import React, { useEffect } from 'react'; import ReactDOM from 'react-dom'; import ModalOverlay from './ModalOverlay'; +import cn from 'classnames'; import { useHistory } from 'react-router'; -export default function Modal({ component, props, hideModal }: any) { +const DEFAULT_WIDTH = 350; +interface Props { + component: any; + className?: string; + props: any; + hideModal?: boolean; + width?: number; +} +function Modal({ component, className = 'bg-white', props, hideModal }: Props) { const history = useHistory(); useEffect(() => { @@ -12,11 +21,15 @@ export default function Modal({ component, props, hideModal }: any) { document.querySelector('body').style.overflow = 'visible'; } }); - }); - return component ? ( + });return component ? ( ReactDOM.createPortal( - {component} +
+ {component} +
, document.querySelector('#modal-root') ) @@ -24,3 +37,17 @@ export default function Modal({ component, props, hideModal }: any) { <> ); } + +Modal.Header = ({ title }: { title: string }) => { + return ( +
+
{title}
+
+ ); +}; + +Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => { + return
{children}
; +}; + +export default Modal; diff --git a/frontend/app/components/Modal/index.tsx b/frontend/app/components/Modal/index.tsx index 860063c77..3df7a98cb 100644 --- a/frontend/app/components/Modal/index.tsx +++ b/frontend/app/components/Modal/index.tsx @@ -3,60 +3,59 @@ import React, { Component, createContext } from 'react'; import Modal from './Modal'; const ModalContext = createContext({ - component: null, - props: { - right: true, - onClose: () => {}, - }, - showModal: (component: any, props: any) => {}, - hideModal: () => {}, + component: null, + props: { + right: true, + onClose: () => {}, + }, + showModal: (component: any, props: any) => {}, + hideModal: () => {}, }); export class ModalProvider extends Component { - handleKeyDown = (e: any) => { - if (e.keyCode === 27) { - this.hideModal(); - } - }; - - showModal = (component, props = { right: true }) => { - this.setState({ - component, - props, - }); - document.addEventListener('keydown', this.handleKeyDown); - document.querySelector('body').style.overflow = 'hidden'; - }; - - hideModal = () => { - document.removeEventListener('keydown', this.handleKeyDown); - document.querySelector('body').style.overflow = 'visible'; - const { props } = this.state; - if (props.onClose) { - props.onClose(); - } - this.setState({ - component: null, - props: {}, - }); - }; - - state = { - component: null, - get isModalActive() { return this.component !== null }, - props: {}, - showModal: this.showModal, - hideModal: this.hideModal, - }; - - render() { - return ( - - - {this.props.children} - - ); + handleKeyDown = (e: any) => { + if (e.keyCode === 27) { + this.hideModal(); } + }; + + showModal = (component, props = { right: true }) => { + this.setState({ + component, + props, + }); + document.addEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'hidden'; + }; + + hideModal = () => { + document.removeEventListener('keydown', this.handleKeyDown); + document.querySelector('body').style.overflow = 'visible'; + const { props } = this.state; + if (props.onClose) { + props.onClose(); + } + this.setState({ + component: null, + props: {}, + }); + }; + + state = { + component: null, + get isModalActive() { return this.component !== null },props: {}, + showModal: this.showModal, + hideModal: this.hideModal, + }; + + render() { + return ( + + + {this.props.children} + + ); + } } export const ModalConsumer = ModalContext.Consumer; diff --git a/frontend/app/components/Modal/withModal.tsx b/frontend/app/components/Modal/withModal.tsx index ec5030e54..9c7319eed 100644 --- a/frontend/app/components/Modal/withModal.tsx +++ b/frontend/app/components/Modal/withModal.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { ModalConsumer } from './'; - -export default BaseComponent => React.memo(props => ( - - { value => } - -)); \ No newline at end of file +export default (BaseComponent) => + React.memo((props) => ( + {(value) => } + )); diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index c474ba326..bf3a970d8 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -119,7 +119,7 @@ export default class DashboardStore { return this.dashboards.filter((d) => ids.includes(d.dashboardId)); } - initDashboard(dashboard: Dashboard) { + initDashboard(dashboard?: Dashboard) { this.dashboardInstance = dashboard ? new Dashboard().fromJson(dashboard) : new Dashboard(); diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 11bcf8894..04a706d32 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -1,153 +1,164 @@ -import { makeAutoObservable, computed } from "mobx" -import Widget from "./types/widget"; -import { metricService, errorService } from "App/services"; +import { makeAutoObservable, computed } from 'mobx'; +import Widget from './types/widget'; +import { metricService, errorService } from 'App/services'; import { toast } from 'react-toastify'; -import Error from "./types/error"; +import Error from './types/error'; export default class MetricStore { - isLoading: boolean = false - isSaving: boolean = false + isLoading: boolean = false; + isSaving: boolean = false; - metrics: Widget[] = [] - instance = new Widget() + metrics: Widget[] = []; + instance = new Widget(); - page: number = 1 - pageSize: number = 10 - metricsSearch: string = "" - sort: any = {} + page: number = 1; + pageSize: number = 10; + metricsSearch: string = ''; + sort: any = { by: 'desc' }; - sessionsPage: number = 1 - sessionsPageSize: number = 10 + sessionsPage: number = 1; + sessionsPageSize: number = 10; - constructor() { - makeAutoObservable(this) + constructor() { + makeAutoObservable(this); + } + + @computed + get sortedWidgets() { + return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified); + } + + // State Actions + init(metric?: Widget | null) { + this.instance.update(metric || new Widget()); + } + + updateKey(key: string, value: any) { + // @ts-ignore + this[key] = value; + } + + merge(object: any) { + Object.assign(this.instance, object); + this.instance.updateKey('hasChanged', true); + } + + reset(id: string) { + const metric = this.findById(id); + if (metric) { + this.instance = metric; } + } - @computed - get sortedWidgets() { - return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified) + addToList(metric: Widget) { + this.metrics.push(metric); + } + + updateInList(metric: Widget) { + // @ts-ignore + const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY]); + if (index >= 0) { + this.metrics[index] = metric; } + } - // State Actions - init(metric?: Widget | null) { - this.instance.update(metric || new Widget()) - } + findById(id: string) { + // @ts-ignore + return this.metrics.find((m) => m[Widget.ID_KEY] === id); + } - updateKey(key: string, value: any) { - // @ts-ignore - this[key] = value - } + removeById(id: string): void { + // @ts-ignore + this.metrics = this.metrics.filter((m) => m[Widget.ID_KEY] !== id); + } - merge(object: any) { - Object.assign(this.instance, object) - this.instance.updateKey('hasChanged', true) - } + get paginatedList(): Widget[] { + const start = (this.page - 1) * this.pageSize; + const end = start + this.pageSize; + return this.metrics.slice(start, end); + } - reset(id: string) { - const metric = this.findById(id) - if (metric) { - this.instance = metric - } - } - - addToList(metric: Widget) { - this.metrics.push(metric) - } - - updateInList(metric: Widget) { - // @ts-ignore - const index = this.metrics.findIndex((m: Widget) => m[Widget.ID_KEY] === metric[Widget.ID_KEY]) - if (index >= 0) { - this.metrics[index] = metric - } - } - - findById(id: string) { - // @ts-ignore - return this.metrics.find(m => m[Widget.ID_KEY] === id) - } - - removeById(id: string): void { - // @ts-ignore - this.metrics = this.metrics.filter(m => m[Widget.ID_KEY] !== id) - } - - get paginatedList(): Widget[] { - const start = (this.page - 1) * this.pageSize - const end = start + this.pageSize - return this.metrics.slice(start, end) - } - - // API Communication - save(metric: Widget, dashboardId?: string): Promise { - const wasCreating = !metric.exists() - this.isSaving = true - return new Promise((resolve, reject) => { - metricService.saveMetric(metric, dashboardId) - .then((metric: any) => { - const _metric = new Widget().fromJson(metric) - if (wasCreating) { - toast.success('Metric created successfully') - this.addToList(_metric) - this.instance = _metric - } else { - toast.success('Metric updated successfully') - this.updateInList(_metric) - } - resolve(_metric) - }).catch(() => { - toast.error('Error saving metric') - reject() - }).finally(() => { - this.instance.updateKey('hasChanged', false) - this.isSaving = false - }) + // API Communication + save(metric: Widget, dashboardId?: string): Promise { + const wasCreating = !metric.exists(); + this.isSaving = true; + return new Promise((resolve, reject) => { + metricService + .saveMetric(metric, dashboardId) + .then((metric: any) => { + const _metric = new Widget().fromJson(metric); + if (wasCreating) { + toast.success('Metric created successfully'); + this.addToList(_metric); + this.instance = _metric; + } else { + toast.success('Metric updated successfully'); + this.updateInList(_metric); + } + resolve(_metric); }) - } - - fetchList() { - this.isLoading = true - return metricService.getMetrics() - .then((metrics: any[]) => { - this.metrics = metrics.map(m => new Widget().fromJson(m)) - }).finally(() => { - this.isLoading = false - }) - } - - fetch(id: string, period?: any) { - this.isLoading = true - return metricService.getMetric(id) - .then((metric: any) => { - return this.instance = new Widget().fromJson(metric, period) - }) - .finally(() => { - this.isLoading = false - }) - } - - delete(metric: Widget) { - this.isSaving = true - // @ts-ignore - return metricService.deleteMetric(metric[Widget.ID_KEY]) - .then(() => { - // @ts-ignore - this.removeById(metric[Widget.ID_KEY]) - toast.success('Metric deleted successfully') - }).finally(() => { - this.instance.updateKey('hasChanged', false) - this.isSaving = false - }) - } - - fetchError(errorId: any): Promise { - return new Promise((resolve, reject) => { - errorService.one(errorId).then((error: any) => { - resolve(new Error().fromJSON(error)) - }).catch((error: any) => { - toast.error('Failed to fetch error details.') - reject(error) - }) + .catch(() => { + toast.error('Error saving metric'); + reject(); }) - } + .finally(() => { + this.instance.updateKey('hasChanged', false); + this.isSaving = false; + }); + }); + } + + fetchList() { + this.isLoading = true; + return metricService + .getMetrics() + .then((metrics: any[]) => { + this.metrics = metrics.map((m) => new Widget().fromJson(m)); + }) + .finally(() => { + this.isLoading = false; + }); + } + + fetch(id: string, period?: any) { + this.isLoading = true; + return metricService + .getMetric(id) + .then((metric: any) => { + return (this.instance = new Widget().fromJson(metric, period)); + }) + .finally(() => { + this.isLoading = false; + }); + } + + delete(metric: Widget) { + this.isSaving = true; + // @ts-ignore + return metricService + .deleteMetric(metric[Widget.ID_KEY]) + .then(() => { + // @ts-ignore + this.removeById(metric[Widget.ID_KEY]); + toast.success('Metric deleted successfully'); + }) + .finally(() => { + this.instance.updateKey('hasChanged', false); + this.isSaving = false; + }); + } + + fetchError(errorId: any): Promise { + return new Promise((resolve, reject) => { + errorService + .one(errorId) + .then((error: any) => { + resolve(new Error().fromJSON(error)); + }) + .catch((error: any) => { + toast.error('Failed to fetch error details.'); + reject(error); + }); + }); + } } From 1974a9b0d94aff3b2fb2c2086eab0373d89d8d8e Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Tue, 18 Oct 2022 16:07:28 +0200 Subject: [PATCH 05/56] change(ui) - added storybook and updated to support webpack --- frontend/.babelrc | 1 + frontend/.storybook/config.DEPRECATED.js | 33 ----------------- frontend/.storybook/main.ts | 36 +++++++++++++++++++ frontend/.storybook/openReplayDecorator.js | 13 +++++++ frontend/.storybook/preview.ts | 13 +++++++ frontend/.storybook/webpack.config.js | 18 ++-------- .../shared/DateRangeDropdown/index.js | 2 +- frontend/package.json | 16 +++++++-- .../Introduction.stories.mdx} | 0 9 files changed, 81 insertions(+), 51 deletions(-) delete mode 100644 frontend/.storybook/config.DEPRECATED.js create mode 100644 frontend/.storybook/main.ts create mode 100644 frontend/.storybook/openReplayDecorator.js create mode 100644 frontend/.storybook/preview.ts rename frontend/{.storybook/preview-head.html => stories/Introduction.stories.mdx} (100%) diff --git a/frontend/.babelrc b/frontend/.babelrc index 8c99ee5a4..631979df1 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -5,6 +5,7 @@ "@babel/preset-typescript" ], "plugins": [ + "babel-plugin-react-require", [ "@babel/plugin-proposal-private-property-in-object", { "loose": true } ], [ "@babel/plugin-transform-runtime", { "regenerator": true } ], [ "@babel/plugin-proposal-decorators", { "legacy":true } ], diff --git a/frontend/.storybook/config.DEPRECATED.js b/frontend/.storybook/config.DEPRECATED.js deleted file mode 100644 index fad172b6f..000000000 --- a/frontend/.storybook/config.DEPRECATED.js +++ /dev/null @@ -1,33 +0,0 @@ -import { configure, addDecorator } from '@storybook/react'; -import { Provider } from 'react-redux'; -import store from '../app/store'; -import { MemoryRouter } from "react-router" - -const withProvider = (story) => ( - - { story() } - -) - -// const req = require.context('../app/components/ui', true, /\.stories\.js$/); -// const issues = require.context('../app/components/Session/Issues', true, /\.stories\.js$/); -// const bugFinder = require.context('../app/components/BugFinder', true, /\.stories\.js$/); - -addDecorator(withProvider); -addDecorator(story => {story()}); - -// function loadStories() { -// req.keys().forEach(filename => req(filename)); -// bugFinder.keys().forEach(filename => bugFinder(filename)); -// } - -// configure(loadStories, module); - - -configure( - [ - // require.context('../app', true, /\.stories\.mdx$/), - require.context('../app', true, /\.stories\.js$/), - ], - module -); diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 000000000..adf1c0504 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,36 @@ +import custom from '../webpack.config'; + +export default { + stories: ['../app/components/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + ], + framework: '@storybook/react', + core: { + builder: '@storybook/builder-webpack5', + }, + reactOptions: { + fastRefresh: true, + }, + webpackFinal: async (config: any) => { + // console.log('CONFIG', config); + // config.plugins.push(...); + config.module = custom.module; + config.resolve = custom.resolve; + config.plugins.unshift(custom.plugins[0]); + config.plugins.unshift(custom.plugins[1]); + config.plugins.unshift(custom.plugins[4]); + config.module.rules.unshift({ + test: /\.(svg)$/i, + exclude: /node_modules/, + use: [ + { + loader: 'file-loader', + }, + ], + }); + return config; + }, +}; diff --git a/frontend/.storybook/openReplayDecorator.js b/frontend/.storybook/openReplayDecorator.js new file mode 100644 index 000000000..730eaf636 --- /dev/null +++ b/frontend/.storybook/openReplayDecorator.js @@ -0,0 +1,13 @@ +import { Provider } from 'react-redux'; +import store from '../app/store'; +import { StoreProvider, RootStore } from '../app/mstore'; + +const withProvider = (Story) => ( + + + + + +); + +export default withProvider; diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts new file mode 100644 index 000000000..0334feb28 --- /dev/null +++ b/frontend/.storybook/preview.ts @@ -0,0 +1,13 @@ +import openReplayProvider from './openReplayDecorator'; +import '../app/styles/index.scss'; + +export default { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +}; +export const decorators = [openReplayProvider] \ No newline at end of file diff --git a/frontend/.storybook/webpack.config.js b/frontend/.storybook/webpack.config.js index 1a123fa52..ff2dbcee8 100644 --- a/frontend/.storybook/webpack.config.js +++ b/frontend/.storybook/webpack.config.js @@ -1,15 +1,3 @@ -const pathAlias = require('../path-alias'); -const mainConfig = require('../webpack.config.js'); - -module.exports = async ({ config }) => { - var conf = mainConfig(); - config.resolve.alias = Object.assign(conf.resolve.alias, config.resolve.alias); // Path Alias - config.resolve.extensions = conf.resolve.extensions - config.module.rules = conf.module.rules; - config.module.rules[0].use[0] = 'style-loader'; // instead of separated css - config.module.rules[1].use[0] = 'style-loader'; - config.plugins.push(conf.plugins[0]); // global React - config.plugins.push(conf.plugins[5]); - config.entry = config.entry.concat(conf.entry.slice(2)) // CSS entries - return config; -}; +export default { + +} \ No newline at end of file diff --git a/frontend/app/components/shared/DateRangeDropdown/index.js b/frontend/app/components/shared/DateRangeDropdown/index.js index d6e6f2f87..d5948cff3 100644 --- a/frontend/app/components/shared/DateRangeDropdown/index.js +++ b/frontend/app/components/shared/DateRangeDropdown/index.js @@ -1 +1 @@ -export { default } from './DateRangeDropdown'; +// export { default } from './DateRangeDropdown'; diff --git a/frontend/package.json b/frontend/package.json index 91d8a61f7..30c2f18e2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,9 +12,10 @@ "gen:constants": "node ./scripts/constants.js", "gen:icons": "node ./scripts/icons.ts", "gen:colors": "node ./scripts/colors.js", - "storybook": "start-storybook", + "storybook": "start-storybook -p 6006", "flow": "flow", - "postinstall": "yarn gen:icons && yarn gen:colors" + "postinstall": "yarn gen:icons && yarn gen:colors", + "build-storybook": "build-storybook" }, "dependencies": { "@floating-ui/react-dom-interactions": "^0.10.3", @@ -84,7 +85,17 @@ "@babel/preset-react": "^7.17.12", "@babel/preset-typescript": "^7.17.12", "@babel/runtime": "^7.17.9", + "@mdx-js/react": "^1.6.22", "@openreplay/sourcemap-uploader": "^3.0.0", + "@storybook/addon-actions": "^6.5.12", + "@storybook/addon-docs": "^6.5.12", + "@storybook/addon-essentials": "^6.5.12", + "@storybook/addon-interactions": "^6.5.12", + "@storybook/addon-links": "^6.5.12", + "@storybook/builder-webpack5": "^6.5.12", + "@storybook/manager-webpack5": "^6.5.12", + "@storybook/react": "^6.5.12", + "@storybook/testing-library": "^0.0.13", "@types/luxon": "^3.0.0", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.4", @@ -95,6 +106,7 @@ "@typescript-eslint/parser": "^5.24.0", "autoprefixer": "^10.4.7", "babel-loader": "^8.2.4", + "babel-plugin-react-require": "^3.1.3", "babel-plugin-recharts": "^1.2.1", "babel-plugin-transform-decorators-legacy": "^1.3.5", "compression-webpack-plugin": "^10.0.0", diff --git a/frontend/.storybook/preview-head.html b/frontend/stories/Introduction.stories.mdx similarity index 100% rename from frontend/.storybook/preview-head.html rename to frontend/stories/Introduction.stories.mdx From 8e9a09d6d352494b0df7573611a6364c5b9fb5ec Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 19 Oct 2022 11:45:03 +0200 Subject: [PATCH 06/56] feat(ui) - analytics sample charts --- .../SankeyChart/SankeyChart.stories.tsx | 20 +++ .../Insights/SankeyChart/SankeyChart.tsx | 121 ++++++++++++++++++ .../shared/Insights/SankeyChart/index.ts | 1 + .../ScatterChart/ScatterChart.stories.tsx | 23 ++++ .../Insights/ScatterChart/ScatterChart.tsx | 45 +++++++ .../shared/Insights/ScatterChart/index.ts | 1 + 6 files changed, 211 insertions(+) create mode 100644 frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx create mode 100644 frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx create mode 100644 frontend/app/components/shared/Insights/SankeyChart/index.ts create mode 100644 frontend/app/components/shared/Insights/ScatterChart/ScatterChart.stories.tsx create mode 100644 frontend/app/components/shared/Insights/ScatterChart/ScatterChart.tsx create mode 100644 frontend/app/components/shared/Insights/ScatterChart/index.ts diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx new file mode 100644 index 000000000..f0e3ed21e --- /dev/null +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx @@ -0,0 +1,20 @@ +import { storiesOf } from '@storybook/react'; +import SankeyChart from './SankeyChart'; + +const data = { + nodes: [ + { name: 'Home Page' }, + { name: 'Dashboard' }, + { name: 'Preferences' }, + { name: 'Billing' }, + + ], + links: [ + { source: 0, target: 1, value: 100 }, + { source: 1, target: 2, value: 50 }, + { source: 1, target: 3, value: 50 }, + { source: 2, target: 3, value: 10 }, + ], +}; + +storiesOf('SankeyChart', module).add('Pure', () => ); diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx new file mode 100644 index 000000000..c09888102 --- /dev/null +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { Sankey, Tooltip, Rectangle, Layer, ResponsiveContainer } from 'recharts'; + +interface Props { + data: any; +} +function SankeyChart(props: Props) { + const { data } = props; + return ( +
+
Sankey Chart
+
+ + } + nodePadding={50} + nodeWidth={10} + margin={{ + left: 10, + right: 100, + top: 10, + bottom: 10, + }} + link={} + > + + + + + + + }/> + + +
+
+ ); +} + +export default SankeyChart; + +const CustomTooltip = (props: any) => { + console.log('props', props); + 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/Insights/SankeyChart/index.ts b/frontend/app/components/shared/Insights/SankeyChart/index.ts new file mode 100644 index 000000000..d651de3d0 --- /dev/null +++ b/frontend/app/components/shared/Insights/SankeyChart/index.ts @@ -0,0 +1 @@ +export { default } from './SankeyChart'; diff --git a/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.stories.tsx b/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.stories.tsx new file mode 100644 index 000000000..a56381cf7 --- /dev/null +++ b/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.stories.tsx @@ -0,0 +1,23 @@ +import { storiesOf } from '@storybook/react'; +import ScatterChart from './ScatterChart'; + +const data01 = [ + { x: 100, y: 200, z: 200 }, + { x: 120, y: 100, z: 260 }, + { x: 170, y: 300, z: 400 }, + { x: 140, y: 250, z: 280 }, + { x: 150, y: 400, z: 500 }, + { x: 110, y: 280, z: 200 }, +]; +const data02 = [ + { x: 200, y: 260, z: 240 }, + { x: 240, y: 290, z: 220 }, + { x: 190, y: 290, z: 250 }, + { x: 198, y: 250, z: 210 }, + { x: 180, y: 280, z: 260 }, + { x: 210, y: 220, z: 230 }, +]; + +storiesOf('ScatterChart', module).add('Pure', () => ( + +)); diff --git a/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.tsx b/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.tsx new file mode 100644 index 000000000..3d0566b4e --- /dev/null +++ b/frontend/app/components/shared/Insights/ScatterChart/ScatterChart.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { + ScatterChart, + Scatter, + Tooltip, + CartesianGrid, + XAxis, + YAxis, + ZAxis, + Legend, + ResponsiveContainer, +} from 'recharts'; + +interface Props { + dataFirst: any; + dataSecond: any; +} +function ScatterChartComponent(props: Props) { + const { dataFirst, dataSecond } = props; + return ( +
+
Scatter Chart
+
+ + + + + + + + + + + + +
+
+ ); +} + +export default ScatterChartComponent; diff --git a/frontend/app/components/shared/Insights/ScatterChart/index.ts b/frontend/app/components/shared/Insights/ScatterChart/index.ts new file mode 100644 index 000000000..26b894075 --- /dev/null +++ b/frontend/app/components/shared/Insights/ScatterChart/index.ts @@ -0,0 +1 @@ +export { default } from './ScatterChart'; From 7bd6ee306511c4a667db13cd388c01062540ca91 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Wed, 7 Dec 2022 14:37:34 +0530 Subject: [PATCH 07/56] feat(ui) - dashboard improvements - wip --- .../components/AddCardModal/AddCardModal.tsx | 8 +- .../DashboardHeader/DashboardHeader.tsx | 8 +- .../DashboardSideMenu/DashboardSideMenu.tsx | 2 +- .../DashboardWidgetGrid.tsx | 2 +- .../MetricTypeItem/MetricTypeItem.tsx | 2 +- .../MetricTypeList/MetricTypeList.tsx | 93 ++------ .../MetricsLibraryModal.tsx | 58 ++++- .../components/MetricsList/MetricsList.tsx | 29 ++- .../components/MetricsView/MetricsView.tsx | 9 +- .../components/WidgetForm/WidgetForm.tsx | 24 +- .../MetricSubtypeDropdown.tsx | 59 +++++ .../components/MetricSubtypeDropdown/index.ts | 1 + .../MetricTypeDropdown.stories.tsx | 11 + .../MetricTypeDropdown/MetricTypeDropdown.tsx | 54 +++++ .../components/MetricTypeDropdown/index.ts | 1 + .../components/WidgetView/WidgetView.tsx | 2 +- frontend/app/components/Modal/Modal.tsx | 2 +- .../CustomDropdownOption.tsx | 38 ++++ .../shared/CustomDropdownOption/index.ts | 1 + .../SankeyChart/SankeyChart.stories.tsx | 18 +- .../Insights/SankeyChart/SankeyChart.tsx | 31 ++- frontend/app/constants/card.ts | 213 ++++++++++++++++++ frontend/app/mstore/dashboardStore.ts | 4 +- frontend/app/types/filter/filterType.ts | 54 +++++ 24 files changed, 591 insertions(+), 133 deletions(-) create mode 100644 frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx create mode 100644 frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/index.ts create mode 100644 frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.stories.tsx create mode 100644 frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx create mode 100644 frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/index.ts create mode 100644 frontend/app/components/shared/CustomDropdownOption/CustomDropdownOption.tsx create mode 100644 frontend/app/components/shared/CustomDropdownOption/index.ts create mode 100644 frontend/app/constants/card.ts diff --git a/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx b/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx index 582e5b2f3..0558a99f6 100644 --- a/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx +++ b/frontend/app/components/Dashboard/components/AddCardModal/AddCardModal.tsx @@ -2,12 +2,16 @@ import Modal from 'App/components/Modal/Modal'; import React from 'react'; import MetricTypeList from '../MetricTypeList'; -function AddCardModal() { +interface Props { + siteId: string; + dashboardId: string; +} +function AddCardModal(props: Props) { return ( <> - + ); diff --git a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx index 0ce2103eb..25ff80e81 100644 --- a/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx +++ b/frontend/app/components/Dashboard/components/DashboardHeader/DashboardHeader.tsx @@ -14,6 +14,7 @@ import DashboardEditModal from '../DashboardEditModal'; import AddCardModal from '../AddCardModal'; interface IProps { + dashboardId: string; siteId: string; renderReport?: any; } @@ -21,10 +22,9 @@ interface IProps { type Props = IProps & RouteComponentProps; function DashboardHeader(props: Props) { - const { siteId } = props; + const { siteId, dashboardId } = props; const { dashboardStore } = useStore(); const { showModal } = useModal(); - // const [showTooltip, setShowTooltip] = React.useState(false); const [focusTitle, setFocusedInput] = React.useState(true); const [showEditModal, setShowEditModal] = React.useState(false); const period = dashboardStore.period; @@ -82,7 +82,9 @@ function DashboardHeader(props: Props) {
- +
} diff --git a/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx b/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx index 380472bde..8b77fc18e 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeItem/MetricTypeItem.tsx @@ -4,7 +4,7 @@ import { Icon } from 'UI'; export interface MetricType { title: string; - icon: IconNames; + icon?: IconNames; description: string; slug: string; } diff --git a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx index d5cea3be5..e0d6e846b 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx @@ -2,90 +2,37 @@ import { useModal } from 'App/components/Modal'; import React from 'react'; import MetricsLibraryModal from '../MetricsLibraryModal'; import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem'; +import { TYPES, LIBRARY } from 'App/constants/card'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { dashboardMetricCreate, withSiteId } from 'App/routes'; -const METRIC_TYPES: MetricType[] = [ - { - title: 'Add From Library', - icon: 'grid', - description: 'Select a pre existing card from card library', - slug: 'library', - }, - { - title: 'Timeseries', - icon: 'graph-up', - description: 'Trend of sessions count in over the time.', - slug: 'timeseries', - }, - { - title: 'Table', - icon: 'list-alt', - description: 'See list of Users, Sessions, Errors, Issues, etc.,', - slug: 'table', - }, - { - title: 'Funnel', - icon: 'funnel', - description: 'Uncover the issues impacting user journeys.', - slug: 'funnel', - }, - { - title: 'Errors Tracking', - icon: 'exclamation-circle', - description: 'Discover user journeys between 2 points.', - slug: 'errors', - }, - { - title: 'Performance Monitoring', - icon: 'speedometer2', - description: 'Retention graph of users / features over a period of time.', - slug: 'performance', - }, - { - title: 'Resource Monitoring', - icon: 'files', - description: 'Find the adoption of your all features in your app.', - slug: 'resource-monitoring', - }, - { - title: 'Web Vitals', - icon: 'activity', - description: 'Find the adoption of your all features in your app.', - slug: 'web-vitals', - }, - { - title: 'User Path', - icon: 'signpost-split', - description: 'Discover user journeys between 2 points.', - slug: 'user-path', - }, - { - title: 'Retention', - icon: 'arrow-repeat', - description: 'Retension graph of users / features over a period of time.', - slug: 'retention', - }, - { - title: 'Feature Adoption', - icon: 'card-checklist', - description: 'Find the adoption of your all features in your app.', - slug: 'feature-adoption', - }, -]; +interface Props extends RouteComponentProps { + dashboardId: number; + siteId: string; +} +function MetricTypeList(props: Props) { + const { dashboardId, siteId, history } = props; + const { hideModal } = useModal(); -function MetricTypeList() { const { showModal } = useModal(); const onClick = ({ slug }: MetricType) => { - if (slug === 'library') { - showModal(, { right: true, width: 700 }); + hideModal(); + if (slug === LIBRARY) { + return showModal(, { right: true, width: 800 }); } + + // TODO redirect to card builder with metricType query param + const path = withSiteId(dashboardMetricCreate(dashboardId + ''), siteId); + history.push(path); }; + return ( <> - {METRIC_TYPES.map((metric: MetricType) => ( + {TYPES.map((metric: MetricType) => ( onClick(metric)} /> ))} ); } -export default MetricTypeList; +export default withRouter(MetricTypeList); diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index f6df7a81f..308924a12 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -1,13 +1,65 @@ import Modal from 'App/components/Modal/Modal'; -import React from 'react'; +import React, { useMemo, useState } from 'react'; +import MetricsList from '../MetricsList'; +import { Button } from 'UI'; +import { useModal } from 'App/components/Modal'; +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; + +interface Props { + dashboardId: number; + siteId: string; +} +function MetricsLibraryModal(props: Props) { + const { siteId, dashboardId } = props; + const [selectedList, setSelectedList] = useState([]); + + console.log('dashboardId', dashboardId); + + const onSelectionChange = (list: any) => { + setSelectedList(list); + }; -function MetricsLibraryModal() { return ( <> - Hello + +
+ +
+ {/* TODO should show the dynamic values */} + +
); } export default MetricsLibraryModal; + +function SelectedContent({ dashboardId, selected }: any) { + const { hideModal } = useModal(); + const { metricStore, dashboardStore } = useStore(); + const total = useObserver(() => metricStore.sortedWidgets.length); + const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]); + + const addSelectedToDashboard = () => { + dashboardStore.addWidgetToDashboard(dashboard, selected).then(hideModal); + }; + + return ( +
+
+ Selected {selected.length} of{' '} + {total} +
+
+ + +
+
+ ); +} diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index b3880dbe2..3f2ca9c3c 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -7,18 +7,31 @@ import MetricListItem from '../MetricListItem'; import { sliceListPerPage } from 'App/utils'; import Widget from 'App/mstore/types/widget'; -function MetricsList({ siteId }: { siteId: string }) { +function MetricsList({ + siteId, + onSelectionChange = () => {}, +}: { + siteId: string; + onSelectionChange?: (selected: any[]) => void; +}) { const { metricStore } = useStore(); const metrics = metricStore.sortedWidgets; const metricsSearch = metricStore.metricsSearch; - const [selectedMetrics, setSelectedMetrics] = useState([]); + const [selectedMetrics, setSelectedMetrics] = useState([]); + + useEffect(() => { + metricStore.fetchList(); + }, []); + + useEffect(() => { + onSelectionChange(selectedMetrics); + }, [selectedMetrics]); const toggleMetricSelection = (id: any) => { - console.log('id', id); if (selectedMetrics.includes(id)) { - selectedMetrics.splice(selectedMetrics.indexOf(id), 1); + setSelectedMetrics(selectedMetrics.filter((i: number) => i !== id)); } else { - selectedMetrics.push(id); + setSelectedMetrics([...selectedMetrics, id]); } }; @@ -48,7 +61,7 @@ function MetricsList({ siteId }: { siteId: string }) { } > -
+
{ e.stopPropagation(); toggleMetricSelection(parseInt(metric.metricId)); @@ -80,7 +93,7 @@ function MetricsList({ siteId }: { siteId: string }) { ))}
-
+
Showing{' '} {Math.min(list.length, metricStore.pageSize)} out diff --git a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx index 929868550..a3621291b 100644 --- a/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsView/MetricsView.tsx @@ -1,7 +1,6 @@ import React from 'react'; import withPageTitle from 'HOCs/withPageTitle'; import MetricsList from '../MetricsList'; -import { useStore } from 'App/mstore'; import { useObserver } from 'mobx-react-lite'; import MetricViewHeader from '../MetricViewHeader'; @@ -9,14 +8,8 @@ interface Props { siteId: string; } function MetricsView({ siteId }: Props) { - const { metricStore } = useStore(); - - React.useEffect(() => { - metricStore.fetchList(); - }, []); - return useObserver(() => ( -
+
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index bf0ccda20..be1d80eef 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -8,6 +8,8 @@ import FilterSeries from '../FilterSeries'; import { confirm, Tooltip } from 'UI'; import Select from 'Shared/Select' import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes' +import MetricTypeDropdown from './components/MetricTypeDropdown'; +import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; interface Props { history: any; @@ -103,18 +105,10 @@ function WidgetForm(props: Props) {
- i.value === metric.metricType) || metricTypes[0]} - // @ts-ignore - list={metricTypes.map((i) => ({ value: i.value, name: i.label, icon: metricIcons[i.value] }))} - /> + + - {metric.metricType === 'timeseries' && ( + {/* {metric.metricType === 'timeseries' && ( <> of - )} + )} */} {metric.metricOf === FilterKey.ISSUE && ( <> @@ -177,7 +171,7 @@ function WidgetForm(props: Props) { variant="text-primary" onClick={() => metric.addSeries()} disabled={!canAddSeries} - >Add Series + >ADD )}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx new file mode 100644 index 000000000..4e76c47de --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx @@ -0,0 +1,59 @@ +import { useStore } from 'App/mstore'; +import { useObserver } from 'mobx-react-lite'; +import { TYPES } from 'App/constants/card'; +import { MetricType } from 'App/components/Dashboard/components/MetricTypeItem/MetricTypeItem'; +import React from 'react'; +import Select from 'Shared/Select'; +import { components } from 'react-select'; +import CustomDropdownOption from 'Shared/CustomDropdownOption'; + +interface Props { + onSelect: any; +} +function MetricSubtypeDropdown(props: Props) { + const { metricStore } = useStore(); + const metric: any = useObserver(() => metricStore.instance); + + const options = React.useMemo(() => { + const type = TYPES.find((i: MetricType) => i.slug === metric.metricType); + if (type && type.subTypes) { + const options = type.subTypes.map((i: MetricType) => ({ + label: i.title, + icon: i.icon, + value: i.slug, + description: i.description, + })); + return options; + } + return false; + }, [metric.metricType]); + + return options ? ( + <> +
of
+ i.value === metric.metricType) || options[0]} + onChange={props.onSelect} + // onSelect={onSelect} + components={{ + MenuList: ({ children, ...props }: any) => { + return ( + + {children} + + ); + }, + Option: ({ children, ...props }: any) => { + const { data } = props; + return ; + }, + }} + /> + ); +} + +export default MetricTypeDropdown; diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/index.ts b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/index.ts new file mode 100644 index 000000000..ae7bd4cb1 --- /dev/null +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/index.ts @@ -0,0 +1 @@ +export { default } from './MetricTypeDropdown'; diff --git a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx index 3b8d8f246..3492e89c7 100644 --- a/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx +++ b/frontend/app/components/Dashboard/components/WidgetView/WidgetView.tsx @@ -100,7 +100,7 @@ function WidgetView(props: Props) {
setExpanded(!expanded)}>
- {expanded ? 'Close' : 'Edit'} + {expanded ? 'Collapse' : 'Edit'}
diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index 52f3d3cfe..ab4a5fa23 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -47,7 +47,7 @@ Modal.Header = ({ title }: { title: string }) => { }; Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => { - return
{children}
; + return
{children}
; }; export default Modal; diff --git a/frontend/app/components/shared/CustomDropdownOption/CustomDropdownOption.tsx b/frontend/app/components/shared/CustomDropdownOption/CustomDropdownOption.tsx new file mode 100644 index 000000000..4d8c6d2ad --- /dev/null +++ b/frontend/app/components/shared/CustomDropdownOption/CustomDropdownOption.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { components, OptionProps } from 'react-select'; +import { Icon } from 'UI'; +import cn from 'classnames'; + +export interface Props extends OptionProps { + icon?: string; + label: string; + description: string; +} +function CustomDropdownOption(props: Props) { + const { icon = '', label, description, isSelected, isFocused } = props; + return ( + +
+ {icon && ( + + )} +
+
{label}
+
{description}
+
+
+
+ ); +} + +export default CustomDropdownOption; diff --git a/frontend/app/components/shared/CustomDropdownOption/index.ts b/frontend/app/components/shared/CustomDropdownOption/index.ts new file mode 100644 index 000000000..240e1246d --- /dev/null +++ b/frontend/app/components/shared/CustomDropdownOption/index.ts @@ -0,0 +1 @@ +export { default } from './CustomDropdownOption'; diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx index f0e3ed21e..dfb9949f8 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.stories.tsx @@ -1,13 +1,13 @@ -import { storiesOf } from '@storybook/react'; -import SankeyChart from './SankeyChart'; +import SankeyChart, { SankeyChartData } from './SankeyChart'; +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; -const data = { +const data: SankeyChartData = { nodes: [ { name: 'Home Page' }, { name: 'Dashboard' }, { name: 'Preferences' }, { name: 'Billing' }, - ], links: [ { source: 0, target: 1, value: 100 }, @@ -17,4 +17,12 @@ const data = { ], }; -storiesOf('SankeyChart', module).add('Pure', () => ); +export default { + title: 'Dashboad/Cards/SankeyChart', + component: SankeyChart, +} as ComponentMeta; + +const Template: ComponentStory = (args: any) => ; + +export const Simple = Template.bind({}); +Simple.args = { data }; diff --git a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx index c09888102..511ca12e8 100644 --- a/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx +++ b/frontend/app/components/shared/Insights/SankeyChart/SankeyChart.tsx @@ -1,11 +1,27 @@ import React from 'react'; import { Sankey, Tooltip, Rectangle, Layer, ResponsiveContainer } from 'recharts'; +type Node = { + name: string; +} + +type Link = { + source: number; + target: number; + value: number; +} + +export interface SankeyChartData { + links: Link[]; + nodes: Node[]; +} interface Props { - data: any; + data: SankeyChartData; + nodePadding?: number; + nodeWidth?: number; } function SankeyChart(props: Props) { - const { data } = props; + const { data, nodePadding = 50, nodeWidth = 10 } = props; return (
Sankey Chart
@@ -17,8 +33,8 @@ function SankeyChart(props: Props) { data={data} // node={{ stroke: '#77c878', strokeWidth: 0 }} node={} - nodePadding={50} - nodeWidth={10} + nodePadding={nodePadding} + nodeWidth={nodeWidth} margin={{ left: 10, right: 100, @@ -33,7 +49,7 @@ function SankeyChart(props: Props) { - }/> + } />
@@ -44,10 +60,7 @@ function SankeyChart(props: Props) { export default SankeyChart; const CustomTooltip = (props: any) => { - console.log('props', props); - return ( -
test
- ) + return
test
; // if (active && payload && payload.length) { // return ( //
diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts new file mode 100644 index 000000000..1c95f66ea --- /dev/null +++ b/frontend/app/constants/card.ts @@ -0,0 +1,213 @@ +import { IconNames } from 'App/components/ui/SVG'; +import { FilterKey, IssueType } from 'Types/filter/filterType'; + +export interface CardType { + title: string; + icon?: IconNames; + description: string; + slug: string; + subTypes?: CardType[]; +} + +export const LIBRARY = 'library'; +export const TIMESERIES = 'timeseries'; +export const TABLE = 'table'; + +export const TYPES: CardType[] = [ + { + title: 'Add From Library', + icon: 'grid', + description: 'Select a pre existing card from card library', + slug: LIBRARY, + }, + { + title: 'Timeseries', + icon: 'graph-up', + description: 'Trend of sessions count in over the time.', + slug: TIMESERIES, + subTypes: [{ title: 'Session Count', slug: 'sessionCount', description: '' }], + }, + { + title: 'Table', + icon: 'list-alt', + description: 'See list of Users, Sessions, Errors, Issues, etc.,', + slug: TABLE, + subTypes: [ + { title: 'Users', slug: FilterKey.USERID, description: '' }, + { title: 'Sessions', slug: FilterKey.SESSIONS, description: '' }, + { title: 'JS Errors', slug: FilterKey.ERRORS, description: '' }, + { title: 'Issues', slug: FilterKey.ISSUE, description: '' }, + { title: 'Browser', slug: FilterKey.USER_BROWSER, description: '' }, + { title: 'Devices', slug: FilterKey.USER_DEVICE, description: '' }, + { title: 'Countries', slug: FilterKey.USER_COUNTRY, description: '' }, + { title: 'URLs', slug: FilterKey.LOCATION, description: '' }, + ], + }, + { + title: 'Funnel', + icon: 'funnel', + description: 'Uncover the issues impacting user journeys.', + slug: 'funnel', + }, + { + title: 'Errors Tracking', + icon: 'exclamation-circle', + description: 'Discover user journeys between 2 points.', + slug: 'errors', + subTypes: [ + { title: 'Resources by Party', slug: FilterKey.RESOURCES_BY_PARTY, description: '' }, + { title: 'Errors per Domains', slug: FilterKey.ERRORS_PER_DOMAINS, description: '' }, + { title: 'Errors per type', slug: FilterKey.ERRORS_PER_TYPE, description: '' }, + { title: 'Calls_Errors', slug: FilterKey.CALLS_ERRORS, description: '' }, + { title: 'Domains_Errors_4xx', slug: FilterKey.DOMAINS_ERRORS_4XX, description: '' }, + { title: 'Domains_Errors_5xx', slug: FilterKey.DOMAINS_ERRORS_5XX, description: '' }, + { + title: 'Impacted_Sessions_By_Js_Errors', + slug: FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS, + description: '', + }, + ], + }, + { + title: 'Performance Monitoring', + icon: 'speedometer2', + description: 'Retention graph of users / features over a period of time.', + slug: 'performance', + subTypes: [ + { title: 'Cpu', slug: FilterKey.CPU, description: '' }, + { title: 'Crashes', slug: FilterKey.CRASHES, description: '' }, + { title: 'Fps', slug: FilterKey.FPS, description: '' }, + { title: 'Pages_Dom_Build_Time', slug: FilterKey.PAGES_DOM_BUILD_TIME, description: '' }, + { title: 'Memory_Consumption', slug: FilterKey.MEMORY_CONSUMPTION, description: '' }, + { title: 'Pages_Response_Time', slug: FilterKey.PAGES_RESPONSE_TIME, description: '' }, + { + title: 'Pages_Response_Time_Distribution', + slug: FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION, + description: '', + }, + { + title: 'Resources_Vs_Visually_Complete', + slug: FilterKey.RESOURCES_VS_VISUALLY_COMPLETE, + description: '', + }, + { title: 'Sessions_Per_Browser', slug: FilterKey.SESSIONS_PER_BROWSER, description: '' }, + { title: 'Slowest_Domains', slug: FilterKey.SLOWEST_DOMAINS, description: '' }, + { title: 'Speed_Location', slug: FilterKey.SPEED_LOCATION, description: '' }, + { title: 'Time_To_Render', slug: FilterKey.TIME_TO_RENDER, description: '' }, + { + title: 'Impacted_Sessions_By_Slow_Pages', + slug: FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES, + description: '', + }, + ], + }, + { + title: 'Resource Monitoring', + icon: 'files', + description: 'Find the adoption of your all features in your app.', + slug: 'resource-monitoring', + subTypes: [ + { + title: 'Breakdown_Of_Loaded_Resources', + slug: FilterKey.BREAKDOWN_OF_LOADED_RESOURCES, + description: '', + }, + { title: 'Missing_Resources', slug: FilterKey.MISSING_RESOURCES, description: '' }, + { + title: 'Resource_Type_Vs_Response_End', + slug: FilterKey.RESOURCE_TYPE_VS_RESPONSE_END, + description: '', + }, + { title: 'Resource_Fetch_Time', slug: FilterKey.RESOURCE_FETCH_TIME, description: '' }, + { title: 'Slowest_Resources', slug: FilterKey.SLOWEST_RESOURCES, description: '' }, + ], + }, + { + title: 'Web Vitals', + icon: 'activity', + description: 'Find the adoption of your all features in your app.', + slug: 'web-vitals', + subTypes: [ + { + title: 'Resources_Count_By_Type', + slug: FilterKey.RESOURCES_COUNT_BY_TYPE, + description: '', + }, + { title: 'Resources_Loading_Time', slug: FilterKey.RESOURCES_LOADING_TIME, description: '' }, + { + title: 'CPU Load', + slug: FilterKey.AVG_CPU, + description: 'Uncover the issues impacting user journeys', + }, + { + title: 'DOM Build Time', + slug: FilterKey.AVG_DOM_CONTENT_LOADED, + description: 'Keep a close eye on errors and track their type, origin and domain.', + }, + { + title: 'DOM Content Loaded Start', + slug: FilterKey.AVG_DOM_CONTENT_LOAD_START, + description: + 'FInd out which resources are missing and those that may be slowign your web app.', + }, + { + title: 'DOM Content Loaded', + slug: FilterKey.AVG_FIRST_CONTENTFUL_PIXEL, + description: + "Optimize your app's performance by tracking slow domains, page resposne times, memory consumption, CPU usage and more.", + }, + { + title: 'First Paint', + slug: FilterKey.AVG_FIRST_PAINT, + description: + 'Find out which resources are missing and those that may be slowing your web app.', + }, + { title: 'Frame Rate', slug: FilterKey.AVG_FPS, description: '' }, + { + title: 'Image Load Time', + slug: FilterKey.AVG_IMAGE_LOAD_TIME, + description: + 'Find out which resources are missing and those that may be slowing your web app.', + }, + { title: 'Page Load Time', slug: FilterKey.AVG_PAGE_LOAD_TIME, description: '' }, + { title: 'DOM Build Time', slug: FilterKey.AVG_PAGES_DOM_BUILD_TIME, description: '' }, + { title: 'Pages Response Time', slug: FilterKey.AVG_PAGES_RESPONSE_TIME, description: '' }, + { title: 'Request Load Time', slug: FilterKey.AVG_REQUEST_LOADT_IME, description: '' }, + { title: 'Response Time ', slug: FilterKey.AVG_RESPONSE_TIME, description: '' }, + { title: 'Session Dueration', slug: FilterKey.AVG_SESSION_DURATION, description: '' }, + { title: 'Time Till First Byte', slug: FilterKey.AVG_TILL_FIRST_BYTE, description: '' }, + { title: 'Time to be Interactive', slug: FilterKey.AVG_TIME_TO_INTERACTIVE, description: '' }, + { title: 'Time to Render', slug: FilterKey.AVG_TIME_TO_RENDER, description: '' }, + { title: 'JS Heap Size', slug: FilterKey.AVG_USED_JS_HEAP_SIZE, description: '' }, + { title: 'Visited Pages', slug: FilterKey.AVG_VISITED_PAGES, description: '' }, + { + title: 'Captured Requests', + slug: FilterKey.COUNT_REQUESTS, + description: 'Trend of sessions count in over the time.', + }, + { + title: 'Captured Sessions', + slug: FilterKey.COUNT_SESSIONS, + description: 'See list of users, sessions, errors, issues, etc.,', + }, + ], + }, + { + title: 'User Path', + icon: 'signpost-split', + description: 'Discover user journeys between 2 points.', + slug: 'user-path', + }, + { + title: 'Retention', + icon: 'arrow-repeat', + description: 'Retension graph of users / features over a period of time.', + slug: 'retention', + }, + { + title: 'Feature Adoption', + icon: 'card-checklist', + description: 'Find the adoption of your all features in your app.', + slug: 'feature-adoption', + }, +]; diff --git a/frontend/app/mstore/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index bf3a970d8..417f20f4d 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -277,9 +277,9 @@ export default class DashboardStore { ); } - getDashboard(dashboardId: string): Dashboard | null { + getDashboard(dashboardId: string|number): Dashboard | null { return ( - this.dashboards.find((d) => d.dashboardId === dashboardId) || null + this.dashboards.find((d) => d.dashboardId == dashboardId) || null ); } diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 9d2748e2e..c82a54779 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -220,4 +220,58 @@ export enum FilterKey { SESSIONS = 'SESSIONS', ERRORS = 'js_exception', + + RESOURCES_COUNT_BY_TYPE = 'resourcesCountByType', + RESOURCES_LOADING_TIME = 'resourcesLoadingTime', + AVG_CPU = 'avgCpu', + AVG_DOM_CONTENT_LOADED = 'avgDomContentLoaded', + AVG_DOM_CONTENT_LOAD_START = 'avgDomContentLoadStart', + AVG_FIRST_CONTENTFUL_PIXEL = 'avgFirstContentfulPixel', + AVG_FIRST_PAINT = 'avgFirstPaint', + AVG_FPS = 'avgFps', + AVG_IMAGE_LOAD_TIME = 'avgImageLoadTime', + AVG_PAGE_LOAD_TIME = 'avgPageLoadTime', + AVG_PAGES_DOM_BUILD_TIME = 'avgPagesDomBuildtime', + AVG_PAGES_RESPONSE_TIME = 'avgPagesResponseTime', + AVG_REQUEST_LOADT_IME = 'avgRequestLoadTime', + AVG_RESPONSE_TIME = 'avgResponseTime', + AVG_SESSION_DURATION = 'avgSessionDuration', + AVG_TILL_FIRST_BYTE = 'avgTillFirstByte', + AVG_TIME_TO_INTERACTIVE = 'avgTimeToInteractive', + AVG_TIME_TO_RENDER = 'avgTimeToRender', + AVG_USED_JS_HEAP_SIZE = 'avgUsedJsHeapSize', + AVG_VISITED_PAGES = 'avgVisitedPages', + COUNT_REQUESTS = 'countRequests', + COUNT_SESSIONS = 'countSessions', + + // Errors + RESOURCES_BY_PARTY = 'resourcesByParty', + ERRORS_PER_DOMAINS = 'errorsPerDomains', + ERRORS_PER_TYPE = 'errorsPerType', + CALLS_ERRORS = 'callsErrors', + DOMAINS_ERRORS_4XX = 'domainsErrors4Xx', + DOMAINS_ERRORS_5XX = 'domainsErrors5Xx', + IMPACTED_SESSIONS_BY_JS_ERRORS = 'impactedSessionsByJsErrors', + + // Performance + CPU = 'cpu', + CRASHES = 'crashes', + FPS = 'fps', + PAGES_DOM_BUILD_TIME = 'pagesDomBuildtime', + MEMORY_CONSUMPTION = 'memoryConsumption', + PAGES_RESPONSE_TIME = 'pagesResponseTime', + PAGES_RESPONSE_TIME_DISTRIBUTION = 'pagesResponseTimeDistribution', + RESOURCES_VS_VISUALLY_COMPLETE = 'resourcesVsVisuallyComplete', + SESSIONS_PER_BROWSER = 'sessionsPerBrowser', + SLOWEST_DOMAINS = 'slowestDomains', + SPEED_LOCATION = 'speedLocation', + TIME_TO_RENDER = 'timeToRender', + IMPACTED_SESSIONS_BY_SLOW_PAGES = 'impactedSessionsBySlowPages', + + // Resources + BREAKDOWN_OF_LOADED_RESOURCES = 'breakdownOfLoadedResources', + MISSING_RESOURCES = 'missingResources', + RESOURCE_TYPE_VS_RESPONSE_END = 'resourceTypeVsResponseEnd', + RESOURCE_FETCH_TIME = 'resourceFetchTime', + SLOWEST_RESOURCES = 'slowestResources', } From 7818bc88ee0cc154d1d3d9af385f92fd03062a0f Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 12 Dec 2022 13:46:52 +0530 Subject: [PATCH 08/56] feat(ui) - dashboard improvements - api - integration --- frontend/app/api_client.js | 2 +- .../DashboardWidgetGrid.tsx | 4 +- .../MetricListItem/MetricListItem.tsx | 5 +- .../MetricTypeList/MetricTypeList.tsx | 8 +++- .../MetricViewHeader/MetricViewHeader.tsx | 21 ++++++-- .../components/MetricsGrid/MetricsGrid.tsx | 14 ++++++ .../Dashboard/components/MetricsGrid/index.ts | 1 + .../components/MetricsList/GridView.tsx | 40 ++++++++++++++++ .../components/MetricsList/ListView.tsx | 45 +++++++++++++++++ .../components/MetricsList/MetricsList.tsx | 48 +++++++++++++------ frontend/app/mstore/metricStore.ts | 1 + frontend/app/services/MetricService.ts | 16 +++---- 12 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 frontend/app/components/Dashboard/components/MetricsGrid/MetricsGrid.tsx create mode 100644 frontend/app/components/Dashboard/components/MetricsGrid/index.ts create mode 100644 frontend/app/components/Dashboard/components/MetricsList/GridView.tsx create mode 100644 frontend/app/components/Dashboard/components/MetricsList/ListView.tsx diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index 8985d0686..4def6c0b2 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -25,7 +25,7 @@ const siteIdRequiredPaths = [ '/heatmaps', '/custom_metrics', '/dashboards', - '/metrics', + '/cards', '/unprocessed', '/notes', // '/custom_metrics/sessions', diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index cef3dd332..f0d5bcd67 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -56,7 +56,7 @@ function DashboardWidgetGrid(props: Props) { >
{smallWidgets.length > 0 ? ( <> -
+
Web Vitals
@@ -82,7 +82,7 @@ function DashboardWidgetGrid(props: Props) { ) : null} {smallWidgets.length > 0 && regularWidgets.length > 0 ? ( -
+
All Metrics
diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index 2bade6afb..df3b09180 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -39,19 +39,20 @@ function MetricListItem(props: Props) { const path = withSiteId(`/metrics/${metric.metricId}`, siteId); history.push(path); }; + return (
- + /> */}
{metric.name}
diff --git a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx index e0d6e846b..8e64d970d 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx @@ -4,7 +4,7 @@ import MetricsLibraryModal from '../MetricsLibraryModal'; import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem'; import { TYPES, LIBRARY } from 'App/constants/card'; import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { dashboardMetricCreate, withSiteId } from 'App/routes'; +import { dashboardMetricCreate, metricCreate, withSiteId } from 'App/routes'; interface Props extends RouteComponentProps { dashboardId: number; @@ -23,7 +23,11 @@ function MetricTypeList(props: Props) { // TODO redirect to card builder with metricType query param const path = withSiteId(dashboardMetricCreate(dashboardId + ''), siteId); - history.push(path); + const queryString = new URLSearchParams({ type: slug }).toString(); + history.push({ + pathname: path, + search: `?${queryString}` + }); }; return ( diff --git a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx index 2c7790d73..4f33a2a8d 100644 --- a/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx +++ b/frontend/app/components/Dashboard/components/MetricViewHeader/MetricViewHeader.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Icon, PageTitle, Button, Link } from 'UI'; +import { Icon, PageTitle, Button, Link, SegmentSelection } from 'UI'; import MetricsSearch from '../MetricsSearch'; import Select from 'Shared/Select'; import { useStore } from 'App/mstore'; @@ -8,6 +8,9 @@ import { useObserver } from 'mobx-react-lite'; function MetricViewHeader() { const { metricStore } = useStore(); const sort = useObserver(() => metricStore.sort); + const listView = useObserver(() => metricStore.listView); + + return (
@@ -15,9 +18,21 @@ function MetricViewHeader() {
- + {/* - + */} + metricStore.updateKey('listView', !listView) } + value={{ value: listView ? 'list' : 'grid' }} + list={ [ + { value: 'list', name: '', icon: 'graph-up-arrow' }, + { value: 'grid', name: '', icon: 'hash' }, + ]} + />
i.value === metric.metricType) || options[0]} - onChange={props.onSelect} + onChange={(selected) => onChange(selected.value.value as string)} // onSelect={onSelect} components={{ MenuList: ({ children, ...props }: any) => { @@ -51,4 +61,4 @@ function MetricTypeDropdown(props: Props) { ); } -export default MetricTypeDropdown; +export default withLocationHandlers()(observer(MetricTypeDropdown)); diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index bea850d11..8f88dce04 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -3,7 +3,7 @@ import cn from 'classnames'; import WidgetWrapper from '../WidgetWrapper'; import { useStore } from 'App/mstore'; import { SegmentSelection, Button, Icon } from 'UI'; -import { useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; import { FilterKey } from 'Types/filter/filterType'; import WidgetDateRange from '../WidgetDateRange/WidgetDateRange'; // import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period'; @@ -18,12 +18,12 @@ function WidgetPreview(props: Props) { const { className = '' } = props; const { metricStore, dashboardStore } = useStore(); const dashboards = dashboardStore.dashboards; - const metric: any = useObserver(() => metricStore.instance); + const metric: any = metricStore.instance; const isTimeSeries = metric.metricType === 'timeseries'; const isTable = metric.metricType === 'table'; - const drillDownFilter = useObserver(() => dashboardStore.drillDownFilter); - const disableVisualization = useObserver(() => metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS); - // const period = useObserver(() => dashboardStore.drillDownPeriod); + const drillDownFilter = dashboardStore.drillDownFilter; + const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS; + // const period = dashboardStore.drillDownPeriod; const chagneViewType = (e, { name, value }: any) => { metric.update({ [ name ]: value }); @@ -40,7 +40,7 @@ function WidgetPreview(props: Props) { const canAddToDashboard = metric.exists() && dashboards.length > 0; - return useObserver(() => ( + return ( <>
@@ -93,7 +93,7 @@ function WidgetPreview(props: Props) { className="ml-2 p-0" onClick={() => setShowDashboardSelectionModal(true)} disabled={!canAddToDashboard} - > + > Add to Dashboard @@ -112,7 +112,7 @@ function WidgetPreview(props: Props) { /> )} - )); + ); } -export default WidgetPreview; +export default observer(WidgetPreview); diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 1c95f66ea..64378e557 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -12,6 +12,7 @@ export interface CardType { export const LIBRARY = 'library'; export const TIMESERIES = 'timeseries'; export const TABLE = 'table'; +export const CLICKMAP = 'clickmap' export const TYPES: CardType[] = [ { @@ -20,6 +21,15 @@ export const TYPES: CardType[] = [ description: 'Select a pre existing card from card library', slug: LIBRARY, }, + { + title: 'Clickmap', + icon: 'puzzle-piece', + description: 'Track the features that are being used the most.', + slug: CLICKMAP, + subTypes: [ + { title: 'Visited URL', slug: FilterKey.CLICKMAP_URL, description: "" }, + ] + }, { title: 'Timeseries', icon: 'graph-up', diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 4f72268bd..cc4b9bee2 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable, computed } from 'mobx'; +import { makeAutoObservable } from 'mobx'; import Widget from './types/widget'; import { metricService, errorService } from 'App/services'; import { toast } from 'react-toastify'; @@ -24,7 +24,6 @@ export default class MetricStore { makeAutoObservable(this); } - @computed get sortedWidgets() { return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified); } @@ -44,6 +43,10 @@ export default class MetricStore { this.instance.updateKey('hasChanged', true); } + changeType(value: string) { + this.instance.update({ metricType: value}) + } + reset(id: string) { const metric = this.findById(id); if (metric) { diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 96e97a092..026ea87ac 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -274,4 +274,6 @@ export enum FilterKey { RESOURCE_TYPE_VS_RESPONSE_END = 'resourceTypeVsResponseEnd', RESOURCE_FETCH_TIME = 'resourceFetchTime', SLOWEST_RESOURCES = 'slowestResources', + + CLICKMAP_URL = 'clickmapUrl' } From 16908faf45788ad822178f310d6ef8f3451aa037 Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 12 Dec 2022 17:38:56 +0100 Subject: [PATCH 11/56] change(ui): auto update metric subtype with metric type change --- .../ClickMapCard/ClickMapCard.tsx | 15 +++++++++++++++ .../CustomMetricsWidgets/ClickMapCard/index.ts | 1 + .../components/WidgetChart/WidgetChart.tsx | 5 +++-- .../MetricSubtypeDropdown.tsx | 5 +++++ .../MetricTypeDropdown/MetricTypeDropdown.tsx | 12 +++++++++--- 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx create mode 100644 frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx new file mode 100644 index 000000000..2a373389c --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -0,0 +1,15 @@ +import React from 'react' +import { useStore } from 'App/mstore' +import { observer } from 'mobx-react-lite' +import { toJS } from 'mobx' + +function ClickMapCard() { + const { metricStore } = useStore() + + console.log(toJS(metricStore.instance)) + return ( +
this is a card
+ ) +} + +export default observer(ClickMapCard) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts new file mode 100644 index 000000000..c72a4090b --- /dev/null +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/index.ts @@ -0,0 +1 @@ +export { default } from './ClickMapCard' diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 732359789..37a0e9297 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -19,6 +19,7 @@ import ErrorsWidget from '../Errors/ErrorsWidget'; 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' interface Props { metric: any; @@ -179,11 +180,11 @@ function WidgetChart(props: Props) { } if (metricType === CLICKMAP) { return ( -
rendering clickmap
+ ) } - return
Unknown
; + return
Unknown metric type
; } return ( diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx index eccfda55e..fe7d3a503 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx @@ -28,6 +28,11 @@ function MetricSubtypeDropdown(props: Props) { return false; }, [metric.metricType]); + React.useEffect(() => { + // @ts-ignore + setTimeout(() => props.onSelect({ name: 'metricOf', value: { value: options[0].value }}), 0) + }, [metric.metricType]) + return options ? ( <>
of
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index 9a7d85fe8..891a21d40 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -8,13 +8,20 @@ import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import withLocationHandlers from 'HOCs/withLocationHandlers'; +interface Options { + label: string; + icon: string; + value: string; + description: string; +} + interface Props { query: Record any>; } function MetricTypeDropdown(props: Props) { const { metricStore } = useStore(); const metric: any = metricStore.instance; - const options: any = useMemo(() => { + const options: Options[] = useMemo(() => { // TYPES.shift(); // remove "Add from library" item return TYPES.filter((i: MetricType) => i.slug !== LIBRARY).map((i: MetricType) => ({ label: i.title, @@ -27,9 +34,8 @@ function MetricTypeDropdown(props: Props) { React.useEffect(() => { const queryCardType = props.query.get('type') if (queryCardType && options.length > 0 && metric.metricType) { - const type = options.find(i => i.value === queryCardType) + const type = options.find((i) => i.value === queryCardType) setTimeout(() => onChange(type.value), 0) - console.log('trying to change to ', type, metric.metricType) } }, []) From 4da30db2d74a32703e964e3143fa93e1d0f6eefb Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 12:09:54 +0100 Subject: [PATCH 12/56] change(ui): some player changes for clickmaps --- .../ClickMapCard/ClickMapCard.tsx | 2 + .../app/components/Session/LivePlayer.tsx | 2 +- .../app/components/Session/PlayerContent.js | 6 +- .../app/components/Session/RightBlock.tsx | 2 +- frontend/app/components/Session/WebPlayer.tsx | 89 ++++++++++--------- .../PageInsightsPanel/PageInsightsPanel.tsx | 1 - .../app/components/Session_/Player/Player.js | 73 +++++++-------- .../app/components/Session_/PlayerBlock.js | 10 ++- .../components/Session_/PlayerBlockHeader.tsx | 2 +- 9 files changed, 100 insertions(+), 87 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index 2a373389c..f18afba81 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -2,6 +2,8 @@ import React from 'react' import { useStore } from 'App/mstore' import { observer } from 'mobx-react-lite' import { toJS } from 'mobx' +// import WebPlayer from "Player/web/WebPlayer"; +// inject mob file from chalice function ClickMapCard() { const { metricStore } = useStore() diff --git a/frontend/app/components/Session/LivePlayer.tsx b/frontend/app/components/Session/LivePlayer.tsx index 54ea109ef..b45ebe1a3 100644 --- a/frontend/app/components/Session/LivePlayer.tsx +++ b/frontend/app/components/Session/LivePlayer.tsx @@ -88,7 +88,7 @@ function LivePlayer({ const TABS = { EVENTS: 'User Steps', - HEATMAPS: 'Click Map', + CLICKMAP: 'Click Map', }; const [activeTab, setActiveTab] = useState(''); diff --git a/frontend/app/components/Session/PlayerContent.js b/frontend/app/components/Session/PlayerContent.js index 64d896455..f78b71897 100644 --- a/frontend/app/components/Session/PlayerContent.js +++ b/frontend/app/components/Session/PlayerContent.js @@ -9,10 +9,10 @@ import { observer } from 'mobx-react-lite'; const TABS = { EVENTS: 'User Steps', - HEATMAPS: 'Click Map', + CLICKMAP: 'Click Map', }; -function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab }) { +function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab, isClickmap }) { const { store } = React.useContext(PlayerContext) const { @@ -51,7 +51,7 @@ function PlayerContent({ session, live, fullscreen, activeTab, setActiveTab }) { style={activeTab && !fullscreen ? { maxWidth: 'calc(100% - 270px)' } : undefined} >
- +
{activeTab !== '' && ( diff --git a/frontend/app/components/Session/RightBlock.tsx b/frontend/app/components/Session/RightBlock.tsx index 66f92a93c..0778c6f07 100644 --- a/frontend/app/components/Session/RightBlock.tsx +++ b/frontend/app/components/Session/RightBlock.tsx @@ -19,7 +19,7 @@ function RightBlock(props: any) { switch(tab) { case props.tabs.EVENTS: return - case props.tabs.HEATMAPS: + case props.tabs.CLICKMAP: return } } diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index 6e169777e..cdbb54ff5 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -15,7 +15,7 @@ import { IPlayerContext, PlayerContext, defaultContextValue } from './playerCont const TABS = { EVENTS: 'User Steps', - HEATMAPS: 'Click Map', + CLICKMAP: 'Click Map', }; function WebPlayer(props: any) { @@ -23,10 +23,11 @@ function WebPlayer(props: any) { session, toggleFullscreen, closeBottomBlock, - live, - fullscreen, - jwt, - fetchList + live, + fullscreen, + fetchList, + isClickmap, + customSession, } = props; const { notesStore } = useStore(); const [activeTab, setActiveTab] = useState(''); @@ -35,22 +36,28 @@ function WebPlayer(props: any) { const [contextValue, setContextValue] = useState(defaultContextValue); useEffect(() => { - fetchList('issues'); - const [WebPlayerInst, PlayerStore] = createWebPlayer(session, (state) => + if (!isClickmap) { + fetchList('issues'); + } + const usedSession = isClickmap ? customSession : session; + + const [WebPlayerInst, PlayerStore] = createWebPlayer(usedSession, (state) => makeAutoObservable(state) ); setContextValue({ player: WebPlayerInst, store: PlayerStore }); props.fetchMembers(); - notesStore.fetchSessionNotes(session.sessionId).then((r) => { - const note = props.query.get('note'); - if (note) { - WebPlayerInst.pause(); - setNoteItem(notesStore.getNoteById(parseInt(note, 10), r)); - setShowNote(true); - } - }); + if (!isClickmap) { + notesStore.fetchSessionNotes(session.sessionId).then((r) => { + const note = props.query.get('note'); + if (note) { + WebPlayerInst.pause(); + setNoteItem(notesStore.getNoteById(parseInt(note, 10), r)); + setShowNote(true); + } + }); + } const jumptTime = props.query.get('jumpto'); if (jumptTime) { @@ -78,36 +85,39 @@ function WebPlayer(props: any) { return ( - <> + <> + {!isClickmap ? ( - {/* @ts-ignore */} - - - {showNoteModal ? ( - ) => m.id === noteItem?.userId) - ?.email || '' - } - note={noteItem} - onClose={onNoteClose} - notFound={!noteItem} - /> - ) : null} - - + ) : null} + {/* @ts-ignore */} + + + {showNoteModal ? ( + ) => m.id === noteItem?.userId)?.email + || '' + } + note={noteItem} + onClose={onNoteClose} + notFound={!noteItem} + /> + ) : null} + + ); } @@ -115,7 +125,6 @@ function WebPlayer(props: any) { export default connect( (state: any) => ({ session: state.getIn(['sessions', 'current']), - jwt: state.get('jwt'), fullscreen: state.getIn(['components', 'player', 'fullscreen']), showEvents: state.get('showEvents'), members: state.getIn(['members', 'list']), diff --git a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx index e1ba5ccba..b43f70ed9 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx @@ -13,7 +13,6 @@ const JUMP_OFFSET = 1000; interface Props { filters: any; fetchInsights: (filters: Record) => void; - urls: []; insights: any; events: Array; urlOptions: Array; diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index afda1e4d8..d2e155b1c 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -45,65 +45,66 @@ function Player(props) { activeTab, fullView, isMultiview, + isClickmap, } = props; - const playerContext = React.useContext(PlayerContext) + const playerContext = React.useContext(PlayerContext); const screenWrapper = React.useRef(); - const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE + const bottomBlockIsActive = !fullscreen && bottomBlock !== NONE; React.useEffect(() => { props.updateLastPlayedSession(props.sessionId); if (!props.closedLive || isMultiview) { const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture - playerContext.player.attach(parentElement) + playerContext.player.attach(parentElement); playerContext.player.play(); } - - }, []) + }, []); React.useEffect(() => { playerContext.player.scale(); - }, [props.bottomBlock, props.fullscreen, playerContext.player]) + }, [props.bottomBlock, props.fullscreen, playerContext.player]); if (!playerContext.player) return null; const maxWidth = activeTab ? 'calc(100vw - 270px)' : '100vw'; return (
- {fullscreen && } -
- -
+ className={cn(className, stl.playerBody, 'flex flex-col relative', fullscreen && 'pb-2')} + data-bottom-block={bottomBlockIsActive} + > + {fullscreen && } +
+ +
+
+ {!fullscreen && !!bottomBlock && ( +
+ {bottomBlock === OVERVIEW && } + {bottomBlock === CONSOLE && } + {bottomBlock === NETWORK && } + {/* {bottomBlock === STACKEVENTS && } */} + {bottomBlock === STACKEVENTS && } + {bottomBlock === STORAGE && } + {bottomBlock === PROFILER && } + {bottomBlock === PERFORMANCE && } + {bottomBlock === GRAPHQL && } + {bottomBlock === EXCEPTIONS && } + {bottomBlock === INSPECTOR && }
- {!fullscreen && !!bottomBlock && ( -
- {bottomBlock === OVERVIEW && } - {bottomBlock === CONSOLE && } - {bottomBlock === NETWORK && ( - - )} - {/* {bottomBlock === STACKEVENTS && } */} - {bottomBlock === STACKEVENTS && } - {bottomBlock === STORAGE && } - {bottomBlock === PROFILER && } - {bottomBlock === PERFORMANCE && } - {bottomBlock === GRAPHQL && } - {bottomBlock === EXCEPTIONS && } - {bottomBlock === INSPECTOR && } -
- )} - {!fullView && !isMultiview && } -
- ) + /> + ) : null} +
+ ); } -export default connect((state) => { +export default connect( + (state) => { const isAssist = window.location.pathname.includes('/assist/'); return { fullscreen: state.getIn(['components', 'player', 'fullscreen']), @@ -120,4 +121,4 @@ export default connect((state) => { fullscreenOff, updateLastPlayedSession, } -)(Player) +)(Player); diff --git a/frontend/app/components/Session_/PlayerBlock.js b/frontend/app/components/Session_/PlayerBlock.js index 5da29a587..bca8a236a 100644 --- a/frontend/app/components/Session_/PlayerBlock.js +++ b/frontend/app/components/Session_/PlayerBlock.js @@ -14,19 +14,21 @@ import styles from './playerBlock.module.css'; })) export default class PlayerBlock extends React.PureComponent { render() { - const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false, isMultiview } = this.props; + const { fullscreen, sessionId, disabled, activeTab, jiraConfig, fullView = false, isMultiview, isClickmap } = this.props; + const shouldShowSubHeader = !fullscreen && !fullView && !isMultiview && !isClickmap return ( -
- {!fullscreen && !fullView && !isMultiview && ( +
+ {shouldShowSubHeader ? ( - )} + ) : null}
); diff --git a/frontend/app/components/Session_/PlayerBlockHeader.tsx b/frontend/app/components/Session_/PlayerBlockHeader.tsx index c1b45f36e..cd19712c7 100644 --- a/frontend/app/components/Session_/PlayerBlockHeader.tsx +++ b/frontend/app/components/Session_/PlayerBlockHeader.tsx @@ -74,7 +74,7 @@ function PlayerBlockHeader(props: any) { return { label: key, value }; }); - const TABS = [props.tabs.EVENTS, props.tabs.HEATMAPS].map((tab) => ({ + const TABS = [props.tabs.EVENTS, props.tabs.CLICKMAP].map((tab) => ({ text: tab, key: tab, })); From c8cba2aeaae9ef7a121e29a8a1163997c80afb38 Mon Sep 17 00:00:00 2001 From: sylenien Date: Tue, 13 Dec 2022 13:36:22 +0100 Subject: [PATCH 13/56] change(ui): more tests for the clickmaps --- .../components/Session_/Player/Overlay.tsx | 4 +- .../app/components/Session_/Player/Player.js | 6 ++- frontend/app/player/web/Screen/Screen.ts | 28 +++++++++---- frontend/app/player/web/Screen/types.ts | 2 +- frontend/app/player/web/WebPlayer.ts | 4 ++ .../app/player/web/addons/TargetMarker.ts | 40 ++++++++++++++++--- 6 files changed, 68 insertions(+), 16 deletions(-) diff --git a/frontend/app/components/Session_/Player/Overlay.tsx b/frontend/app/components/Session_/Player/Overlay.tsx index 37a94b8c6..e7645b34e 100644 --- a/frontend/app/components/Session_/Player/Overlay.tsx +++ b/frontend/app/components/Session_/Player/Overlay.tsx @@ -19,11 +19,13 @@ import { observer } from 'mobx-react-lite'; interface Props { nextId: string, closedLive?: boolean, + isClickmap?: boolean, } function Overlay({ nextId, closedLive, + isClickmap, }: Props) { const { player, store } = React.useContext(PlayerContext) @@ -49,7 +51,7 @@ function Overlay({ const concetionStatus = peerConnectionStatus const showAutoplayTimer = !live && completed && autoplay && nextId - const showPlayIconLayer = !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer; + const showPlayIconLayer = !isClickmap && !live && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer; const showLiveStatusText = live && livePlay && liveStatusText && !loading; const showRequestWindow = diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index d2e155b1c..a247e3e0b 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -45,7 +45,7 @@ function Player(props) { activeTab, fullView, isMultiview, - isClickmap, + isClickmap = true, } = props; const playerContext = React.useContext(PlayerContext); const screenWrapper = React.useRef(); @@ -57,6 +57,8 @@ function Player(props) { const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture playerContext.player.attach(parentElement); playerContext.player.play(); + + setInterval(() => playerContext.player.scaleFullPage(), 4000) } }, []); @@ -74,7 +76,7 @@ function Player(props) { > {fullscreen && }
- +
{!fullscreen && !!bottomBlock && ( diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 043be5357..8925e387e 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -191,22 +191,22 @@ export default class Screen { this.iframe.style.display = flag ? '' : 'none'; } - private s: number = 1; + private scaleRatio: number = 1; getScale() { - return this.s; + return this.scaleRatio; } scale({ height, width }: Dimensions) { if (!this.parentElement) return; const { offsetWidth, offsetHeight } = this.parentElement; - this.s = Math.min(offsetWidth / width, offsetHeight / height); - if (this.s > 1) { - this.s = 1; + this.scaleRatio = Math.min(offsetWidth / width, offsetHeight / height); + if (this.scaleRatio > 1) { + this.scaleRatio = 1; } else { - this.s = Math.round(this.s * 1e3) / 1e3; + this.scaleRatio = Math.round(this.scaleRatio * 1e3) / 1e3; } - this.screen.style.transform = `scale(${ this.s }) translate(-50%, -50%)`; + this.screen.style.transform = `scale(${ this.scaleRatio }) translate(-50%, -50%)`; this.screen.style.width = width + 'px'; this.screen.style.height = height + 'px'; this.iframe.style.width = width + 'px'; @@ -214,4 +214,18 @@ export default class Screen { this.boundingRect = this.overlay.getBoundingClientRect(); } + + scaleFullPage() { + const { height, width } = this.document.body.getBoundingClientRect(); + const offsetHeight = this.parentElement.getBoundingClientRect().height + if (!this.parentElement) return; + + console.log(height, width) + this.scaleRatio = 1 + this.screen.style.transform = `scale(1) translate(-50%, -50%)`; + this.screen.style.overflow = 'scroll'; + this.screen.style.height = `${offsetHeight - 50}px`; + this.iframe.style.width = width + 'px'; + this.iframe.style.height = height + 'px'; + } } diff --git a/frontend/app/player/web/Screen/types.ts b/frontend/app/player/web/Screen/types.ts index 2c464fdfc..093649892 100644 --- a/frontend/app/player/web/Screen/types.ts +++ b/frontend/app/player/web/Screen/types.ts @@ -6,4 +6,4 @@ export interface Point { export interface Dimensions { width: number height: number -} \ No newline at end of file +} diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index bd80c1189..fc0534e2a 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -87,6 +87,10 @@ export default class WebPlayer extends Player { // this.updateMarketTargets() ?? } + scaleFullPage =() => { + return this.screen.scaleFullPage() + } + // Inspector & marker mark(e: Element) { this.inspectorController.marker?.mark(e) diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts index 49282e96a..a21e0b089 100644 --- a/frontend/app/player/web/addons/TargetMarker.ts +++ b/frontend/app/player/web/addons/TargetMarker.ts @@ -36,6 +36,7 @@ export interface State { export default class TargetMarker { + private clickMapOverlay: HTMLDivElement static INITIAL_STATE: State = { markedTargets: null, activeTargetIndex: 0 @@ -50,8 +51,8 @@ export default class TargetMarker { const { markedTargets } = this.store.get() if (markedTargets) { this.store.update({ - markedTargets: markedTargets.map((mt: any) => ({ - ...mt, + markedTargets: markedTargets.map((mt: any) => ({ + ...mt, boundingRect: this.calculateRelativeBoundingRect(mt.el), })), }); @@ -95,22 +96,48 @@ export default class TargetMarker { }) }, 0) } - + } this.store.update({ activeTargetIndex: index }); } private actualScroll: Point | null = null markTargets(selections: { selector: string, count: number }[] | null) { + if (selections) { const totalCount = selections.reduce((a, b) => { return a + b.count }, 0); const markedTargets: MarkedTarget[] = []; let index = 0; + + const overlay = document.createElement("div") + overlay.style.position = "absolute" + overlay.style.top = "0px" + overlay.style.left = "0px" + overlay.style.width = '100%' + overlay.style.height = "100%" + overlay.style.background = 'rgba(0,0,0, 0.1)' + this.screen.document.body.appendChild(overlay) + this.clickMapOverlay = overlay selections.forEach((s) => { const el = this.screen.getElementBySelector(s.selector); if (!el) return; + const test = document.createElement("div") + const top = el.getBoundingClientRect().top + const left = el.getBoundingClientRect().left + test.innerHTML = '' + s.count + 'Clicks' + Object.assign(test.style, { + position: 'absolute', + top: top + 'px', + left: left + 'px', + padding: '10px', + borderRadius: '12px', + background: 'white', + boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)', + }) + + overlay.appendChild(test) markedTargets.push({ ...s, el, @@ -120,7 +147,7 @@ export default class TargetMarker { count: s.count, }) }); - this.actualScroll = this.screen.getCurrentScroll() + this.actualScroll = this.screen.getCurrentScroll() this.store.update({ markedTargets }); } else { if (this.actualScroll) { @@ -128,7 +155,10 @@ export default class TargetMarker { this.actualScroll = null } this.store.update({ markedTargets: null }); + this.clickMapOverlay.remove() } } -} \ No newline at end of file + + +} From 62f53275bed165e1028745bcf4f4bf520e100751 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 16 Dec 2022 11:33:55 +0100 Subject: [PATCH 14/56] change(ui): url filter for clickmap --- .../ClickMapCard/ClickMapCard.tsx | 2 +- .../components/FilterSeries/FilterSeries.tsx | 11 +- .../components/WidgetForm/WidgetForm.tsx | 54 +++++++-- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 3 +- frontend/app/components/Session/WebPlayer.tsx | 44 +++++++- .../PageInsightsPanel/PageInsightsPanel.tsx | 2 +- .../app/components/Session_/Player/Player.js | 4 +- .../FilterAutoComplete/FilterAutoComplete.tsx | 4 +- .../shared/Filters/FilterItem/FilterItem.tsx | 2 + .../Filters/FilterValue/FilterValue.tsx | 1 + frontend/app/constants/card.ts | 2 +- frontend/app/constants/filterOptions.js | 12 +- frontend/app/player/web/MessageManager.ts | 1 + frontend/app/player/web/Screen/Screen.ts | 2 +- frontend/app/player/web/WebPlayer.ts | 5 + .../app/player/web/addons/TargetMarker.ts | 105 +++++++++++++----- .../app/player/web/addons/clickmapStyles.ts | 58 ++++++++++ frontend/app/types/filter/newFilter.js | 36 +++--- 18 files changed, 275 insertions(+), 73 deletions(-) create mode 100644 frontend/app/player/web/addons/clickmapStyles.ts diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index f18afba81..c3e9f4ba4 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -8,7 +8,7 @@ import { toJS } from 'mobx' function ClickMapCard() { const { metricStore } = useStore() - console.log(toJS(metricStore.instance)) + // console.log(toJS(metricStore.instance)) return (
this is a card
) diff --git a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx index ccb269cbd..2496d0377 100644 --- a/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx +++ b/frontend/app/components/Dashboard/components/FilterSeries/FilterSeries.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import FilterList from 'Shared/Filters/FilterList'; -import { +import { edit, updateSeries, addSeriesFilterFilter, @@ -21,7 +21,7 @@ interface Props { edit: typeof edit; updateSeries: typeof updateSeries; onRemoveSeries: (seriesIndex: any) => void; - canDelete?: boolean; + canDelete?: boolean; addSeriesFilterFilter: typeof addSeriesFilterFilter; editSeriesFilterFilter: typeof editSeriesFilterFilter; editSeriesFilter: typeof editSeriesFilter; @@ -43,6 +43,7 @@ function FilterSeries(props: Props) { const onUpdateFilter = (filterIndex: any, filter: any) => { series.filter.updateFilter(filterIndex, filter) + console.log('hi', filterIndex, filter) observeChanges() } @@ -62,7 +63,7 @@ function FilterSeries(props: Props) {
series.update('name', name) } />
- +
@@ -103,11 +104,11 @@ function FilterSeries(props: Props) { ); } -export default connect(null, { +export default connect(null, { edit, updateSeries, addSeriesFilterFilter, editSeriesFilterFilter, editSeriesFilter, removeSeriesFilterFilter, -})(observer(FilterSeries)); \ No newline at end of file +})(observer(FilterSeries)); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 93df06c54..85676e8ed 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -1,15 +1,20 @@ import React from 'react'; -import { metricTypes, metricOf, issueOptions } from 'App/constants/filterOptions'; +import { metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { Button, Icon, SegmentSelection } from 'UI' +import { Button, Icon } from 'UI' import FilterSeries from '../FilterSeries'; -import { confirm, Tooltip } from 'UI'; +import { confirm, Tooltip, Input } from 'UI'; import Select from 'Shared/Select' import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes' import MetricTypeDropdown from './components/MetricTypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; +import { TIMESERIES, TABLE, CLICKMAP } from 'App/constants/card' +import { pageUrlOperators } from 'App/constants/filterOptions' +import FilterAutoComplete from 'Shared/Filters/FilterAutoComplete'; +import { clickmapFilter } from 'App/types/filter/newFilter'; +import { toJS } from 'mobx' interface Props { history: any; @@ -27,7 +32,6 @@ function WidgetForm(props: Props) { const { history, match: { params: { siteId, dashboardId } } } = props; const { metricStore, dashboardStore } = useStore(); - const dashboards = dashboardStore.dashboards; const isSaving = metricStore.isSaving; const metric: any = metricStore.instance @@ -35,7 +39,6 @@ function WidgetForm(props: Props) { const tableOptions = metricOf.filter(i => i.type === 'table'); const isTable = metric.metricType === 'table'; const isFunnel = metric.metricType === 'funnel'; - const canAddToDashboard = metric.exists() && dashboards.length > 0; 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); @@ -60,13 +63,16 @@ function WidgetForm(props: Props) { } if (name === 'metricType') { - if (value === 'timeseries') { + if (value === TIMESERIES) { obj['metricOf'] = timeseriesOptions[0].value; obj['viewType'] = 'lineChart'; - } else if (value === 'table') { + } else if (value === TABLE) { obj['metricOf'] = tableOptions[0].value; obj['viewType'] = 'table'; } + if (value === CLICKMAP) { + obj['viewType'] = 'chart' + } } metricStore.merge(obj); @@ -99,13 +105,21 @@ function WidgetForm(props: Props) { metricStore.delete(metric).then(props.onDelete); } } - + const updateClickMapURL = (_, item) => { + console.log('updating filter', item) + const newValues = { + value: item + } + metric.series[0].filter.updateFilter(0, newValues) + console.log(toJS(metric.series)) + } + console.log(metric.series, metric.series[0].filter) return (
- + {/* {metric.metricType === 'timeseries' && ( @@ -160,6 +174,28 @@ function WidgetForm(props: Props) { )}
+ {metric.metricType === CLICKMAP && ( +
+
Where Visited URL
+ */} + +
+ )}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index 891a21d40..9623df971 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -17,6 +17,7 @@ interface Options { interface Props { query: Record any>; + onSelect: (arg: any) => void; } function MetricTypeDropdown(props: Props) { const { metricStore } = useStore(); @@ -48,7 +49,7 @@ function MetricTypeDropdown(props: Props) { placeholder="Select Card Type" options={options} value={options.find((i: any) => i.value === metric.metricType) || options[0]} - onChange={(selected) => onChange(selected.value.value as string)} + onChange={props.onSelect} // onSelect={onSelect} components={{ MenuList: ({ children, ...props }: any) => { diff --git a/frontend/app/components/Session/WebPlayer.tsx b/frontend/app/components/Session/WebPlayer.tsx index cdbb54ff5..6be605c7f 100644 --- a/frontend/app/components/Session/WebPlayer.tsx +++ b/frontend/app/components/Session/WebPlayer.tsx @@ -12,6 +12,9 @@ import ReadNote from '../Session_/Player/Controls/components/ReadNote'; import { fetchList as fetchMembers } from 'Duck/member'; import PlayerContent from './PlayerContent'; import { IPlayerContext, PlayerContext, defaultContextValue } from './playerContext'; +import { fetchInsights } from 'Duck/sessions'; +import Period, { LAST_30_DAYS } from 'Types/app/period'; +import { observer } from 'mobx-react-lite'; const TABS = { EVENTS: 'User Steps', @@ -26,8 +29,13 @@ function WebPlayer(props: any) { live, fullscreen, fetchList, - isClickmap, customSession, + isClickmap = true, + fetchInsights, + host, + visitedEvents, + insightsFilters, + insights, } = props; const { notesStore } = useStore(); const [activeTab, setActiveTab] = useState(''); @@ -36,10 +44,16 @@ function WebPlayer(props: any) { const [contextValue, setContextValue] = useState(defaultContextValue); useEffect(() => { - if (!isClickmap) { + if (isClickmap) { + const urlOptions = visitedEvents.map(({ url, host }: any) => ({ label: url, value: url, host })) + const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value; + // @ts-ignore + const { startDate, endDate, rangeValue } = new Period({ rangeName: LAST_30_DAYS }) + fetchInsights({ ...insightsFilters, url, startDate, endDate, rangeValue }) + } else { fetchList('issues'); } - const usedSession = isClickmap ? customSession : session; + const usedSession = isClickmap && customSession ? customSession : session; const [WebPlayerInst, PlayerStore] = createWebPlayer(usedSession, (state) => makeAutoObservable(state) @@ -67,6 +81,23 @@ function WebPlayer(props: any) { return () => WebPlayerInst.clean(); }, [session.sessionId]); + const isPlayerReady = contextValue.store?.get().ready + + React.useEffect(() => { + contextValue.player && contextValue.player.play() + if (isClickmap && isPlayerReady && insights.size > 0) { + setTimeout(() => { + contextValue.player.jump(500) + contextValue.player.pause() + contextValue.player.scaleFullPage() + setTimeout(() => { contextValue.player.showClickmap(insights) }, 250) + }, 500) + } + return () => { + isPlayerReady && contextValue.player.showClickmap(null) + } + }, [insights, isPlayerReady]) + // LAYOUT (TODO: local layout state - useContext or something..) useEffect( () => () => { @@ -125,6 +156,10 @@ function WebPlayer(props: any) { export default connect( (state: any) => ({ session: state.getIn(['sessions', 'current']), + insightsFilters: state.getIn(['sessions', 'insightFilters']), + host: state.getIn(['sessions', 'host']), + insights: state.getIn(['sessions', 'insights']), + visitedEvents: state.getIn(['sessions', 'visitedEvents']), fullscreen: state.getIn(['components', 'player', 'fullscreen']), showEvents: state.get('showEvents'), members: state.getIn(['members', 'list']), @@ -134,5 +169,6 @@ export default connect( closeBottomBlock, fetchList, fetchMembers, + fetchInsights, } -)(withLocationHandlers()(WebPlayer)); +)(withLocationHandlers()(observer(WebPlayer))); diff --git a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx index b43f70ed9..ce017c601 100644 --- a/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx +++ b/frontend/app/components/Session_/PageInsightsPanel/PageInsightsPanel.tsx @@ -100,7 +100,7 @@ function PageInsightsPanel({ filters, fetchInsights, events = [], insights, urlO } export default connect( - (state) => { + (state: any) => { const events = state.getIn(['sessions', 'visitedEvents']); return { filters: state.getIn(['sessions', 'insightFilters']), diff --git a/frontend/app/components/Session_/Player/Player.js b/frontend/app/components/Session_/Player/Player.js index a247e3e0b..e7e29ad3e 100644 --- a/frontend/app/components/Session_/Player/Player.js +++ b/frontend/app/components/Session_/Player/Player.js @@ -45,7 +45,7 @@ function Player(props) { activeTab, fullView, isMultiview, - isClickmap = true, + isClickmap, } = props; const playerContext = React.useContext(PlayerContext); const screenWrapper = React.useRef(); @@ -57,8 +57,6 @@ function Player(props) { const parentElement = findDOMNode(screenWrapper.current); //TODO: good architecture playerContext.player.attach(parentElement); playerContext.player.play(); - - setInterval(() => playerContext.player.scaleFullPage(), 4000) } }, []); diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index a616d8853..4ad93deb2 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -115,6 +115,7 @@ interface Props { onSelect: (e: any, item: any) => void; value: any; icon?: string; + hideOrText?: boolean } function FilterAutoComplete(props: Props) { @@ -128,6 +129,7 @@ function FilterAutoComplete(props: Props) { endpoint = '', params = {}, value = '', + hideOrText = false, } = props; const [loading, setLoading] = useState(false); const [options, setOptions] = useState([]); @@ -240,7 +242,7 @@ function FilterAutoComplete(props: Props) {
- {!showOrButton &&
or
} + {!showOrButton && !hideOrText &&
or
}
); } diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index 4b27e4a93..6cebb0d94 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -37,6 +37,7 @@ function FilterItem(props: Props) { }; const onUpdateSubFilter = (subFilter: any, subFilterIndex: any) => { + console.log(subFilter, subFilterIndex) props.onUpdate({ ...filter, filters: filter.filters.map((i: any, index: any) => { @@ -48,6 +49,7 @@ function FilterItem(props: Props) { }); }; + console.log('filterItem', filter) return (
diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index 7daab8acb..4a13b2c4a 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -40,6 +40,7 @@ function FilterValue(props: Props) { } return _; }); + console.log(item ,{ ...filter, value: newValues }); props.onUpdate({ ...filter, value: newValues }); }; diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 64378e557..78626f8c1 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -12,7 +12,7 @@ export interface CardType { export const LIBRARY = 'library'; export const TIMESERIES = 'timeseries'; export const TABLE = 'table'; -export const CLICKMAP = 'clickmap' +export const CLICKMAP = 'clickMap' export const TYPES: CardType[] = [ { diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js index 224a32ae6..442717ff5 100644 --- a/frontend/app/constants/filterOptions.js +++ b/frontend/app/constants/filterOptions.js @@ -1,7 +1,7 @@ import { FilterKey, IssueType } from 'Types/filter/filterType'; // TODO remove text property from options export const options = [ - { key: 'on', label: 'on', value: 'on' }, + { key: 'on', label: 'on', value: 'on' }, { key: 'notOn', label: 'not on', value: 'notOn' }, { key: 'onAny', label: 'on any', value: 'onAny' }, { key: 'is', label: 'is', value: 'is' }, @@ -13,9 +13,9 @@ export const options = [ { key: 'contains', label: 'contains', value: 'contains' }, { key: 'notContains', label: 'not contains', value: 'notContains' }, { key: 'hasAnyValue', label: 'has any value', value: 'hasAnyValue' }, - { key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' }, + { key: 'hasNoValue', label: 'has no value', value: 'hasNoValue' }, { key: 'isSignedUp', label: 'is signed up', value: 'isSignedUp' }, - { key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' }, + { key: 'notSignedUp', label: 'not signed up', value: 'notSignedUp' }, { key: 'before', label: 'before', value: 'before' }, { key: 'after', label: 'after', value: 'after' }, { key: 'inRage', label: 'in rage', value: 'inRage' }, @@ -37,6 +37,7 @@ const stringFilterKeysPerformance = ['is', 'inAnyPage', 'isNot', 'contains', 'st const targetFilterKeys = ['on', 'notOn', 'onAny', 'contains', 'startsWith', 'endsWith', 'notContains']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; +const pageUrlFilter = ['contains', 'startsWith', 'endsWith'] const getOperatorsByKeys = (keys) => { return options.filter(option => keys.includes(option.key)); @@ -50,6 +51,7 @@ export const booleanOperators = [ { key: 'true', label: 'true', value: 'true' }, { key: 'false', label: 'false', value: 'false' }, ] +export const pageUrlOperators = options.filter(({key}) => pageUrlFilter.includes(key)) export const customOperators = [ { key: '=', label: '=', value: '=' }, @@ -86,6 +88,7 @@ export const metricOf = [ { label: 'Devices', value: FilterKey.USER_DEVICE, type: 'table' }, { label: 'Countries', value: FilterKey.USER_COUNTRY, type: 'table' }, { label: 'URLs', value: FilterKey.LOCATION, type: 'table' }, + ] export const methodOptions = [ @@ -97,7 +100,7 @@ export const methodOptions = [ { label: 'HEAD', value: 'HEAD' }, { label: 'OPTIONS', value: 'OPTIONS' }, { label: 'TRACE', value: 'TRACE' }, - { label: 'CONNECT', value: 'CONNECT' }, + { label: 'CONNECT', value: 'CONNECT' }, ] export const issueOptions = [ @@ -128,4 +131,5 @@ export default { metricOf, issueOptions, methodOptions, + pageUrlOperators, } diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index e548f406e..bfb127247 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -207,6 +207,7 @@ export default class MessageManager { this.waitingForFiles = false this.setMessagesLoading(false) + // this.state.update({ filesLoaded: true }) } private async loadMessages() { diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 8925e387e..f10a3bd35 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -217,10 +217,10 @@ export default class Screen { scaleFullPage() { const { height, width } = this.document.body.getBoundingClientRect(); + this.cursor.toggle(false) const offsetHeight = this.parentElement.getBoundingClientRect().height if (!this.parentElement) return; - console.log(height, width) this.scaleRatio = 1 this.screen.style.transform = `scale(1) translate(-50%, -50%)`; this.screen.style.overflow = 'scroll'; diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index fc0534e2a..7a7c84cb8 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -122,6 +122,11 @@ export default class WebPlayer extends Player { this.targetMarker.markTargets(...args) } + showClickmap = (...args: Parameters) => { + this.pause() + this.targetMarker.injectTargets(...args) + } + // TODO separate message receivers toggleTimetravel = async () => { diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts index a21e0b089..028858f6d 100644 --- a/frontend/app/player/web/addons/TargetMarker.ts +++ b/frontend/app/player/web/addons/TargetMarker.ts @@ -1,7 +1,7 @@ import type Screen from '../Screen/Screen' import type { Point } from '../Screen/types' import type { Store } from '../../common/types' - +import { clickmapStyles } from './clickmapStyles' function getOffset(el: Element, innerWindow: Window) { const rect = el.getBoundingClientRect(); @@ -37,6 +37,8 @@ export interface State { export default class TargetMarker { private clickMapOverlay: HTMLDivElement + private clickContainers: HTMLDivElement[] = [] + private smallClicks: HTMLDivElement[] = [] static INITIAL_STATE: State = { markedTargets: null, activeTargetIndex: 0 @@ -103,41 +105,16 @@ export default class TargetMarker { private actualScroll: Point | null = null markTargets(selections: { selector: string, count: number }[] | null) { - if (selections) { const totalCount = selections.reduce((a, b) => { return a + b.count }, 0); const markedTargets: MarkedTarget[] = []; let index = 0; - - const overlay = document.createElement("div") - overlay.style.position = "absolute" - overlay.style.top = "0px" - overlay.style.left = "0px" - overlay.style.width = '100%' - overlay.style.height = "100%" - overlay.style.background = 'rgba(0,0,0, 0.1)' - this.screen.document.body.appendChild(overlay) - this.clickMapOverlay = overlay selections.forEach((s) => { const el = this.screen.getElementBySelector(s.selector); if (!el) return; - const test = document.createElement("div") - const top = el.getBoundingClientRect().top - const left = el.getBoundingClientRect().left - test.innerHTML = '' + s.count + 'Clicks' - Object.assign(test.style, { - position: 'absolute', - top: top + 'px', - left: left + 'px', - padding: '10px', - borderRadius: '12px', - background: 'white', - boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)', - }) - overlay.appendChild(test) markedTargets.push({ ...s, el, @@ -155,10 +132,84 @@ export default class TargetMarker { this.actualScroll = null } this.store.update({ markedTargets: null }); - this.clickMapOverlay.remove() } } + injectTargets(selections: { selector: string, count: number }[] | null) { + if (selections) { + const totalCount = selections.reduce((a, b) => { + return a + b.count + }, 0); + + const overlay = document.createElement("div") + Object.assign(overlay.style, clickmapStyles.overlayStyle) + + this.clickMapOverlay = overlay + selections.forEach((s, i) => { + const el = this.screen.getElementBySelector(s.selector); + if (!el) return; + + const bubbleContainer = document.createElement("div") + const {top, left, width, height} = el.getBoundingClientRect() + + const totalClicks = document.createElement("div") + totalClicks.innerHTML = `${s.count} ${s.count !== 1 ? 'Clicks' : 'Click'}` + Object.assign(totalClicks.style, clickmapStyles.totalClicks) + + const percent = document.createElement("div") + percent.style.fontSize = "14px" + percent.innerHTML = `${Math.round((s.count * 100) / totalCount)}% of the clicks recorded in this page` + + bubbleContainer.appendChild(totalClicks) + bubbleContainer.appendChild(percent) + const containerId = `clickmap-bubble-${i}` + bubbleContainer.id = containerId + this.clickContainers.push(bubbleContainer) + Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left })) + + const border = document.createElement("div") + Object.assign(border.style, clickmapStyles.highlight({ width, height, top, left })) + + const smallClicksBubble = document.createElement("div") + smallClicksBubble.innerHTML = '' + s.count + const smallClicksId = containerId + '-small' + smallClicksBubble.id = smallClicksId + this.smallClicks.push(smallClicksBubble) + + border.onclick = () => { + this.clickContainers.forEach(container => { + if (container.id === containerId) { + container.style.visibility = "visible" + } else { + container.style.visibility = "hidden" + } + }) + this.smallClicks.forEach(container => { + if (container.id !== smallClicksId) { + container.style.visibility = "visible" + } else { + container.style.visibility = "hidden" + } + }) + } + + Object.assign(smallClicksBubble.style, clickmapStyles.clicks) + + border.appendChild(smallClicksBubble) + overlay.appendChild(bubbleContainer) + overlay.appendChild(border) + }); + + this.screen.document.body.appendChild(overlay) + // this.store.update({ markedTargets }); + } else { + this.store.update({ markedTargets: null }); + this.clickMapOverlay?.remove() + this.clickMapOverlay = null + this.smallClicks = [] + this.clickContainers = [] + } + } } diff --git a/frontend/app/player/web/addons/clickmapStyles.ts b/frontend/app/player/web/addons/clickmapStyles.ts new file mode 100644 index 000000000..086260089 --- /dev/null +++ b/frontend/app/player/web/addons/clickmapStyles.ts @@ -0,0 +1,58 @@ +export const clickmapStyles = { + overlayStyle: { + position: 'absolute', + top: '0px', + left: '0px', + width: '100%', + height: '100%', + background: 'rgba(0,0,0, 0.15)', + zIndex: 9 * 10e3, + // pointerEvents: 'none', + }, + totalClicks: { + fontSize: '16px', + fontWeight: '600', + }, + bubbleContainer: ({ top, left }: { top: number; left: number }) => ({ + position: 'absolute', + top: top + 'px', + left: left + 'px', + padding: '10px', + borderRadius: '6px', + background: 'white', + border: '1px solid rgba(0, 0, 0, 0.12)', + boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)', + transform: `translate(-25%, -110%)`, + textAlign: 'center', + visibility: 'hidden', + }), + highlight: ({ + width, + height, + top, + left, + }: { + width: number; + height: number; + top: number; + left: number; + }) => ({ + width: width + 'px', + height: height + 'px', + border: '2px dotted red', + cursor: 'pointer', + top: top + 'px', + left: left + 'px', + position: 'absolute', + }), + clicks: { + top: 0, + left: 0, + position: 'absolute', + borderRadius: '999px', + padding: '6px', + background: 'white', + lineHeight: '0.5', + transform: 'translate(-70%, -70%)', + }, +}; diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 8abd3f5fc..2e5410f50 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -29,7 +29,7 @@ export const filters = [ { 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' }, @@ -42,7 +42,7 @@ export const filters = [ // { 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 }, @@ -53,6 +53,12 @@ export const filters = [ { 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 clickmapFilter = { + key: FilterKey.LOCATION, + type: FilterType.MULTIPLE, + category: FilterCategory.INTERACTIONS, + label: '', placeholder: 'Enter URL or path to select', operator: 'is', operatorOptions: filterOptions.pageUrlOperators, icon: 'filters/location', isEvent: true } + const mapFilters = (list) => { return list.reduce((acc, filter) => { acc[filter.key] = filter; @@ -97,12 +103,12 @@ export const clearMetaFilters = () => { /** * Add a new filter to the filter list - * @param {*} category - * @param {*} key - * @param {*} type - * @param {*} operator - * @param {*} operatorOptions - * @param {*} icon + * @param {*} category + * @param {*} key + * @param {*} type + * @param {*} operator + * @param {*} operatorOptions + * @param {*} icon */ export const addElementToFiltersMap = ( category = FilterCategory.METADATA, @@ -143,15 +149,15 @@ export default Record({ value: [""], source: [""], category: '', - + custom: '', // target: Target(), level: '', - + hasNoValue: false, isFilter: false, actualValue: '', - + hasSource: false, source: [""], sourceType: '', @@ -161,7 +167,7 @@ export default Record({ sourceOperatorOptions: [], operator: '', - operatorOptions: [], + operatorOptions: [], operatorDisabled: false, isEvent: false, index: 0, @@ -199,8 +205,8 @@ export default Record({ /** * Group filters by category - * @param {*} filtersMap - * @returns + * @param {*} filtersMap + * @returns */ export const generateFilterOptions = (map) => { const filterSection = {}; @@ -229,4 +235,4 @@ export const generateLiveFilterOptions = (map) => { } }); return filterSection; -} \ No newline at end of file +} From a9fe18689b81e74563319c53261d3b68485b3874 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 16 Dec 2022 14:58:58 +0100 Subject: [PATCH 15/56] change(ui): apply session url series by default when switching on clickmap --- .../components/WidgetForm/WidgetForm.tsx | 391 +++++++++--------- frontend/app/types/filter/filterType.ts | 2 +- frontend/app/types/filter/newFilter.js | 2 +- 3 files changed, 188 insertions(+), 207 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 85676e8ed..bc3a67651 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -3,126 +3,128 @@ import { metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { Button, Icon } from 'UI' +import { Button, Icon } from 'UI'; import FilterSeries from '../FilterSeries'; import { confirm, Tooltip, Input } from 'UI'; -import Select from 'Shared/Select' -import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes' +import Select from 'Shared/Select'; +import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; import MetricTypeDropdown from './components/MetricTypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; -import { TIMESERIES, TABLE, CLICKMAP } from 'App/constants/card' -import { pageUrlOperators } from 'App/constants/filterOptions' +import { TIMESERIES, TABLE, CLICKMAP } from 'App/constants/card'; +import { pageUrlOperators } from 'App/constants/filterOptions'; import FilterAutoComplete from 'Shared/Filters/FilterAutoComplete'; import { clickmapFilter } from 'App/types/filter/newFilter'; -import { toJS } from 'mobx' +import { toJS } from 'mobx'; +import { List } from 'immutable'; interface Props { - history: any; - match: any; - onDelete: () => void; + history: any; + match: any; + onDelete: () => void; } const metricIcons = { - timeseries: 'graph-up', - table: 'table', - funnel: 'funnel', -} + timeseries: 'graph-up', + table: 'table', + funnel: 'funnel', +}; function WidgetForm(props: Props) { + const { + history, + match: { + params: { siteId, dashboardId }, + }, + } = props; + const { metricStore, dashboardStore } = useStore(); + const isSaving = metricStore.isSaving; + const metric: any = metricStore.instance; - const { history, match: { params: { siteId, dashboardId } } } = props; - const { metricStore, dashboardStore } = useStore(); - const isSaving = metricStore.isSaving; - const metric: any = metricStore.instance + const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); + const tableOptions = metricOf.filter((i) => i.type === 'table'); + const isTable = metric.metricType === 'table'; + const isFunnel = metric.metricType === 'funnel'; + const canAddSeries = metric.series.length < 3; + const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length; + const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); - const timeseriesOptions = metricOf.filter(i => i.type === 'timeseries'); - const tableOptions = metricOf.filter(i => i.type === 'table'); - const isTable = metric.metricType === 'table'; - const isFunnel = metric.metricType === 'funnel'; - const canAddSeries = metric.series.length < 3; - const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length - const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); + const writeOption = ({ value, name }: any) => { + value = Array.isArray(value) ? value : value.value; + const obj: any = { [name]: value }; - const writeOption = ({ value, name }: any) => { - value = Array.isArray(value) ? value : value.value - const obj: any = { [ name ]: value }; + if (name === 'metricValue') { + obj['metricValue'] = value; - if (name === 'metricValue') { - obj['metricValue'] = value; - - // handle issues (remove all when other option is selected) - if (Array.isArray(obj['metricValue']) && obj['metricValue'].length > 1) { - obj['metricValue'] = obj['metricValue'].filter(i => i.value !== 'all'); - } - } - - if (name === 'metricOf') { - // if (value === FilterKey.ISSUE) { - // obj['metricValue'] = [{ value: 'all', label: 'All' }]; - // } - } - - if (name === 'metricType') { - if (value === TIMESERIES) { - obj['metricOf'] = timeseriesOptions[0].value; - obj['viewType'] = 'lineChart'; - } else if (value === TABLE) { - obj['metricOf'] = tableOptions[0].value; - obj['viewType'] = 'table'; - } - if (value === CLICKMAP) { - obj['viewType'] = 'chart' - } - } - - metricStore.merge(obj); - }; - - const onSelect = (_: any, option: Record) => writeOption({ value: { value: option.value }, name: option.name}) - - const onSave = () => { - const wasCreating = !metric.exists() - metricStore.save(metric, dashboardId) - .then((metric: any) => { - if (wasCreating) { - if (parseInt(dashboardId) > 0) { - history.replace(withSiteId(dashboardMetricDetails(dashboardId, metric.metricId), siteId)); - const dashboard = dashboardStore.getDashboard(parseInt(dashboardId)) - dashboardStore.addWidgetToDashboard(dashboard, [metric.metricId]) - } else { - history.replace(withSiteId(metricDetails(metric.metricId), siteId)); - } - } - }); + // handle issues (remove all when other option is selected) + if (Array.isArray(obj['metricValue']) && obj['metricValue'].length > 1) { + obj['metricValue'] = obj['metricValue'].filter((i) => i.value !== 'all'); + } } - const onDelete = async () => { - if (await confirm({ - header: 'Confirm', - confirmButton: 'Yes, delete', - confirmation: `Are you sure you want to permanently delete this metric?` - })) { - metricStore.delete(metric).then(props.onDelete); - } + if (name === 'metricOf') { + // if (value === FilterKey.ISSUE) { + // obj['metricValue'] = [{ value: 'all', label: 'All' }]; + // } } - const updateClickMapURL = (_, item) => { - console.log('updating filter', item) - const newValues = { - value: item - } - metric.series[0].filter.updateFilter(0, newValues) - console.log(toJS(metric.series)) - } - console.log(metric.series, metric.series[0].filter) - return ( -
-
- -
- - - {/* {metric.metricType === 'timeseries' && ( + if (name === 'metricType') { + if (value === TIMESERIES) { + obj['metricOf'] = timeseriesOptions[0].value; + obj['viewType'] = 'lineChart'; + } else if (value === TABLE) { + obj['metricOf'] = tableOptions[0].value; + obj['viewType'] = 'table'; + } + if (value === CLICKMAP) { + obj['viewType'] = 'chart'; + if (metric.series[0].filter.filters.length < 1) { + metric.series[0].filter.addFilter({ + ...clickmapFilter, + value: [''], + },) + } + } + } + metricStore.merge(obj); + }; + + const onSave = () => { + const wasCreating = !metric.exists(); + metricStore.save(metric, dashboardId).then((metric: any) => { + if (wasCreating) { + if (parseInt(dashboardId) > 0) { + history.replace(withSiteId(dashboardMetricDetails(dashboardId, metric.metricId), siteId)); + const dashboard = dashboardStore.getDashboard(parseInt(dashboardId)); + dashboardStore.addWidgetToDashboard(dashboard, [metric.metricId]); + } else { + history.replace(withSiteId(metricDetails(metric.metricId), siteId)); + } + } + }); + }; + + const onDelete = async () => { + if ( + await confirm({ + header: 'Confirm', + confirmButton: 'Yes, delete', + confirmation: `Are you sure you want to permanently delete this metric?`, + }) + ) { + metricStore.delete(metric).then(props.onDelete); + } + }; + + console.log(toJS(metric)); + return ( +
+
+ +
+ + + + {/* {metric.metricType === 'timeseries' && ( <> of )} */} - {metric.metricOf === FilterKey.ISSUE && ( - <> - issue type - + + )} - {metric.metricType === 'table' && !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( - <> - showing - null } - /> - {/* */} - -
- )} -
- -
-
- {`${(isTable || isFunnel) ? 'Filter by' : 'Chart Series'}`} - {!isTable && !isFunnel && ( - - )} -
- - {metric.series.length > 0 && metric.series.slice(0, (isTable || isFunnel) ? 1 : metric.series.length).map((series: any, index: number) => ( -
- metric.updateKey('hasChanged', true)} - hideHeader={ isTable } - seriesIndex={index} - series={series} - onRemoveSeries={() => metric.removeSeries(index)} - canDelete={metric.series.length > 1} - emptyMessage={isTable ? - 'Filter data using any event or attribute. Use Add Step button below to do so.' : - 'Add user event or filter to define the series by clicking Add Step.' - } - /> -
- ))} -
- -
- - - -
- {metric.exists() && ( - - )} -
-
+ {metric.metricType === 'table' && + !(metric.metricOf === FilterKey.ERRORS || metric.metricOf === FilterKey.SESSIONS) && ( + <> + showing + @@ -118,7 +149,7 @@ function WidgetSessions(props: Props) {
- )); + ); } const getListSessionsBySeries = (data: any, seriesId: any) => { diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index cc4b9bee2..7f0efdddd 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -20,6 +20,9 @@ export default class MetricStore { sessionsPageSize: number = 10; listView?: boolean = false + clickMapSearch = '' + clickMapLabel = '' + constructor() { makeAutoObservable(this); } @@ -38,6 +41,11 @@ export default class MetricStore { this[key] = value; } + changeClickMapSearch(val: string, label: string) { + this.clickMapSearch = val + this.clickMapLabel = label + } + merge(object: any) { Object.assign(this.instance, object); this.instance.updateKey('hasChanged', true); @@ -83,12 +91,12 @@ export default class MetricStore { } // API Communication - save(metric: Widget, dashboardId?: string): Promise { + save(metric: Widget): Promise { const wasCreating = !metric.exists(); this.isSaving = true; return new Promise((resolve, reject) => { metricService - .saveMetric(metric, dashboardId) + .saveMetric(metric) .then((metric: any) => { const _metric = new Widget().fromJson(metric); if (wasCreating) { diff --git a/frontend/app/mstore/sessionStore.ts b/frontend/app/mstore/sessionStore.ts index d055a9aa8..553f742b2 100644 --- a/frontend/app/mstore/sessionStore.ts +++ b/frontend/app/mstore/sessionStore.ts @@ -94,7 +94,7 @@ export default class SessionStore { getSessions(filter: any): Promise { return new Promise((resolve, reject) => { sessionService - .getSessions(filter.toJson()) + .getSessions(filter.toJson?.() || filter) .then((response: any) => { resolve({ sessions: response.sessions.map((session: any) => new Session().fromJson(session)), diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 753691d3d..b7504c4c6 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -6,6 +6,7 @@ import Funnelissue from 'App/mstore/types/funnelIssue'; import { issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import Period, { LAST_24_HOURS } from 'Types/app/period'; +import { metricService } from "App/services"; export default class Widget { public static get ID_KEY():string { return "metricId" } diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 4e6e768bb..ed1cb540a 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -176,11 +176,11 @@ export default class Screen { return this.getElementsFromInternalPoint(this.getInternalViewportCoordinates(point)); } - getElementBySelector(selector: string): Element | null { + getElementBySelector(selector: string) { if (!selector) return null; try { const safeSelector = selector.replace(/:/g, '\\\\3A ').replace(/\//g, '\\/'); - return this.document?.querySelector(safeSelector) || null; + return this.document?.querySelector(safeSelector) || null; } catch (e) { console.error("Can not select element. ", e) return null diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts index e20b4c6c4..f063b8dca 100644 --- a/frontend/app/player/web/addons/TargetMarker.ts +++ b/frontend/app/player/web/addons/TargetMarker.ts @@ -39,7 +39,7 @@ export default class TargetMarker { private clickMapOverlay: HTMLDivElement private clickContainers: HTMLDivElement[] = [] private smallClicks: HTMLDivElement[] = [] - private onMarkerClick: (selector: string) => void + private onMarkerClick: (selector: string, innerText: string) => void static INITIAL_STATE: State = { markedTargets: null, activeTargetIndex: 0 @@ -182,7 +182,8 @@ export default class TargetMarker { border.onclick = (e) => { e.stopPropagation() - this.onMarkerClick?.(s.selector) + const innerText = el.innerText.length > 25 ? `${el.innerText.slice(0, 20)}...` : el.innerText + this.onMarkerClick?.(s.selector, innerText) this.clickContainers.forEach(container => { if (container.id === containerId) { container.style.visibility = "visible" @@ -201,6 +202,7 @@ export default class TargetMarker { overlay.onclick = (e) => { e.stopPropagation() + this.onMarkerClick('', '') this.clickContainers.forEach(container => { container.style.visibility = "hidden" }) From 857ae565ae6c7d6ce24ff0e3c8fd170c50e57a8d Mon Sep 17 00:00:00 2001 From: sylenien Date: Wed, 21 Dec 2022 17:39:07 +0100 Subject: [PATCH 22/56] change(ui): fix for horizontal align in missing data msgs --- .../CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index b9412de7d..8c187e10c 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -17,9 +17,9 @@ function ClickMapCard({ setCustomSession, visitedEvents }: any) { } }, [metricStore.instance.data.mobsUrl]) - if (!metricStore.instance.data?.mobsUrl) return
No Data for selected period or URL.
+ if (!metricStore.instance.data?.mobsUrl) return
No Data for selected period or URL.
if (!visitedEvents || !visitedEvents.length) { - return
Loading session
+ return
Loading session
} const searchUrl = metricStore.instance.series[0].filter.filters[0].value[0] From ec2aabf8779fa0efe6f19ad46c676ac090fab3c1 Mon Sep 17 00:00:00 2001 From: sylenien Date: Fri, 23 Dec 2022 11:22:18 +0100 Subject: [PATCH 23/56] change(ui): some more changes to chec clickmap, clickrage subtype --- .../ClickMapCard/ClickMapCard.tsx | 45 ++++++++++++++++--- .../components/WidgetChart/WidgetChart.tsx | 2 +- .../components/WidgetForm/WidgetForm.tsx | 21 ++++++++- frontend/app/components/Session/WebPlayer.tsx | 18 +------- .../shared/Filters/FilterList/FilterList.tsx | 2 +- frontend/app/player/web/Screen/Screen.ts | 20 +++++---- frontend/app/types/filter/filterType.ts | 2 +- 7 files changed, 75 insertions(+), 35 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx index 8c187e10c..1e3f4b078 100644 --- a/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx +++ b/frontend/app/components/Dashboard/Widgets/CustomMetricsWidgets/ClickMapCard/ClickMapCard.tsx @@ -4,9 +4,18 @@ import { observer } from 'mobx-react-lite' import WebPlayer from 'App/components/Session/WebPlayer' import { connect } from 'react-redux' import { setCustomSession } from 'App/duck/sessions' +import Period, { LAST_30_DAYS } from "Types/app/period"; +import { fetchInsights } from 'Duck/sessions'; -function ClickMapCard({ setCustomSession, visitedEvents }: any) { - const { metricStore } = useStore(); +function ClickMapCard({ + setCustomSession, + visitedEvents, + insights, + fetchInsights, + insightsFilters, + host, +}: any) { + const { metricStore, dashboardStore } = useStore(); const onMarkerClick = (s: string, innerText: string) => { metricStore.changeClickMapSearch(s, innerText) } @@ -15,9 +24,26 @@ function ClickMapCard({ setCustomSession, visitedEvents }: any) { if (metricStore.instance.data.mobsUrl) { setCustomSession(metricStore.instance.data) } - }, [metricStore.instance.data.mobsUrl]) + }, [metricStore.instance]) - if (!metricStore.instance.data?.mobsUrl) return
No Data for selected period or URL.
+ React.useEffect(() => { + if (visitedEvents.length) { + const urlOptions = visitedEvents.map(({ url, host }: any) => ({ label: url, value: url, host })) + const url = insightsFilters.url ? insightsFilters.url : host + urlOptions[0].value; + // @ts-ignore + // const { startDate, endDate, rangeValue } = Period({ rangeName: LAST_30_DAYS }).toJSON() + const rangeValue = dashboardStore.drillDownPeriod.rangeValue + const startDate = dashboardStore.drillDownPeriod.start + const endDate = dashboardStore.drillDownPeriod.end + fetchInsights({ ...insightsFilters, url, startDate, endDate, rangeValue }) + } + }, [visitedEvents]) + + if (!metricStore.instance.data.mobsUrl || insights.size === 0) { + return ( +
No Data for selected period or URL.
+ ) + } if (!visitedEvents || !visitedEvents.length) { return
Loading session
} @@ -28,6 +54,7 @@ function ClickMapCard({ setCustomSession, visitedEvents }: any) { return evt }) const jumpTimestamp = (jumpToEvent.timestamp - metricStore.instance.data.startTs) + jumpToEvent.domBuildingTime + return (
({ visitedEvents: state.getIn(['sessions', 'visitedEvents']) }), - { setCustomSession }) + (state: any) => ({ + insightsFilters: state.getIn(['sessions', 'insightFilters']), + visitedEvents: state.getIn(['sessions', 'visitedEvents']), + insights: state.getIn(['sessions', 'insights']), + host: state.getIn(['sessions', 'host']), + }), + { setCustomSession, fetchInsights } +) (observer(ClickMapCard)) diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 01faed546..ab6a34042 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -90,7 +90,7 @@ function WidgetChart(props: Props) { const timestmaps = drillDownPeriod.toTimestamps(); const payload = isWidget ? { ...params } : { ...metricParams, ...timestmaps, ...metric.toJson() }; debounceRequest(metric, payload, isWidget, !isWidget ? drillDownPeriod : period); - }, [drillDownPeriod, period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType]); + }, [drillDownPeriod, period, depsString, _metric.page, metric.metricType, metric.metricOf, metric.viewType, metric.metricValue]); const renderChart = () => { diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 4460f69a2..11aacfc73 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -41,6 +41,7 @@ function WidgetForm(props: Props) { const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); const writeOption = ({ value, name }: any) => { + console.log(name, value) value = Array.isArray(value) ? value : value.value; const obj: any = { [name]: value }; @@ -67,9 +68,12 @@ function WidgetForm(props: Props) { obj['metricOf'] = tableOptions[0].value; obj['viewType'] = 'table'; } + if (metric.metricType === CLICKMAP && value !== CLICKMAP) { + metric.series[0].filter.removeFilter(0) + } if (value === CLICKMAP) { obj['viewType'] = 'chart'; - + obj['metricValue'] = 'clicks' if (metric.series[0].filter.filters.length < 1) { metric.series[0].filter.addFilter({ ...clickmapFilter, @@ -90,7 +94,7 @@ function WidgetForm(props: Props) { console.error(e) } } - metricStore.save(metric, dashboardId).then((metric: any) => { + metricStore.save(metric).then((metric: any) => { if (wasCreating) { if (parseInt(dashboardId) > 0) { history.replace(withSiteId(dashboardMetricDetails(dashboardId, metric.metricId), siteId)); @@ -173,6 +177,19 @@ function WidgetForm(props: Props) { /> )} + + {metric.metricType === CLICKMAP && ( + <> + showing + - - )} -
diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index 8f88dce04..8ed5df9f4 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -6,8 +6,9 @@ import { SegmentSelection, Button, Icon } from 'UI'; import { observer } from 'mobx-react-lite'; import { FilterKey } from 'Types/filter/filterType'; import WidgetDateRange from '../WidgetDateRange/WidgetDateRange'; -// import Period, { LAST_24_HOURS, LAST_30_DAYS } from 'Types/app/period'; +import ClickMapRagePicker from "Components/Dashboard/components/ClickMapRagePicker"; import DashboardSelectionModal from '../DashboardSelectionModal/DashboardSelectionModal'; +import { CLICKMAP, TABLE, TIMESERIES } from "App/constants/card"; interface Props { className?: string; @@ -19,8 +20,8 @@ function WidgetPreview(props: Props) { const { metricStore, dashboardStore } = useStore(); const dashboards = dashboardStore.dashboards; const metric: any = metricStore.instance; - const isTimeSeries = metric.metricType === 'timeseries'; - const isTable = metric.metricType === 'table'; + const isTimeSeries = metric.metricType === TIMESERIES; + const isTable = metric.metricType === TABLE; const drillDownFilter = dashboardStore.drillDownFilter; const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS; // const period = dashboardStore.drillDownPeriod; @@ -85,6 +86,9 @@ function WidgetPreview(props: Props) { )}
+ {metric.metricType === CLICKMAP ? ( + + ) : null} {/* add to dashboard */} {metric.exists() && ( diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 7f0efdddd..6ae858062 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -19,6 +19,7 @@ export default class MetricStore { sessionsPage: number = 1; sessionsPageSize: number = 10; listView?: boolean = false + clickMapFilter: boolean = false clickMapSearch = '' clickMapLabel = '' @@ -41,6 +42,10 @@ export default class MetricStore { this[key] = value; } + setClickMaps(val: boolean) { + this.clickMapFilter = val + } + changeClickMapSearch(val: string, label: string) { this.clickMapSearch = val this.clickMapLabel = label diff --git a/frontend/app/player/web/addons/TargetMarker.ts b/frontend/app/player/web/addons/TargetMarker.ts index f063b8dca..bac17db8a 100644 --- a/frontend/app/player/web/addons/TargetMarker.ts +++ b/frontend/app/player/web/addons/TargetMarker.ts @@ -36,7 +36,7 @@ export interface State { export default class TargetMarker { - private clickMapOverlay: HTMLDivElement + private clickMapOverlay: HTMLDivElement | null = null private clickContainers: HTMLDivElement[] = [] private smallClicks: HTMLDivElement[] = [] private onMarkerClick: (selector: string, innerText: string) => void @@ -137,7 +137,7 @@ export default class TargetMarker { } - injectTargets(selections: { selector: string, count: number }[] | null) { + injectTargets(selections: { selector: string, count: number, clickRage?: boolean }[] | null) { if (selections) { const totalCount = selections.reduce((a, b) => { return a + b.count @@ -169,13 +169,13 @@ export default class TargetMarker { const containerId = `clickmap-bubble-${i}` bubbleContainer.id = containerId this.clickContainers.push(bubbleContainer) - Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left })) + Object.assign(bubbleContainer.style, clickmapStyles.bubbleContainer({ top, left, height })) const border = document.createElement("div") Object.assign(border.style, clickmapStyles.highlight({ width, height, top, left })) const smallClicksBubble = document.createElement("div") - smallClicksBubble.innerHTML = '' + s.count + smallClicksBubble.innerHTML = `${s.count}` const smallClicksId = containerId + '-small' smallClicksBubble.id = smallClicksId this.smallClicks.push(smallClicksBubble) @@ -211,14 +211,14 @@ export default class TargetMarker { }) } - Object.assign(smallClicksBubble.style, clickmapStyles.clicks) + Object.assign(smallClicksBubble.style, clickmapStyles.clicks({ top, height, isRage: s.clickRage })) border.appendChild(smallClicksBubble) overlay.appendChild(bubbleContainer) overlay.appendChild(border) }); - this.screen.getParentElement().appendChild(overlay) + this.screen.getParentElement()?.appendChild(overlay) } else { this.store.update({ markedTargets: null }); this.clickMapOverlay?.remove() diff --git a/frontend/app/player/web/addons/clickmapStyles.ts b/frontend/app/player/web/addons/clickmapStyles.ts index 522a2b285..f6ed76311 100644 --- a/frontend/app/player/web/addons/clickmapStyles.ts +++ b/frontend/app/player/web/addons/clickmapStyles.ts @@ -4,8 +4,8 @@ export const clickmapStyles = { position: 'absolute', top: '0px', left: '0px', - width: width, - height: height, + width, + height, background: 'rgba(0,0,0, 0.15)', zIndex: 9 * 10e3, transformOrigin: 'left top', @@ -14,18 +14,19 @@ export const clickmapStyles = { fontSize: '16px', fontWeight: '600', }, - bubbleContainer: ({ top, left }: { top: number; left: number }) => ({ + bubbleContainer: ({ top, left, height }: { top: number; left: number, height: number }) => ({ position: 'absolute', - top: top + 'px', - left: left + 'px', + top: top > 20 ? top + 'px' : height + 2 + 'px', + left: `${left}px`, padding: '10px', borderRadius: '6px', background: 'white', border: '1px solid rgba(0, 0, 0, 0.12)', boxShadow: '0px 2px 10px 2px rgba(0,0,0,0.5)', - transform: `translate(-25%, -110%)`, + transform: top > 20 ? 'translate(-25%, -110%)' : 'translate(-25%, 0%)', textAlign: 'center', visibility: 'hidden', + zIndex: 2, }), highlight: ({ width, @@ -38,22 +39,23 @@ export const clickmapStyles = { top: number; left: number; }) => ({ - width: width + 'px', - height: height + 'px', + width: `${width}px`, + height: `${height}px`, border: '2px dotted red', cursor: 'pointer', - top: top + 'px', - left: left + 'px', + top: `${top}px`, + left: `${left}px`, position: 'absolute', }), - clicks: { - top: 0, + clicks: ({ top, height, isRage }: { top: number; height: number, isRage?: boolean }) => ({ + top: top > 20 ? 0 : `${height}px`, left: 0, position: 'absolute', borderRadius: '999px', padding: '6px', - background: 'white', + background: isRage ? 'red' : 'white', + color: isRage ? 'white' : 'black', lineHeight: '0.5', transform: 'translate(-70%, -70%)', - }, + }), }; From 9ff3b2f017173e4cc38c7f408bb634bd5db6cc6d Mon Sep 17 00:00:00 2001 From: sylenien Date: Mon, 26 Dec 2022 15:14:21 +0100 Subject: [PATCH 26/56] change(ui): fix labels, small refactoring --- .../components/WidgetPreview/WidgetPreview.tsx | 17 +++-------------- .../WidgetSessions/WidgetSessions.tsx | 4 +--- frontend/app/duck/user.js | 5 ----- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx index 8ed5df9f4..3bbc1c022 100644 --- a/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx +++ b/frontend/app/components/Dashboard/components/WidgetPreview/WidgetPreview.tsx @@ -22,23 +22,12 @@ function WidgetPreview(props: Props) { const metric: any = metricStore.instance; const isTimeSeries = metric.metricType === TIMESERIES; const isTable = metric.metricType === TABLE; - const drillDownFilter = dashboardStore.drillDownFilter; const disableVisualization = metric.metricOf === FilterKey.SESSIONS || metric.metricOf === FilterKey.ERRORS; - // const period = dashboardStore.drillDownPeriod; - const chagneViewType = (e, { name, value }: any) => { + const changeViewType = (_, { name, value }: any) => { metric.update({ [ name ]: value }); } - // const onChangePeriod = (period: any) => { - // dashboardStore.setDrillDownPeriod(period); - // const periodTimestamps = period.toTimestamps(); - // drillDownFilter.merge({ - // startTimestamp: periodTimestamps.startTimestamp, - // endTimestamp: periodTimestamps.endTimestamp, - // }) - // } - const canAddToDashboard = metric.exists() && dashboards.length > 0; return ( @@ -57,7 +46,7 @@ function WidgetPreview(props: Props) { className="my-3" primary icons={true} - onSelect={ chagneViewType } + onSelect={ changeViewType } value={{ value: metric.viewType }} list={ [ { value: 'lineChart', name: 'Chart', icon: 'graph-up-arrow' }, @@ -75,7 +64,7 @@ function WidgetPreview(props: Props) { className="my-3" primary={true} icons={true} - onSelect={ chagneViewType } + onSelect={ changeViewType } value={{ value: metric.viewType }} list={[ { value: 'table', name: 'Table', icon: 'table' }, diff --git a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx index 316bb55e9..ebb07b49c 100644 --- a/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx +++ b/frontend/app/components/Dashboard/components/WidgetSessions/WidgetSessions.tsx @@ -90,15 +90,13 @@ function WidgetSessions(props: Props) { } }, [filter.startTimestamp, filter.endTimestamp, filter.filters, depsString, metricStore.sessionsPage, metricStore.clickMapSearch]); - - return (

{metricStore.clickMapSearch ? 'Clicks' : 'Sessions'}

- {metricStore.clickMapLabel ? `"${metricStore.clickMapLabel}" ` : null} + {metricStore.clickMapLabel ? `on "${metricStore.clickMapLabel}" ` : null} between {startTime} and{' '} {endTime}{' '}
diff --git a/frontend/app/duck/user.js b/frontend/app/duck/user.js index 1420cae45..b5c076088 100644 --- a/frontend/app/duck/user.js +++ b/frontend/app/duck/user.js @@ -21,7 +21,6 @@ const PUSH_NEW_SITE = 'user/PUSH_NEW_SITE'; const SET_ONBOARDING = 'user/SET_ONBOARDING'; const initialState = Map({ - // client: Client(), account: Account(), siteId: null, passwordRequestError: false, @@ -121,10 +120,6 @@ export const fetchUserInfo = () => dispatch => Promise.all([ types: FETCH_ACCOUNT.toArray(), call: client => client.get('/account'), }), - // dispatch({ - // types: FETCH_CLIENT.toArray(), - // call: client => client.get('/client'), - // }), ]); export function logout() { From 7fb72a7f20d8fded61f0de5ebbd4a4a5b91ec842 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 09:31:45 +0100 Subject: [PATCH 27/56] feat(ui) - dashboard improvements - api - integration and other fixes --- .../MetricListItem/MetricListItem.tsx | 4 +- .../MetricsLibraryModal.tsx | 11 ++- .../components/MetricsList/ListView.tsx | 4 +- .../components/WidgetChart/WidgetChart.tsx | 22 ++--- .../components/WidgetForm/WidgetForm.tsx | 6 +- .../WidgetPredefinedChart.tsx | 51 +++++------ .../ui/SegmentSelection/SegmentSelection.js | 4 +- frontend/app/constants/card.ts | 68 +++++++------- frontend/app/mstore/metricStore.ts | 3 +- frontend/app/services/MetricService.ts | 2 +- frontend/app/types/filter/filterType.ts | 88 +++++++++---------- 11 files changed, 141 insertions(+), 122 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index df3b09180..b8c7a3115 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -46,13 +46,13 @@ function MetricListItem(props: Props) { onClick={onItemClick} >
- {/* */} + />
{metric.name}
diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 308924a12..0593e839e 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -1,20 +1,23 @@ import Modal from 'App/components/Modal/Modal'; -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import MetricsList from '../MetricsList'; import { Button } from 'UI'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; +import { observer, useObserver } from 'mobx-react-lite'; interface Props { dashboardId: number; siteId: string; } function MetricsLibraryModal(props: Props) { + const { metricStore } = useStore(); const { siteId, dashboardId } = props; const [selectedList, setSelectedList] = useState([]); - console.log('dashboardId', dashboardId); + useEffect(() => { + metricStore.updateKey('listView', true) + }, []) const onSelectionChange = (list: any) => { setSelectedList(list); @@ -34,7 +37,7 @@ function MetricsLibraryModal(props: Props) { ); } -export default MetricsLibraryModal; +export default observer(MetricsLibraryModal); function SelectedContent({ dashboardId, selected }: any) { const { hideModal } = useModal(); diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index cd213406d..f7129a97a 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -13,7 +13,7 @@ function ListView(props: Props) { return (
- {/*
+
selectedList(list.map((i: any) => i.metricId))} /> Title -
*/} +
Owner
Visibility
Last Modified
diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index ab6a34042..3b8779c2c 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -13,7 +13,7 @@ import { getStartAndEndTimestampsByDensity } from 'Types/dashboard/helper'; import { debounce } from 'App/utils'; import useIsMounted from 'App/hooks/useIsMounted' import { FilterKey } from 'Types/filter/filterType'; -import { CLICKMAP } from 'App/constants/card'; +import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS } from 'App/constants/card'; import FunnelWidget from 'App/components/Funnels/FunnelWidget'; import ErrorsWidget from '../Errors/ErrorsWidget'; import SessionWidget from '../Sessions/SessionWidget'; @@ -101,23 +101,25 @@ function WidgetChart(props: Props) { return } - if (metricType === 'errors') { - return - } + // if (metricType === ERRORS) { + // return + // } - if (metricType === 'funnel') { + if (metricType === FUNNEL) { return } - if (metricType === 'predefined') { + if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) { const defaultMetric = metric.data.chart.length === 0 ? metricWithData : metric if (isOverviewWidget) { return } - return + return } - if (metricType === 'timeseries') { + // TODO add USER_PATH, RETENTION, FEATUER_ADOPTION + + if (metricType === TIMESERIES) { if (viewType === 'lineChart') { return ( ) } - if (viewType === 'table') { + if (viewType === TABLE) { return ( { switch (predefinedKey) { // ERRORS - case 'errors_per_type': + case FilterKey.ERRORS_PER_TYPE: return - case 'errors_per_domains': + case FilterKey.ERRORS_PER_DOMAINS: return - case 'resources_by_party': + case FilterKey.RESOURCES_BY_PARTY: return - case 'impacted_sessions_by_js_errors': + case FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS: return - case 'domains_errors_4xx': + case FilterKey.DOMAINS_ERRORS_4XX: return - case 'domains_errors_5xx': + case FilterKey.DOMAINS_ERRORS_5XX: return - case 'calls_errors': + case FilterKey.CALLS_ERRORS: return // PERFORMANCE - case 'impacted_sessions_by_slow_pages': + case FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES: return - case 'pages_response_time_distribution': + case FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION: return - case 'speed_location': + case FilterKey.SPEED_LOCATION: return - case 'cpu': + case FilterKey.CPU: return - case 'crashes': + case FilterKey.CRASHES: return - case 'pages_dom_buildtime': + case FilterKey.PAGES_DOM_BUILD_TIME: return - case 'fps': + case FilterKey.FPS: return - case 'memory_consumption': + case FilterKey.MEMORY_CONSUMPTION: return - case 'pages_response_time': + case FilterKey.PAGES_RESPONSE_TIME: return - case 'resources_vs_visually_complete': + case FilterKey.RESOURCES_VS_VISUALLY_COMPLETE: return - case 'sessions_per_browser': + case FilterKey.SESSIONS_PER_BROWSER: return - case 'slowest_domains': + case FilterKey.SLOWEST_DOMAINS: return - case 'time_to_render': + case FilterKey.TIME_TO_RENDER: return // Resources - case 'resources_count_by_type': + case FilterKey.BREAKDOWN_OF_LOADED_RESOURCES: return - case 'missing_resources': + case FilterKey.MISSING_RESOURCES: return - case 'resource_type_vs_response_end': + case FilterKey.RESOURCE_TYPE_VS_RESPONSE_END: return - case 'resources_loading_time': + case FilterKey.RESOURCES_LOADING_TIME: return - case 'slowest_resources': + case FilterKey.SLOWEST_RESOURCES: return default: diff --git a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js index d3c71b6cf..36307eab3 100644 --- a/frontend/app/components/ui/SegmentSelection/SegmentSelection.js +++ b/frontend/app/components/ui/SegmentSelection/SegmentSelection.js @@ -38,9 +38,9 @@ class SegmentSelection extends React.Component { className )} > - {list.map((item) => ( + {list.map((item, i) => (
!item.disabled && this.setActiveItem(item)} diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 78626f8c1..8ce72f8c8 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -12,7 +12,15 @@ export interface CardType { export const LIBRARY = 'library'; export const TIMESERIES = 'timeseries'; export const TABLE = 'table'; -export const CLICKMAP = 'clickMap' +export const CLICKMAP = 'clickMap'; +export const FUNNEL = 'funnel'; +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 RETENTION = 'retention'; +export const FEATURE_ADOPTION = 'featureAdoption'; export const TYPES: CardType[] = [ { @@ -57,22 +65,22 @@ export const TYPES: CardType[] = [ title: 'Funnel', icon: 'funnel', description: 'Uncover the issues impacting user journeys.', - slug: 'funnel', + slug: FUNNEL, }, { title: 'Errors Tracking', icon: 'exclamation-circle', description: 'Discover user journeys between 2 points.', - slug: 'errors', + slug: ERRORS, subTypes: [ { title: 'Resources by Party', slug: FilterKey.RESOURCES_BY_PARTY, description: '' }, { title: 'Errors per Domains', slug: FilterKey.ERRORS_PER_DOMAINS, description: '' }, { title: 'Errors per type', slug: FilterKey.ERRORS_PER_TYPE, description: '' }, - { title: 'Calls_Errors', slug: FilterKey.CALLS_ERRORS, description: '' }, - { title: 'Domains_Errors_4xx', slug: FilterKey.DOMAINS_ERRORS_4XX, description: '' }, - { title: 'Domains_Errors_5xx', slug: FilterKey.DOMAINS_ERRORS_5XX, description: '' }, + { title: 'Calls Errors', slug: FilterKey.CALLS_ERRORS, description: '' }, + { title: 'Domains Errors 4xx', slug: FilterKey.DOMAINS_ERRORS_4XX, description: '' }, + { title: 'Domains Errors 5xx', slug: FilterKey.DOMAINS_ERRORS_5XX, description: '' }, { - title: 'Impacted_Sessions_By_Js_Errors', + title: 'Impacted Sessions by JS Errors', slug: FilterKey.IMPACTED_SESSIONS_BY_JS_ERRORS, description: '', }, @@ -82,30 +90,30 @@ export const TYPES: CardType[] = [ title: 'Performance Monitoring', icon: 'speedometer2', description: 'Retention graph of users / features over a period of time.', - slug: 'performance', + slug: PERFORMANCE, subTypes: [ { title: 'Cpu', slug: FilterKey.CPU, description: '' }, { title: 'Crashes', slug: FilterKey.CRASHES, description: '' }, { title: 'Fps', slug: FilterKey.FPS, description: '' }, - { title: 'Pages_Dom_Build_Time', slug: FilterKey.PAGES_DOM_BUILD_TIME, description: '' }, - { title: 'Memory_Consumption', slug: FilterKey.MEMORY_CONSUMPTION, description: '' }, - { title: 'Pages_Response_Time', slug: FilterKey.PAGES_RESPONSE_TIME, description: '' }, + { title: 'Pages Dom Build Time', slug: FilterKey.PAGES_DOM_BUILD_TIME, description: '' }, + { title: 'Memory Consumption', slug: FilterKey.MEMORY_CONSUMPTION, description: '' }, + { title: 'Pages Response Time', slug: FilterKey.PAGES_RESPONSE_TIME, description: '' }, { - title: 'Pages_Response_Time_Distribution', + title: 'Pages Response Time Distribution', slug: FilterKey.PAGES_RESPONSE_TIME_DISTRIBUTION, description: '', }, { - title: 'Resources_Vs_Visually_Complete', + title: 'Resources Vs Visually Complete', slug: FilterKey.RESOURCES_VS_VISUALLY_COMPLETE, description: '', }, - { title: 'Sessions_Per_Browser', slug: FilterKey.SESSIONS_PER_BROWSER, description: '' }, - { title: 'Slowest_Domains', slug: FilterKey.SLOWEST_DOMAINS, description: '' }, - { title: 'Speed_Location', slug: FilterKey.SPEED_LOCATION, description: '' }, - { title: 'Time_To_Render', slug: FilterKey.TIME_TO_RENDER, description: '' }, + { title: 'Sessions Per Browser', slug: FilterKey.SESSIONS_PER_BROWSER, description: '' }, + { title: 'Slowest Domains', slug: FilterKey.SLOWEST_DOMAINS, description: '' }, + { title: 'Speed Location', slug: FilterKey.SPEED_LOCATION, description: '' }, + { title: 'Time To Render', slug: FilterKey.TIME_TO_RENDER, description: '' }, { - title: 'Impacted_Sessions_By_Slow_Pages', + title: 'Impacted Sessions By Slow Pages', slug: FilterKey.IMPACTED_SESSIONS_BY_SLOW_PAGES, description: '', }, @@ -115,35 +123,35 @@ export const TYPES: CardType[] = [ title: 'Resource Monitoring', icon: 'files', description: 'Find the adoption of your all features in your app.', - slug: 'resource-monitoring', + slug: RESOURCE_MONITORING, subTypes: [ { - title: 'Breakdown_Of_Loaded_Resources', + title: 'Breakdown of Loaded Resources', slug: FilterKey.BREAKDOWN_OF_LOADED_RESOURCES, description: '', }, - { title: 'Missing_Resources', slug: FilterKey.MISSING_RESOURCES, description: '' }, + { title: 'Missing Resources', slug: FilterKey.MISSING_RESOURCES, description: '' }, { - title: 'Resource_Type_Vs_Response_End', + title: 'Resource Type vs Response End', slug: FilterKey.RESOURCE_TYPE_VS_RESPONSE_END, description: '', }, - { title: 'Resource_Fetch_Time', slug: FilterKey.RESOURCE_FETCH_TIME, description: '' }, - { title: 'Slowest_Resources', slug: FilterKey.SLOWEST_RESOURCES, description: '' }, + { title: 'Resource Fetch Time', slug: FilterKey.RESOURCE_FETCH_TIME, description: '' }, + { title: 'Slowest Resources', slug: FilterKey.SLOWEST_RESOURCES, description: '' }, ], }, { title: 'Web Vitals', icon: 'activity', description: 'Find the adoption of your all features in your app.', - slug: 'web-vitals', + slug: WEB_VITALS, subTypes: [ { - title: 'Resources_Count_By_Type', + title: 'Resources Count By Type', slug: FilterKey.RESOURCES_COUNT_BY_TYPE, description: '', }, - { title: 'Resources_Loading_Time', slug: FilterKey.RESOURCES_LOADING_TIME, description: '' }, + { title: 'Resources Loading Time', slug: FilterKey.RESOURCES_LOADING_TIME, description: '' }, { title: 'CPU Load', slug: FilterKey.AVG_CPU, @@ -206,18 +214,18 @@ export const TYPES: CardType[] = [ title: 'User Path', icon: 'signpost-split', description: 'Discover user journeys between 2 points.', - slug: 'user-path', + slug: USER_PATH, }, { title: 'Retention', icon: 'arrow-repeat', description: 'Retension graph of users / features over a period of time.', - slug: 'retention', + slug: RETENTION, }, { title: 'Feature Adoption', icon: 'card-checklist', description: 'Find the adoption of your all features in your app.', - slug: 'feature-adoption', + slug: FEATURE_ADOPTION, }, ]; diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 6ae858062..24d848d0c 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -18,7 +18,7 @@ export default class MetricStore { sessionsPage: number = 1; sessionsPageSize: number = 10; - listView?: boolean = false + listView?: boolean = true clickMapFilter: boolean = false clickMapSearch = '' @@ -38,6 +38,7 @@ export default class MetricStore { } updateKey(key: string, value: any) { + console.log('up'); // @ts-ignore this[key] = value; } diff --git a/frontend/app/services/MetricService.ts b/frontend/app/services/MetricService.ts index 7be2ee9d3..4b482a4a0 100644 --- a/frontend/app/services/MetricService.ts +++ b/frontend/app/services/MetricService.ts @@ -18,7 +18,7 @@ export default class MetricService { * @returns {Promise} */ getMetrics(): Promise { - return this.client.post('/cards/search', { limit: 100 }) + return this.client.get('/cards') .then((response: { json: () => any; }) => response.json()) .then((response: { data: any; }) => response.data || []); } diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index b696a0d22..0f0f1bdc8 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -135,18 +135,18 @@ export const getFilterKeyTypeByKey = (key: string) => { }; export enum IssueType { - CLICK_RAGE = 'clickRage', - DEAD_CLICK = 'deadClick', - EXCESSIVE_SCROLLING = 'excessiveScrolling', - BAD_REQUEST = 'badRequest', - MISSING_RESOURCE = 'missingResource', + CLICK_RAGE = 'click_rage', + DEAD_CLICK = 'dead_click', + EXCESSIVE_SCROLLING = 'excessive_scrolling', + BAD_REQUEST = 'bad_request', + MISSING_RESOURCE = 'missing_resource', MEMORY = 'memory', CPU = 'cpu', - SLOW_RESOURCE = 'slowResource', - SLOW_PAGE_LOAD = 'slowPageLoad', + SLOW_RESOURCE = 'slow_resource', + SLOW_PAGE_LOAD = 'slow_pageLoad', CRASH = 'crash', CUSTOM = 'custom', - JS_EXCEPTION = 'jsException', + JS_EXCEPTION = 'js_exception', } export enum FilterType { @@ -165,39 +165,39 @@ export enum FilterType { } export enum FilterKey { - ERROR = 'ERROR', - MISSING_RESOURCE = 'MISSING_RESOURCE', - SLOW_SESSION = 'SLOW_SESSION', - CLICK_RAGE = 'CLICK_RAGE', - CLICK = 'CLICK', - INPUT = 'INPUT', - LOCATION = 'LOCATION', - VIEW = 'VIEW', - CONSOLE = 'CONSOLE', - METADATA = 'METADATA', - CUSTOM = 'CUSTOM', - URL = 'URL', - USER_BROWSER = 'USERBROWSER', - USER_OS = 'USEROS', - USER_DEVICE = 'USERDEVICE', - PLATFORM = 'PLATFORM', - DURATION = 'DURATION', - REFERRER = 'REFERRER', - USER_COUNTRY = 'USERCOUNTRY', - JOURNEY = 'JOURNEY', - REQUEST = 'REQUEST', - GRAPHQL = 'GRAPHQL', - STATEACTION = 'STATEACTION', - REVID = 'REVID', - USERANONYMOUSID = 'USERANONYMOUSID', - USERID = 'USERID', - ISSUE = 'ISSUE', - EVENTS_COUNT = 'EVENTS_COUNT', - UTM_SOURCE = 'UTM_SOURCE', - UTM_MEDIUM = 'UTM_MEDIUM', - UTM_CAMPAIGN = 'UTM_CAMPAIGN', + ERROR = 'error', + MISSING_RESOURCE = 'missingResource', + SLOW_SESSION = 'slowSession', + CLICK_RAGE = 'clickRage', + CLICK = 'click', + INPUT = 'input', + LOCATION = 'location', + VIEW = 'view', + CONSOLE = 'console', + METADATA = 'metadata', + CUSTOM = 'custom', + URL = 'url', + USER_BROWSER = 'userBrowser', + USER_OS = 'userOs', + USER_DEVICE = 'userDevice', + PLATFORM = 'platform', + DURATION = 'duration', + REFERRER = 'preferrer', + USER_COUNTRY = 'userCountry', + JOURNEY = 'journey', + REQUEST = 'request', + GRAPHQL = 'graphql', + STATEACTION = 'stateAction', + REVID = 'revId', + USERANONYMOUSID = 'userAnonymousId', + USERID = 'userId', + ISSUE = 'issue', + EVENTS_COUNT = 'eventsCount', + UTM_SOURCE = 'utmSource', + UTM_MEDIUM = 'utmMedium', + UTM_CAMPAIGN = 'utmCampaign', - DOM_COMPLETE = 'DOM_COMPLETE', + DOM_COMPLETE = 'domComplete', LARGEST_CONTENTFUL_PAINT_TIME = 'LARGEST_CONTENTFUL_PAINT_TIME', TIME_BETWEEN_EVENTS = 'TIME_BETWEEN_EVENTS', TTFB = 'TTFB', @@ -218,8 +218,8 @@ export enum FilterKey { GRAPHQL_REQUEST_BODY = 'GRAPHQL_REQUEST_BODY', GRAPHQL_RESPONSE_BODY = 'GRAPHQL_RESPONSE_BODY', - SESSIONS = 'SESSIONS', - ERRORS = 'js_exception', + SESSIONS = 'sessions', + ERRORS = 'jsException', RESOURCES_COUNT_BY_TYPE = 'resourcesCountByType', RESOURCES_LOADING_TIME = 'resourcesLoadingTime', @@ -249,8 +249,8 @@ export enum FilterKey { ERRORS_PER_DOMAINS = 'errorsPerDomains', ERRORS_PER_TYPE = 'errorsPerType', CALLS_ERRORS = 'callsErrors', - DOMAINS_ERRORS_4XX = 'domainsErrors4Xx', - DOMAINS_ERRORS_5XX = 'domainsErrors5Xx', + DOMAINS_ERRORS_4XX = 'domainsErrors4xx', + DOMAINS_ERRORS_5XX = 'domainsErrors5xx', IMPACTED_SESSIONS_BY_JS_ERRORS = 'impactedSessionsByJsErrors', // Performance From b4982a55f3bc952412d630eda6d21841084fb7a1 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 11:27:09 +0100 Subject: [PATCH 28/56] feat(ui) - cards - metric selection and remove add buttons --- .../DashboardWidgetGrid/DashboardWidgetGrid.tsx | 4 ---- .../MetricsLibraryModal/MetricsLibraryModal.tsx | 1 - .../Dashboard/components/MetricsList/ListView.tsx | 8 +++++--- .../Dashboard/components/MetricsList/MetricsList.tsx | 1 + 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx index f0d5bcd67..515d3eaa2 100644 --- a/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx +++ b/frontend/app/components/Dashboard/components/DashboardWidgetGrid/DashboardWidgetGrid.tsx @@ -3,7 +3,6 @@ import { useStore } from 'App/mstore'; import WidgetWrapper from '../WidgetWrapper'; import { NoContent, Loader, Icon } from 'UI'; import { useObserver } from 'mobx-react-lite'; -import AddMetricContainer from './AddMetricContainer'; import Widget from 'App/mstore/types/widget'; import MetricTypeList from '../MetricTypeList'; @@ -104,9 +103,6 @@ function DashboardWidgetGrid(props: Props) { /> ))} -
- -
diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 0593e839e..caa6e0bb1 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -30,7 +30,6 @@ function MetricsLibraryModal(props: Props) {
- {/* TODO should show the dynamic values */} diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index f7129a97a..b31df78ac 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -7,6 +7,7 @@ interface Props { siteId: any; selectedList: any; toggleSelection?: (metricId: any) => void; + toggleAll?: (e: any) => void; } function ListView(props: Props) { const { siteId, list, selectedList, toggleSelection } = props; @@ -18,8 +19,9 @@ function ListView(props: Props) { name="slack" className="mr-4" type="checkbox" - checked={false} - onClick={() => selectedList(list.map((i: any) => i.metricId))} + checked={selectedList.length === list.length} + // onClick={() => selectedList(list.map((i: any) => i.metricId))} + onClick={props.toggleAll} /> Title
@@ -34,7 +36,7 @@ function ListView(props: Props) { selected={selectedList.includes(parseInt(metric.metricId))} toggleSelection={(e: any) => { e.stopPropagation(); - toggleSelection(parseInt(metric.metricId)); + toggleSelection && toggleSelection(parseInt(metric.metricId)); }} /> ))} diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index d3167de57..f11eb9bf2 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -69,6 +69,7 @@ function MetricsList({ list={sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize)} selectedList={selectedMetrics} toggleSelection={toggleMetricSelection} + toggleAll={({ target: { checked, name } }) => setSelectedMetrics(checked ? list.map((i: any) => i.metricId) : [])} /> ) : ( Date: Mon, 2 Jan 2023 11:35:31 +0100 Subject: [PATCH 29/56] feat(ui) - cards - cards modal footer alignment --- .../MetricsLibraryModal/MetricsLibraryModal.tsx | 8 +++++--- frontend/app/components/Modal/Modal.tsx | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index caa6e0bb1..155c3c123 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -26,12 +26,14 @@ function MetricsLibraryModal(props: Props) { return ( <> - +
-
+ + + ); } @@ -49,7 +51,7 @@ function SelectedContent({ dashboardId, selected }: any) { }; return ( -
+
Selected {selected.length} of{' '} {total} diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index ab4a5fa23..fb375333e 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -50,4 +50,8 @@ Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: s return
{children}
; }; +Modal.Footer = ({ children, className = ''} : any) => { + return
{children}
; +} + export default Modal; From 9c86c8f4fc62f4eb6b9e5de89a6029a80a9e2b39 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 11:44:58 +0100 Subject: [PATCH 30/56] feat(ui) - cards - add selected to the dashboard --- .../components/DashboardView/DashboardView.tsx | 2 +- .../MetricsLibraryModal/MetricsLibraryModal.tsx | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index 5ac13ef9f..3579baae3 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -import { Loader, Tooltip, Popover } from 'UI'; +import { Loader } from 'UI'; import { withSiteId } from 'App/routes'; import withModal from 'App/components/Modal/withModal'; import DashboardWidgetGrid from '../DashboardWidgetGrid'; diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 155c3c123..5755211c1 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -16,8 +16,8 @@ function MetricsLibraryModal(props: Props) { const [selectedList, setSelectedList] = useState([]); useEffect(() => { - metricStore.updateKey('listView', true) - }, []) + metricStore.updateKey('listView', true); + }, []); const onSelectionChange = (list: any) => { setSelectedList(list); @@ -26,7 +26,7 @@ function MetricsLibraryModal(props: Props) { return ( <> - +
@@ -47,7 +47,11 @@ function SelectedContent({ dashboardId, selected }: any) { const dashboard = useMemo(() => dashboardStore.getDashboard(dashboardId), [dashboardId]); const addSelectedToDashboard = () => { - dashboardStore.addWidgetToDashboard(dashboard, selected).then(hideModal); + if (!dashboard || !dashboard.dashboardId) return; + dashboardStore.addWidgetToDashboard(dashboard, selected).then(() => { + hideModal(); + dashboardStore.fetch(dashboard.dashboardId); + }); }; return ( From 6ff4d74131958b11f9d6764dc8936581979d5120 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 13:43:56 +0100 Subject: [PATCH 31/56] feat(ui) - cards - metric search and other changes --- .../MetricTypeList/MetricTypeList.tsx | 6 +++- .../MetricsLibraryModal.tsx | 31 +++++++++++++++++-- .../components/WidgetChart/WidgetChart.tsx | 2 +- .../components/WidgetForm/WidgetForm.tsx | 8 ++--- frontend/app/components/Modal/Modal.tsx | 31 ++++++++++++++----- frontend/app/constants/card.ts | 6 ---- frontend/app/mstore/metricStore.ts | 7 ++++- frontend/app/mstore/types/widget.ts | 3 +- frontend/app/types/filter/filterType.ts | 2 +- 9 files changed, 72 insertions(+), 24 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx index 8e64d970d..e53d6e7a2 100644 --- a/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx +++ b/frontend/app/components/Dashboard/components/MetricTypeList/MetricTypeList.tsx @@ -5,6 +5,7 @@ import MetricTypeItem, { MetricType } from '../MetricTypeItem/MetricTypeItem'; import { TYPES, LIBRARY } from 'App/constants/card'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { dashboardMetricCreate, metricCreate, withSiteId } from 'App/routes'; +import { useStore } from 'App/mstore'; interface Props extends RouteComponentProps { dashboardId: number; @@ -12,13 +13,16 @@ interface Props extends RouteComponentProps { } function MetricTypeList(props: Props) { const { dashboardId, siteId, history } = props; + const { metricStore } = useStore(); const { hideModal } = useModal(); const { showModal } = useModal(); const onClick = ({ slug }: MetricType) => { hideModal(); if (slug === LIBRARY) { - return showModal(, { right: true, width: 800 }); + return showModal(, { right: true, width: 800, onClose: () => { + metricStore.updateKey('metricsSearch', '') + } }); } // TODO redirect to card builder with metricType query param diff --git a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx index 5755211c1..5227b7171 100644 --- a/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx +++ b/frontend/app/components/Dashboard/components/MetricsLibraryModal/MetricsLibraryModal.tsx @@ -1,7 +1,7 @@ import Modal from 'App/components/Modal/Modal'; import React, { useEffect, useMemo, useState } from 'react'; import MetricsList from '../MetricsList'; -import { Button } from 'UI'; +import { Button, Icon } from 'UI'; import { useModal } from 'App/components/Modal'; import { useStore } from 'App/mstore'; import { observer, useObserver } from 'mobx-react-lite'; @@ -23,9 +23,22 @@ function MetricsLibraryModal(props: Props) { setSelectedList(list); }; + const onChange = ({ target: { value } }: any) => { + metricStore.updateKey('metricsSearch', value) + }; + return ( <> - + +
+
+
Cards Library
+
+
+ +
+
+
@@ -40,6 +53,20 @@ function MetricsLibraryModal(props: Props) { export default observer(MetricsLibraryModal); +function MetricSearch({ onChange }: any) { + return ( +
+ + +
+ ); +} + function SelectedContent({ dashboardId, selected }: any) { const { hideModal } = useModal(); const { metricStore, dashboardStore } = useStore(); diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 3b8779c2c..7cc3cee08 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -37,7 +37,7 @@ function WidgetChart(props: Props) { const drillDownFilter = dashboardStore.drillDownFilter; const colors = Styles.customMetricColors; const [loading, setLoading] = useState(true) - const isOverviewWidget = metric.metricType === 'predefined' && metric.viewType === 'overview'; + const isOverviewWidget = metric.metricType === WEB_VITALS; const params = { density: isOverviewWidget ? 7 : 70 } const metricParams = { ...params } const prevMetricRef = useRef(); diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 494fc975b..35f4be01f 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -10,7 +10,7 @@ import Select from 'Shared/Select'; import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; import MetricTypeDropdown from './components/MetricTypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; -import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS } from 'App/constants/card'; +import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING } from 'App/constants/card'; import { clickmapFilter } from 'App/types/filter/newFilter'; import { renderClickmapThumbnail } from './renderMap' @@ -41,7 +41,6 @@ function WidgetForm(props: Props) { const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); const writeOption = ({ value, name }: any) => { - console.log(name, value) value = Array.isArray(value) ? value : value.value; const obj: any = { [name]: value }; @@ -69,9 +68,10 @@ function WidgetForm(props: Props) { obj['viewType'] = 'table'; } else if (value === FUNNEL) { obj['metricOf'] = 'sessionCount'; - } else if (value === ERRORS) { + } else if (value === ERRORS || value === RESOURCE_MONITORING) { obj['viewType'] = 'chart'; - } + } + if (metric.metricType === CLICKMAP && value !== CLICKMAP) { metric.series[0].filter.removeFilter(0) } diff --git a/frontend/app/components/Modal/Modal.tsx b/frontend/app/components/Modal/Modal.tsx index fb375333e..d2e8a838d 100644 --- a/frontend/app/components/Modal/Modal.tsx +++ b/frontend/app/components/Modal/Modal.tsx @@ -21,7 +21,9 @@ function Modal({ component, className = 'bg-white', props, hideModal }: Props) { document.querySelector('body').style.overflow = 'visible'; } }); - });return component ? ( + }); + + return component ? ( ReactDOM.createPortal(
{ - return ( +Modal.Header = ({ title, children }: { title?: string, children?: any }) => { + return !!children ? ( +
+ {children} +
+ ): (
{title}
@@ -47,11 +53,22 @@ Modal.Header = ({ title }: { title: string }) => { }; Modal.Content = ({ children, className = 'p-4' }: { children: any; className?: string }) => { - return
{children}
; + return ( +
+ {children} +
+ ); }; -Modal.Footer = ({ children, className = ''} : any) => { - return
{children}
; -} +Modal.Footer = ({ children, className = '' }: any) => { + return ( +
+ {children} +
+ ); +}; export default Modal; diff --git a/frontend/app/constants/card.ts b/frontend/app/constants/card.ts index 8ce72f8c8..24cddf9b3 100644 --- a/frontend/app/constants/card.ts +++ b/frontend/app/constants/card.ts @@ -146,12 +146,6 @@ export const TYPES: CardType[] = [ description: 'Find the adoption of your all features in your app.', slug: WEB_VITALS, subTypes: [ - { - title: 'Resources Count By Type', - slug: FilterKey.RESOURCES_COUNT_BY_TYPE, - description: '', - }, - { title: 'Resources Loading Time', slug: FilterKey.RESOURCES_LOADING_TIME, description: '' }, { title: 'CPU Load', slug: FilterKey.AVG_CPU, diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index 24d848d0c..df403bd5e 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -3,6 +3,7 @@ import Widget from './types/widget'; import { metricService, errorService } from 'App/services'; import { toast } from 'react-toastify'; import Error from './types/error'; +import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS } from 'App/constants/card'; export default class MetricStore { isLoading: boolean = false; @@ -58,7 +59,11 @@ export default class MetricStore { } changeType(value: string) { - this.instance.update({ metricType: value}) + const obj: any = { metricType: value}; + if (value === ERRORS || value === RESOURCE_MONITORING || value === PERFORMANCE || value === WEB_VITALS) { + obj['viewType'] = 'chart'; + } + this.instance.update(obj) } reset(id: string) { diff --git a/frontend/app/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index b7504c4c6..4f835266d 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -7,6 +7,7 @@ import { issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import Period, { LAST_24_HOURS } from 'Types/app/period'; import { metricService } from "App/services"; +import { WEB_VITALS } from "App/constants/card"; export default class Widget { public static get ID_KEY():string { return "metricId" } @@ -134,7 +135,7 @@ export default class Widget { thumbnail: this.thumbnail, config: { ...this.config, - col: (this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS || this.metricOf === FilterKey.SESSIONS) ? 4 : 2 + col: (this.metricType === 'funnel' || this.metricOf === FilterKey.ERRORS || this.metricOf === FilterKey.SESSIONS) ? 4 : (this.metricType === WEB_VITALS ? 1 : 2) }, } } diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 0f0f1bdc8..74dfb6e6d 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -269,7 +269,7 @@ export enum FilterKey { IMPACTED_SESSIONS_BY_SLOW_PAGES = 'impactedSessionsBySlowPages', // Resources - BREAKDOWN_OF_LOADED_RESOURCES = 'breakdownOfLoadedResources', + BREAKDOWN_OF_LOADED_RESOURCES = 'resourcesCountByType', MISSING_RESOURCES = 'missingResources', RESOURCE_TYPE_VS_RESPONSE_END = 'resourceTypeVsResponseEnd', RESOURCE_FETCH_TIME = 'resourceFetchTime', From e5557a5e4bde5088e095279684df76f2fcaced64 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 13:51:50 +0100 Subject: [PATCH 32/56] feat(ui) - cards - metric selection --- .../MetricListItem/MetricListItem.tsx | 20 +++++++++++-------- .../components/MetricsList/ListView.tsx | 8 ++++++-- .../components/MetricsList/MetricsList.tsx | 10 ++++++++-- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index b8c7a3115..097f18faf 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -9,6 +9,7 @@ interface Props extends RouteComponentProps { siteId: string; selected?: boolean; toggleSelection?: any; + disableSelection?: boolean } function MetricTypeIcon({ type }: any) { @@ -33,7 +34,7 @@ function MetricTypeIcon({ type }: any) { } function MetricListItem(props: Props) { - const { metric, history, siteId, selected, toggleSelection = () => {} } = props; + const { metric, history, siteId, selected, toggleSelection = () => {}, disableSelection = false } = props; const onItemClick = () => { const path = withSiteId(`/metrics/${metric.metricId}`, siteId); @@ -46,13 +47,16 @@ function MetricListItem(props: Props) { onClick={onItemClick} >
- + {!disableSelection && ( + + )} +
{metric.name}
diff --git a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx index b31df78ac..838e7cbdc 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/ListView.tsx @@ -8,13 +8,15 @@ interface Props { selectedList: any; toggleSelection?: (metricId: any) => void; toggleAll?: (e: any) => void; + disableSelection?: boolean } function ListView(props: Props) { - const { siteId, list, selectedList, toggleSelection } = props; + const { siteId, list, selectedList, toggleSelection, disableSelection = false } = props; return (
-
+ {!disableSelection && ( +
Title
+ )}
Owner
Visibility
Last Modified
{list.map((metric: any) => ( {}, + onSelectionChange, }: { siteId: string; onSelectionChange?: (selected: any[]) => void; @@ -26,6 +26,9 @@ function MetricsList({ }, []); useEffect(() => { + if (!onSelectionChange) { + return; + } onSelectionChange(selectedMetrics); }, [selectedMetrics]); @@ -65,11 +68,14 @@ function MetricsList({ > {listView ? ( setSelectedMetrics(checked ? list.map((i: any) => i.metricId) : [])} + toggleAll={({ target: { checked, name } }) => + setSelectedMetrics(checked ? list.map((i: any) => i.metricId) : []) + } /> ) : ( Date: Mon, 2 Jan 2023 13:59:33 +0100 Subject: [PATCH 33/56] feat(ui) - cards - icon display --- .../MetricListItem/MetricListItem.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx index 097f18faf..2c14b0939 100644 --- a/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx +++ b/frontend/app/components/Dashboard/components/MetricListItem/MetricListItem.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Icon, Checkbox, Tooltip } from 'UI'; import { checkForRecent } from 'App/date'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { withSiteId } from 'App/routes'; +import { TYPES, TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING } from 'App/constants/card'; interface Props extends RouteComponentProps { metric: any; @@ -13,21 +14,16 @@ interface Props extends RouteComponentProps { } function MetricTypeIcon({ type }: any) { - const getIcon = () => { - switch (type) { - case 'funnel': - return 'filter'; - case 'table': - return 'list-alt'; - case 'timeseries': - return 'bar-chart-line'; - } - }; + const [card, setCard] = useState(''); + useEffect(() => { + const t = TYPES.find(i => i.slug === type); + setCard(t) + }, [type]) return ( - {type}
} > + {card.title}
} >
- + { card.icon && }
); From ef84d8a9598d6169fdd69c8a29bd9d1d8dfa7c51 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 14:59:38 +0100 Subject: [PATCH 34/56] feat(ui) - cards - keys and dashboardId fix --- .../components/DashboardView/DashboardView.tsx | 2 +- .../DashboardWidgetGrid/DashboardWidgetGrid.tsx | 2 +- frontend/app/mstore/metricStore.ts | 11 ++++++++++- frontend/app/types/filter/filterType.ts | 8 ++++---- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx index 3579baae3..facb9fcfa 100644 --- a/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx +++ b/frontend/app/components/Dashboard/components/DashboardView/DashboardView.tsx @@ -77,7 +77,7 @@ function DashboardView(props: Props) {
{/* @ts-ignore */} - +
- +
} diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index df403bd5e..e485a7b80 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -39,7 +39,6 @@ export default class MetricStore { } updateKey(key: string, value: any) { - console.log('up'); // @ts-ignore this[key] = value; } @@ -60,9 +59,19 @@ export default class MetricStore { changeType(value: string) { const obj: any = { metricType: value}; + if (value === TABLE || value === TIMESERIES) { + obj['viewType'] = 'table'; + } + if (value === TIMESERIES) { + obj['viewType'] = 'lineChart'; + } if (value === ERRORS || value === RESOURCE_MONITORING || value === PERFORMANCE || value === WEB_VITALS) { obj['viewType'] = 'chart'; } + + if (value === FUNNEL) { + obj['metricOf'] = 'sessionCount'; + } this.instance.update(obj) } diff --git a/frontend/app/types/filter/filterType.ts b/frontend/app/types/filter/filterType.ts index 74dfb6e6d..179a5dbf8 100644 --- a/frontend/app/types/filter/filterType.ts +++ b/frontend/app/types/filter/filterType.ts @@ -182,7 +182,7 @@ export enum FilterKey { USER_DEVICE = 'userDevice', PLATFORM = 'platform', DURATION = 'duration', - REFERRER = 'preferrer', + REFERRER = 'referrer', USER_COUNTRY = 'userCountry', JOURNEY = 'journey', REQUEST = 'request', @@ -249,8 +249,8 @@ export enum FilterKey { ERRORS_PER_DOMAINS = 'errorsPerDomains', ERRORS_PER_TYPE = 'errorsPerType', CALLS_ERRORS = 'callsErrors', - DOMAINS_ERRORS_4XX = 'domainsErrors4xx', - DOMAINS_ERRORS_5XX = 'domainsErrors5xx', + DOMAINS_ERRORS_4XX = 'domainsErrors4Xx', + DOMAINS_ERRORS_5XX = 'domainsErrors5Xx', IMPACTED_SESSIONS_BY_JS_ERRORS = 'impactedSessionsByJsErrors', // Performance @@ -272,7 +272,7 @@ export enum FilterKey { BREAKDOWN_OF_LOADED_RESOURCES = 'resourcesCountByType', MISSING_RESOURCES = 'missingResources', RESOURCE_TYPE_VS_RESPONSE_END = 'resourceTypeVsResponseEnd', - RESOURCE_FETCH_TIME = 'resourceFetchTime', + RESOURCE_FETCH_TIME = 'resourcesLoadingTime', SLOWEST_RESOURCES = 'slowestResources', CLICKMAP_URL = 'clickMapUrl', From 2d8e23933cc80d6e037bd432c059c085338294af Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 16:24:29 +0100 Subject: [PATCH 35/56] feat(ui) - cards - data check and filters condition for predefined cards --- .../ErrorsByOrigin/ErrorsByOrigin.tsx | 2 +- .../components/WidgetChart/WidgetChart.tsx | 2 +- .../components/WidgetForm/WidgetForm.tsx | 43 ++++++------------- 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx index e405ba422..cd95853fe 100644 --- a/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx +++ b/frontend/app/components/Dashboard/Widgets/PredefinedWidgets/ErrorsByOrigin/ErrorsByOrigin.tsx @@ -20,7 +20,7 @@ function ErrorsByOrigin(props: Props) { diff --git a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 7cc3cee08..c32d0d498 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -110,7 +110,7 @@ function WidgetChart(props: Props) { } if (metricType === 'predefined' || metricType === ERRORS || metricType === PERFORMANCE || metricType === RESOURCE_MONITORING || metricType === WEB_VITALS) { - const defaultMetric = metric.data.chart.length === 0 ? metricWithData : metric + const defaultMetric = metric.data.chart && metric.data.chart.length === 0 ? metricWithData : metric if (isOverviewWidget) { return } diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 35f4be01f..49c3cb208 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -3,14 +3,13 @@ import { metricOf, issueOptions } from 'App/constants/filterOptions'; import { FilterKey } from 'Types/filter/filterType'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; -import { Button, Icon } from 'UI'; +import { Button, Icon, confirm, Tooltip } from 'UI'; import FilterSeries from '../FilterSeries'; -import { confirm, Tooltip } from 'UI'; import Select from 'Shared/Select'; import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; import MetricTypeDropdown from './components/MetricTypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; -import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING } from 'App/constants/card'; +import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS } from 'App/constants/card'; import { clickmapFilter } from 'App/types/filter/newFilter'; import { renderClickmapThumbnail } from './renderMap' @@ -39,6 +38,7 @@ function WidgetForm(props: Props) { const canAddSeries = metric.series.length < 3; const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length; const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); + const isPredefined = metric.metricType === ERRORS || metric.metricType === PERFORMANCE || metric.metricType === RESOURCE_MONITORING || metric.metricType === WEB_VITALS; const writeOption = ({ value, name }: any) => { value = Array.isArray(value) ? value : value.value; @@ -68,7 +68,7 @@ function WidgetForm(props: Props) { obj['viewType'] = 'table'; } else if (value === FUNNEL) { obj['metricOf'] = 'sessionCount'; - } else if (value === ERRORS || value === RESOURCE_MONITORING) { + } else if (value === ERRORS || value === RESOURCE_MONITORING || value === RESOURCE_MONITORING || value === WEB_VITALS) { obj['viewType'] = 'chart'; } @@ -132,30 +132,6 @@ function WidgetForm(props: Props) { - {/* {metric.metricType === 'timeseries' && ( - <> - of - - - )} */} - {metric.metricOf === FilterKey.ISSUE && ( <> issue type @@ -186,6 +162,14 @@ function WidgetForm(props: Props) {
+ {isPredefined && ( +
+ +
Filtering or modification of OpenReplay provided metrics isn't supported at the moment.
+
+ )} + + {!isPredefined && (
{`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`} @@ -221,7 +205,8 @@ function WidgetForm(props: Props) { />
))} -
+
+ )}
Date: Mon, 2 Jan 2023 16:41:17 +0100 Subject: [PATCH 36/56] feat(ui) - cards - fixed an issue with card subtype selection --- .../MetricSubtypeDropdown/MetricSubtypeDropdown.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx index fe7d3a503..37e2b46ad 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricSubtypeDropdown/MetricSubtypeDropdown.tsx @@ -14,7 +14,7 @@ function MetricSubtypeDropdown(props: Props) { const { metricStore } = useStore(); const metric: any = metricStore.instance; - const options = React.useMemo(() => { + const options: any = React.useMemo(() => { const type = TYPES.find((i: MetricType) => i.slug === metric.metricType); if (type && type.subTypes) { const options = type.subTypes.map((i: MetricType) => ({ @@ -30,7 +30,9 @@ function MetricSubtypeDropdown(props: Props) { React.useEffect(() => { // @ts-ignore - setTimeout(() => props.onSelect({ name: 'metricOf', value: { value: options[0].value }}), 0) + if (options && !options.map(i => i.value).includes(metric.metricOf)) { + setTimeout(() => props.onSelect({ name: 'metricOf', value: { value: options[0].value }}), 0) + } }, [metric.metricType]) return options ? ( From 38e5003114c36fc98d44d3ca0d1f556ee2b736f7 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 16:56:26 +0100 Subject: [PATCH 37/56] feat(ui) - cards - button name --- .../components/Dashboard/components/WidgetForm/WidgetForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 49c3cb208..3aec009c1 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -214,7 +214,7 @@ function WidgetForm(props: Props) { disabled={!cannotSaveFunnel} >
From 39a31bb1e4136b012aae68118075a54d72748277 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 17:10:47 +0100 Subject: [PATCH 38/56] feat(ui) - cards - widget form updates --- .../components/WidgetForm/WidgetForm.tsx | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 3aec009c1..82e43c872 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -40,49 +40,50 @@ function WidgetForm(props: Props) { const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); const isPredefined = metric.metricType === ERRORS || metric.metricType === PERFORMANCE || metric.metricType === RESOURCE_MONITORING || metric.metricType === WEB_VITALS; - const writeOption = ({ value, name }: any) => { + const writeOption = ({ value, name }: { value: any; name: any }) => { value = Array.isArray(value) ? value : value.value; const obj: any = { [name]: value }; - + if (name === 'metricValue') { - obj['metricValue'] = value; - - // handle issues (remove all when other option is selected) - if (Array.isArray(obj['metricValue']) && obj['metricValue'].length > 1) { - obj['metricValue'] = obj['metricValue'].filter((i) => i.value !== 'all'); + obj.metricValue = value; + + if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) { + obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all'); } } - - if (name === 'metricOf') { - // if (value === FilterKey.ISSUE) { - // obj['metricValue'] = [{ value: 'all', label: 'All' }]; - // } - } - + if (name === 'metricType') { - if (value === TIMESERIES) { - obj['metricOf'] = timeseriesOptions[0].value; - obj['viewType'] = 'lineChart'; - } else if (value === TABLE) { - obj['metricOf'] = tableOptions[0].value; - obj['viewType'] = 'table'; - } else if (value === FUNNEL) { - obj['metricOf'] = 'sessionCount'; - } else if (value === ERRORS || value === RESOURCE_MONITORING || value === RESOURCE_MONITORING || value === WEB_VITALS) { - obj['viewType'] = 'chart'; - } + switch (value) { + case TIMESERIES: + obj.metricOf = timeseriesOptions[0].value; + obj.viewType = 'lineChart'; + break; + case TABLE: + obj.metricOf = tableOptions[0].value; + obj.viewType = 'table'; + break; + case FUNNEL: + obj.metricOf = 'sessionCount'; + break; + case ERRORS: + case RESOURCE_MONITORING: + case WEB_VITALS: + obj.viewType = 'chart'; + break; + case CLICKMAP: + obj.viewType = 'chart'; - if (metric.metricType === CLICKMAP && value !== CLICKMAP) { - metric.series[0].filter.removeFilter(0) - } - if (value === CLICKMAP) { - obj['viewType'] = 'chart'; - if (metric.series[0].filter.filters.length < 1) { + if (value !== CLICKMAP) { + metric.series[0].filter.removeFilter(0) + } + + if (metric.series[0].filter.filters.length < 1) { metric.series[0].filter.addFilter({ - ...clickmapFilter, - value: [''], - }) - } + ...clickmapFilter, + value: [''], + }); + } + break; } } metricStore.merge(obj); From cb70d038a5bddfd5f78ef3f947f85a7257cd4c36 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 17:17:07 +0100 Subject: [PATCH 39/56] feat(ui) - cards - widget form updates --- .../components/WidgetForm/WidgetForm.tsx | 110 ++++++++++-------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index 82e43c872..b4e2613e9 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -9,9 +9,18 @@ import Select from 'Shared/Select'; import { withSiteId, dashboardMetricDetails, metricDetails } from 'App/routes'; import MetricTypeDropdown from './components/MetricTypeDropdown'; import MetricSubtypeDropdown from './components/MetricSubtypeDropdown'; -import { TIMESERIES, TABLE, CLICKMAP, FUNNEL, ERRORS, RESOURCE_MONITORING, PERFORMANCE, WEB_VITALS } from 'App/constants/card'; +import { + TIMESERIES, + TABLE, + CLICKMAP, + FUNNEL, + ERRORS, + RESOURCE_MONITORING, + PERFORMANCE, + WEB_VITALS, +} from 'App/constants/card'; import { clickmapFilter } from 'App/types/filter/newFilter'; -import { renderClickmapThumbnail } from './renderMap' +import { renderClickmapThumbnail } from './renderMap'; interface Props { history: any; @@ -33,25 +42,27 @@ function WidgetForm(props: Props) { const timeseriesOptions = metricOf.filter((i) => i.type === 'timeseries'); const tableOptions = metricOf.filter((i) => i.type === 'table'); const isTable = metric.metricType === 'table'; - const isClickmap = metric.metricType === CLICKMAP + const isClickmap = metric.metricType === CLICKMAP; const isFunnel = metric.metricType === 'funnel'; const canAddSeries = metric.series.length < 3; const eventsLength = metric.series[0].filter.filters.filter((i: any) => i.isEvent).length; const cannotSaveFunnel = isFunnel && (!metric.series[0] || eventsLength <= 1); - const isPredefined = metric.metricType === ERRORS || metric.metricType === PERFORMANCE || metric.metricType === RESOURCE_MONITORING || metric.metricType === WEB_VITALS; + const isPredefined = [ERRORS, PERFORMANCE, RESOURCE_MONITORING, WEB_VITALS].includes( + metric.metricType + ); const writeOption = ({ value, name }: { value: any; name: any }) => { value = Array.isArray(value) ? value : value.value; const obj: any = { [name]: value }; - + if (name === 'metricValue') { obj.metricValue = value; - + if (Array.isArray(obj.metricValue) && obj.metricValue.length > 1) { obj.metricValue = obj.metricValue.filter((i: any) => i.value !== 'all'); } } - + if (name === 'metricType') { switch (value) { case TIMESERIES: @@ -74,7 +85,7 @@ function WidgetForm(props: Props) { obj.viewType = 'chart'; if (value !== CLICKMAP) { - metric.series[0].filter.removeFilter(0) + metric.series[0].filter.removeFilter(0); } if (metric.series[0].filter.filters.length < 1) { @@ -93,9 +104,9 @@ function WidgetForm(props: Props) { const wasCreating = !metric.exists(); if (isClickmap) { try { - metric.thumbnail = await renderClickmapThumbnail() + metric.thumbnail = await renderClickmapThumbnail(); } catch (e) { - console.error(e) + console.error(e); } } metricStore.save(metric).then((metric: any) => { @@ -159,53 +170,54 @@ function WidgetForm(props: Props) { /> )} -
{isPredefined && (
-
Filtering or modification of OpenReplay provided metrics isn't supported at the moment.
+
+ Filtering or modification of OpenReplay provided metrics isn't supported at the moment. +
)} {!isPredefined && ( -
-
- {`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`} - {!isTable && !isFunnel && !isClickmap && ( - - )} -
+
+
+ {`${isTable || isFunnel || isClickmap ? 'Filter by' : 'Chart Series'}`} + {!isTable && !isFunnel && !isClickmap && ( + + )} +
- {metric.series.length > 0 && - metric.series - .slice(0, isTable || isFunnel || isClickmap ? 1 : metric.series.length) - .map((series: any, index: number) => ( -
- metric.updateKey('hasChanged', true)} - hideHeader={isTable || isClickmap} - seriesIndex={index} - series={series} - onRemoveSeries={() => metric.removeSeries(index)} - canDelete={metric.series.length > 1} - emptyMessage={ - isTable - ? 'Filter data using any event or attribute. Use Add Step button below to do so.' - : 'Add user event or filter to define the series by clicking Add Step.' - } - /> -
- ))} + {metric.series.length > 0 && + metric.series + .slice(0, isTable || isFunnel || isClickmap ? 1 : metric.series.length) + .map((series: any, index: number) => ( +
+ metric.updateKey('hasChanged', true)} + hideHeader={isTable || isClickmap} + seriesIndex={index} + series={series} + onRemoveSeries={() => metric.removeSeries(index)} + canDelete={metric.series.length > 1} + emptyMessage={ + isTable + ? 'Filter data using any event or attribute. Use Add Step button below to do so.' + : 'Add user event or filter to define the series by clicking Add Step.' + } + /> +
+ ))}
)} @@ -215,7 +227,11 @@ function WidgetForm(props: Props) { disabled={!cannotSaveFunnel} >
From 89499d346c598727570fa1ce9080ff925bd88e98 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 17:28:07 +0100 Subject: [PATCH 40/56] feat(ui) - cards - widget form updates --- .../components/WidgetForm/WidgetForm.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index b4e2613e9..ac14b68db 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -109,19 +109,15 @@ function WidgetForm(props: Props) { console.error(e); } } - metricStore.save(metric).then((metric: any) => { - if (wasCreating) { - if (parseInt(dashboardId) > 0) { - history.replace(withSiteId(dashboardMetricDetails(dashboardId, metric.metricId), siteId)); - const dashboard = dashboardStore.getDashboard(parseInt(dashboardId)); - if (dashboard) { - dashboardStore.addWidgetToDashboard(dashboard, [metric.metricId]); - } - } else { - history.replace(withSiteId(metricDetails(metric.metricId), siteId)); - } + const savedMetric = await metricStore.save(metric); + if (wasCreating) { + if (parseInt(dashboardId, 10) > 0) { + history.replace(withSiteId(dashboardMetricDetails(dashboardId, savedMetric.metricId), siteId)); + dashboardStore.addWidgetToDashboard(dashboardStore.getDashboard(parseInt(dashboardId, 10))!, [savedMetric.metricId]); + } else { + history.replace(withSiteId(metricDetails(savedMetric.metricId), siteId)); } - }); + } }; const onDelete = async () => { From 9f3d5e553f0bb89fd71fcc9c722b77704a7c1abb Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 17:50:52 +0100 Subject: [PATCH 41/56] feat(ui) - cards - cards sort --- .../components/MetricsList/MetricsList.tsx | 48 ++++++------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 76bdff875..377d45148 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -20,6 +20,7 @@ function MetricsList({ const metricsSearch = metricStore.metricsSearch; const listView = useObserver(() => metricStore.listView); const [selectedMetrics, setSelectedMetrics] = useState([]); + const sortBy = useObserver(() => metricStore.sort.by); useEffect(() => { metricStore.fetchList(); @@ -44,12 +45,26 @@ function MetricsList({ const dashboardsStr = item.dashboards.map((d: any) => d.name).join(' '); return searchRE.test(dashboardsStr); }; + const list = metricsSearch !== '' ? filterList(metrics, metricsSearch, ['name', 'metricType', 'owner'], filterByDashboard) : metrics; + const lenth = list.length; + const sortList = () => { + list.sort((a, b) => { + const aDate = new Date(a.lastModified); + const bDate = new Date(b.lastModified); + return sortBy === 'asc' + ? aDate.getTime() - bDate.getTime() + : bDate.getTime() - aDate.getTime(); + }); + }; + + sortList(); + useEffect(() => { metricStore.updateKey('sessionsPage', 1); }, []); @@ -85,39 +100,6 @@ function MetricsList({ toggleSelection={toggleMetricSelection} /> )} - {/* -
-
-
- setSelectedMetrics(list.map((i: any) => i.metricId))} - /> - Title -
-
Owner
-
Visibility
-
Last Modified
-
- - {sliceListPerPage(list, metricStore.page - 1, metricStore.pageSize).map((metric: any) => ( - // - // { - // e.stopPropagation(); - // toggleMetricSelection(parseInt(metric.metricId)); - // }} - // /> - // - ))} -
- */}
From 2fa02e2ebbac3e9a8ac06019d27b3c1080023ac5 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 17:54:35 +0100 Subject: [PATCH 42/56] feat(ui) - cards - cards sort --- .../Dashboard/components/MetricsList/MetricsList.tsx | 12 ------------ frontend/app/mstore/metricStore.ts | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx index 377d45148..3f3bad3d0 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/MetricsList.tsx @@ -53,18 +53,6 @@ function MetricsList({ const lenth = list.length; - const sortList = () => { - list.sort((a, b) => { - const aDate = new Date(a.lastModified); - const bDate = new Date(b.lastModified); - return sortBy === 'asc' - ? aDate.getTime() - bDate.getTime() - : bDate.getTime() - aDate.getTime(); - }); - }; - - sortList(); - useEffect(() => { metricStore.updateKey('sessionsPage', 1); }, []); diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index e485a7b80..f37b79732 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -30,7 +30,7 @@ export default class MetricStore { } get sortedWidgets() { - return [...this.metrics].sort((a, b) => b.lastModified - a.lastModified); + return [...this.metrics].sort((a, b) => this.sort.by === 'desc' ? b.lastModified - a.lastModified : a.lastModified - b.lastModified) } // State Actions From b38ca1a720e64b438ee4d674b003649d8ade3050 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 18:04:12 +0100 Subject: [PATCH 43/56] feat(ui) - cards - metric store save method updates --- frontend/app/mstore/metricStore.ts | 46 +++++++++++++----------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/frontend/app/mstore/metricStore.ts b/frontend/app/mstore/metricStore.ts index f37b79732..abb6a30f9 100644 --- a/frontend/app/mstore/metricStore.ts +++ b/frontend/app/mstore/metricStore.ts @@ -111,33 +111,27 @@ export default class MetricStore { } // API Communication - save(metric: Widget): Promise { - const wasCreating = !metric.exists(); + async save(metric: Widget): Promise { this.isSaving = true; - return new Promise((resolve, reject) => { - metricService - .saveMetric(metric) - .then((metric: any) => { - const _metric = new Widget().fromJson(metric); - if (wasCreating) { - toast.success('Metric created successfully'); - this.addToList(_metric); - this.instance = _metric; - } else { - toast.success('Metric updated successfully'); - this.updateInList(_metric); - } - resolve(_metric); - }) - .catch(() => { - toast.error('Error saving metric'); - reject(); - }) - .finally(() => { - this.instance.updateKey('hasChanged', false); - this.isSaving = false; - }); - }); + try { + const metricData = await metricService.saveMetric(metric); + const _metric = new Widget().fromJson(metricData); + if (!metric.exists()) { + toast.success('Metric created successfully'); + this.addToList(_metric); + } else { + toast.success('Metric updated successfully'); + this.updateInList(_metric); + } + this.instance = _metric; + this.instance.updateKey('hasChanged', false); + return _metric; + } catch (error) { + toast.error('Error saving metric'); + throw error; + } finally { + this.isSaving = false; + } } fetchList() { From 13451e8434230d4e6d7af0323b1ccbe133427ba2 Mon Sep 17 00:00:00 2001 From: Shekar Siri Date: Mon, 2 Jan 2023 18:08:55 +0100 Subject: [PATCH 44/56] feat(ui) - cards - fixed grid view cols --- .../components/Dashboard/components/MetricsList/GridView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx index c48a2a9cd..df84fbb6e 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx @@ -11,7 +11,7 @@ interface Props { function GridView(props: Props) { const { siteId, list, selectedList, toggleSelection } = props; return ( -
+
{list.map((metric: any) => ( Date: Mon, 2 Jan 2023 18:25:58 +0100 Subject: [PATCH 45/56] feat(ui) - cards - metric to card text, card type dropdown with icon --- .../components/MetricsList/GridView.tsx | 9 ------- .../components/WidgetForm/WidgetForm.tsx | 2 +- .../MetricTypeDropdown/MetricTypeDropdown.tsx | 25 +++++++++++++------ frontend/app/mstore/dashboardStore.ts | 4 +-- frontend/app/mstore/metricStore.ts | 6 ++--- frontend/app/mstore/types/widget.ts | 2 +- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx index df84fbb6e..052dd6ec3 100644 --- a/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx +++ b/frontend/app/components/Dashboard/components/MetricsList/GridView.tsx @@ -22,15 +22,6 @@ function GridView(props: Props) { isWidget={metric.metricType === 'predefined'} onClick={() => toggleSelection(parseInt(metric.metricId))} /> - {/* { - e.stopPropagation(); - toggleSelection(parseInt(metric.metricId)); - }} - /> */} ))}
diff --git a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx index ac14b68db..db0525aab 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/WidgetForm.tsx @@ -135,8 +135,8 @@ function WidgetForm(props: Props) { return (
-
+ Card showing diff --git a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx index 9623df971..ac47765b8 100644 --- a/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx +++ b/frontend/app/components/Dashboard/components/WidgetForm/components/MetricTypeDropdown/MetricTypeDropdown.tsx @@ -7,7 +7,7 @@ import CustomDropdownOption from 'Shared/CustomDropdownOption'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; import withLocationHandlers from 'HOCs/withLocationHandlers'; - +import { Icon } from 'UI'; interface Options { label: string; icon: string; @@ -33,16 +33,16 @@ function MetricTypeDropdown(props: Props) { }, []); React.useEffect(() => { - const queryCardType = props.query.get('type') + const queryCardType = props.query.get('type'); if (queryCardType && options.length > 0 && metric.metricType) { - const type = options.find((i) => i.value === queryCardType) - setTimeout(() => onChange(type.value), 0) + const type = options.find((i) => i.value === queryCardType); + setTimeout(() => onChange(type.value), 0); } - }, []) + }, []); const onChange = (type: string) => { - metricStore.changeType(type) - } + metricStore.changeType(type); + }; return (