diff --git a/frontend/.storybook/config.DEPRECATED.js b/frontend/.storybook/config.DEPRECATED.js index fad172b6f..ba3f6eb05 100644 --- a/frontend/.storybook/config.DEPRECATED.js +++ b/frontend/.storybook/config.DEPRECATED.js @@ -11,14 +11,12 @@ const withProvider = (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); diff --git a/frontend/app/Router.js b/frontend/app/Router.js index 8ee29802a..584fb59e5 100644 --- a/frontend/app/Router.js +++ b/frontend/app/Router.js @@ -8,7 +8,6 @@ import { fetchUserInfo } from 'Duck/user'; import withSiteIdUpdater from 'HOCs/withSiteIdUpdater'; import Header from 'Components/Header/Header'; import { fetchList as fetchSiteList } from 'Duck/site'; -import { fetchList as fetchAnnouncements } from 'Duck/announcements'; import { fetchList as fetchAlerts } from 'Duck/alerts'; import { withStore } from 'App/mstore'; @@ -30,7 +29,7 @@ const LiveSessionPure = lazy(() => import('Components/Session/LiveSession')); const OnboardingPure = lazy(() => import('Components/Onboarding/Onboarding')); const ClientPure = lazy(() => import('Components/Client/Client')); const AssistPure = lazy(() => import('Components/Assist')); -const BugFinderPure = lazy(() => import('Components/Overview')); +const SessionsOverviewPure = lazy(() => import('Components/Overview')); const DashboardPure = lazy(() => import('Components/Dashboard/NewDashboard')); const ErrorsPure = lazy(() => import('Components/Errors/Errors')); const FunnelDetailsPure = lazy(() => import('Components/Funnels/FunnelDetails')); @@ -38,7 +37,7 @@ const FunnelIssueDetails = lazy(() => import('Components/Funnels/FunnelIssueDeta const FunnelPagePure = lazy(() => import('Components/Funnels/FunnelPage')); const MultiviewPure = lazy(() => import('Components/Session_/Multiview/Multiview.tsx')); -const BugFinder = withSiteIdUpdater(BugFinderPure); +const SessionsOverview = withSiteIdUpdater(SessionsOverviewPure); const Dashboard = withSiteIdUpdater(DashboardPure); const Session = withSiteIdUpdater(SessionPure); const LiveSession = withSiteIdUpdater(LiveSessionPure); @@ -115,7 +114,6 @@ const MULTIVIEW_INDEX_PATH = routes.multiviewIndex(); fetchTenants, setSessionPath, fetchSiteList, - fetchAnnouncements, fetchAlerts, } ) @@ -237,7 +235,7 @@ class Router extends React.Component { - + } /> diff --git a/frontend/app/api_client.js b/frontend/app/api_client.js index c0ccf9bcd..5673a0aab 100644 --- a/frontend/app/api_client.js +++ b/frontend/app/api_client.js @@ -88,7 +88,6 @@ export default class APIClient { if ( path !== '/targets_temp' && !path.includes('/metadata/session_search') && - !path.includes('/watchdogs/rules') && !path.includes('/assist/credentials') && !!this.siteId && siteIdRequiredPaths.some(sidPath => path.startsWith(sidPath)) diff --git a/frontend/app/api_middleware.js b/frontend/app/api_middleware.js index 8ccc6bc37..2b4fd4b4a 100644 --- a/frontend/app/api_middleware.js +++ b/frontend/app/api_middleware.js @@ -41,9 +41,9 @@ export default () => (next) => (action) => { function parseError(e) { try { - return JSON.parse(e).errors || []; + return [...JSON.parse(e).errors] || []; } catch { - return e; + return Array.isArray(e) ? e : [e]; } } diff --git a/frontend/app/components/Alerts/Notifications/Notifications.tsx b/frontend/app/components/Alerts/Notifications/Notifications.tsx index 05bcea06b..2bc78384f 100644 --- a/frontend/app/components/Alerts/Notifications/Notifications.tsx +++ b/frontend/app/components/Alerts/Notifications/Notifications.tsx @@ -1,34 +1,27 @@ import React, { useEffect } from 'react'; import stl from './notifications.module.css'; -import { connect } from 'react-redux'; import { Icon, Tooltip } from 'UI'; -import { fetchList, setViewed, clearAll } from 'Duck/notifications'; -import { setLastRead } from 'Duck/announcements'; import { useModal } from 'App/components/Modal'; import AlertTriggersModal from 'Shared/AlertTriggersModal'; import { useStore } from 'App/mstore'; -import { useObserver } from 'mobx-react-lite'; +import { observer } from 'mobx-react-lite'; const AUTOREFRESH_INTERVAL = 5 * 60 * 1000; -interface Props { - notifications: any; - fetchList: any; -} -function Notifications(props: Props) { +function Notifications() { const { showModal } = useModal(); const { notificationStore } = useStore(); - const count = useObserver(() => notificationStore.notificationsCount); + const count = notificationStore.notificationsCount; useEffect(() => { const interval = setInterval(() => { - notificationStore.fetchNotificationsCount(); + void notificationStore.fetchNotificationsCount(); }, AUTOREFRESH_INTERVAL); return () => clearInterval(interval); }, []); - return useObserver(() => ( + return (
- )); + ); } -export default connect( - (state: any) => ({ - notifications: state.getIn(['notifications', 'list']), - }), - { fetchList, setLastRead, setViewed, clearAll } -)(Notifications); +export default observer(Notifications) \ No newline at end of file diff --git a/frontend/app/components/Announcements/Announcements.js b/frontend/app/components/Announcements/Announcements.js deleted file mode 100644 index 55306e643..000000000 --- a/frontend/app/components/Announcements/Announcements.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import stl from './announcements.module.css'; -import ListItem from './ListItem'; -import { connect } from 'react-redux'; -import { SlideModal, Icon, NoContent, Tooltip } from 'UI'; -import { fetchList, setLastRead } from 'Duck/announcements'; -import withToggle from 'Components/hocs/withToggle'; -import { withRouter } from 'react-router-dom'; -import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG'; - -@withToggle('visible', 'toggleVisisble') -@withRouter -class Announcements extends React.Component { - - navigateToUrl = url => { - if (url) { - if (url.startsWith(window.env.ORIGIN || window.location.origin)) { - const { history } = this.props; - var path = new URL(url).pathname - if (path.includes('/metrics')) { - const { siteId, sites } = this.props; - const activeSite = sites.find(s => s.id == siteId); - history.push(`/${activeSite.id + path}`); - } else { - history.push(path) - } - } else { - window.open(url, "_blank") - } - this.toggleModal() - } - } - - toggleModal = () => { - if (!this.props.visible) { - const { setLastRead, fetchList } = this.props; - fetchList().then(() => { setTimeout(() => { setLastRead() }, 5000); }); - } - this.props.toggleVisisble(!this.props.visible); - } - - render() { - const { announcements, visible, loading } = this.props; - const unReadNotificationsCount = announcements.filter(({viewed}) => !viewed).size - - return ( -
- -
-
- { unReadNotificationsCount } -
- -
-
- - - - -
No announcements to show.
-
- } - size="small" - show={ !loading && announcements.size === 0 } - > - { - announcements.map(item => ( - - )) - } - - - } - /> - - ); - } -} - -export default connect(state => ({ - announcements: state.getIn(['announcements', 'list']), - loading: state.getIn(['announcements', 'fetchList', 'loading']), - siteId: state.getIn([ 'site', 'siteId' ]), - sites: state.getIn([ 'site', 'list' ]), -}), { fetchList, setLastRead })(Announcements); \ No newline at end of file diff --git a/frontend/app/components/Announcements/ListItem/ListItem.js b/frontend/app/components/Announcements/ListItem/ListItem.js deleted file mode 100644 index dd777c719..000000000 --- a/frontend/app/components/Announcements/ListItem/ListItem.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Button, Label } from 'UI'; -import stl from './listItem.module.css'; - -const ListItem = ({ announcement, onButtonClick }) => { - return ( -
-
-
{announcement.createdAt && announcement.createdAt.toFormat('LLL dd, yyyy')}
- -
- {announcement.imageUrl && - - } -
-

{announcement.title}

-
{announcement.description}
- {announcement.buttonUrl && - - } -
-
- ) -} - -export default ListItem diff --git a/frontend/app/components/Announcements/ListItem/index.js b/frontend/app/components/Announcements/ListItem/index.js deleted file mode 100644 index 741aed270..000000000 --- a/frontend/app/components/Announcements/ListItem/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './ListItem'; diff --git a/frontend/app/components/Announcements/ListItem/listItem.module.css b/frontend/app/components/Announcements/ListItem/listItem.module.css deleted file mode 100644 index 5bc3a44c8..000000000 --- a/frontend/app/components/Announcements/ListItem/listItem.module.css +++ /dev/null @@ -1,5 +0,0 @@ -.wrapper { - background-color: white; - margin-bottom: 20px; - padding: 15px; -} \ No newline at end of file diff --git a/frontend/app/components/Announcements/announcements.module.css b/frontend/app/components/Announcements/announcements.module.css deleted file mode 100644 index 5a3704af2..000000000 --- a/frontend/app/components/Announcements/announcements.module.css +++ /dev/null @@ -1,39 +0,0 @@ -.wrapper { - position: relative; -} - -.button { - position: relative; - cursor: pointer; - display: flex; - align-items: center; - padding: 0 15px; - height: 50px; - transition: all 0.3s; - - &:hover { - background-color: $gray-lightest; - transition: all 0.2s; - } - - &[data-active=true] { - background-color: $gray-lightest; - } -} - -.counter { - position: absolute; - top: 8px; - left: 24px; - background-color: #CC0000; - color: white; - font-size: 9px; - font-weight: 300; - min-width: 16px; - height: 16px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - padding: 3px; -} diff --git a/frontend/app/components/Announcements/index.js b/frontend/app/components/Announcements/index.js deleted file mode 100644 index faeffcfcd..000000000 --- a/frontend/app/components/Announcements/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './Announcements'; \ No newline at end of file diff --git a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx index 25283bd3b..212fea91c 100644 --- a/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx +++ b/frontend/app/components/Assist/RequestingWindow/RequestingWindow.tsx @@ -82,5 +82,5 @@ function RequestingWindow({ userDisplayName, getWindowType }: Props) { } export default connect((state: any) => ({ - userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']), + userDisplayName: state.getIn(['sessions', 'current']).userDisplayName, }))(RequestingWindow); diff --git a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx index a474c9cc8..f324ce433 100644 --- a/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx +++ b/frontend/app/components/Assist/components/AssistActions/AssistActions.tsx @@ -255,7 +255,7 @@ const con = connect( return { hasPermission: permissions.includes('ASSIST_CALL'), isEnterprise: state.getIn(['user', 'account', 'edition']) === 'ee', - userDisplayName: state.getIn(['sessions', 'current', 'userDisplayName']), + userDisplayName: state.getIn(['sessions', 'current']).userDisplayName, }; }, { toggleChatWindow } diff --git a/frontend/app/components/Assist/components/SessionList/SessionList.tsx b/frontend/app/components/Assist/components/SessionList/SessionList.tsx index ad4eb4df5..da3ffbf06 100644 --- a/frontend/app/components/Assist/components/SessionList/SessionList.tsx +++ b/frontend/app/components/Assist/components/SessionList/SessionList.tsx @@ -36,7 +36,7 @@ function SessionList(props: Props) { diff --git a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js b/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js deleted file mode 100644 index 59ed9d9e9..000000000 --- a/frontend/app/components/BugFinder/AutoComplete/AutoComplete.js +++ /dev/null @@ -1,199 +0,0 @@ -import React from 'react'; -import APIClient from 'App/api_client'; -import cn from 'classnames'; -import { Input, Icon } from 'UI'; -import { debounce } from 'App/utils'; -import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; -import EventSearchInput from 'Shared/EventSearchInput'; -import stl from './autoComplete.module.css'; -import FilterItem from '../CustomFilters/FilterItem'; - -const TYPE_TO_SEARCH_MSG = "Start typing to search..."; -const NO_RESULTS_MSG = "No results found."; -const SOME_ERROR_MSG = "Some error occured."; -const defaultValueToText = value => value; -const defaultOptionMapping = (values, valueToText) => values.map(value => ({ text: valueToText(value), value })); - -const hiddenStyle = { - whiteSpace: 'pre-wrap', - opacity: 0, position: 'fixed', left: '-3000px' -}; - -let pasted = false; -let changed = false; - -class AutoComplete extends React.PureComponent { - static defaultProps = { - method: 'GET', - params: {}, - } - - state = { - values: [], - noResultsMessage: TYPE_TO_SEARCH_MSG, - ddOpen: false, - query: this.props.value, - loading: false, - error: false - } - - componentWillReceiveProps(newProps) { - if (this.props.value !== newProps.value) { - this.setState({ query: newProps.value}); - } - } - - onClickOutside = () => { - this.setState({ ddOpen: false }); - } - - requestValues = (q) => { - const { params, endpoint, method } = this.props; - this.setState({ - loading: true, - error: false, - }); - return new APIClient()[ method.toLowerCase() ](endpoint, { ...params, q }) - .then(response => response.json()) - .then(({ errors, data }) => { - if (errors) { - this.setError(); - } else { - this.setState({ - ddOpen: true, - values: data, - loading: false, - noResultsMessage: NO_RESULTS_MSG, - }); - } - }) - .catch(this.setError); - } - - debouncedRequestValues = debounce(this.requestValues, 1000) - - setError = () => this.setState({ - loading: false, - error: true, - noResultsMessage: SOME_ERROR_MSG, - }) - - - onInputChange = ({ target: { value } }) => { - changed = true; - this.setState({ query: value, updated: true }) - const _value = value ? value.trim() : undefined; - if (_value !== '' && _value !== ' ') { - this.debouncedRequestValues(_value) - } - } - - onBlur = ({ target: { value } }) => { - // to avoid sending unnecessary request on focus in/out without changing - if (!changed && !pasted) return; - - value = pasted ? this.hiddenInput.value : value; - const { onSelect, name } = this.props; - if (value !== this.props.value) { - const _value = value ? value.trim() : undefined; - onSelect(null, {name, value: _value}); - } - - changed = false; - pasted = false; - } - - onItemClick = (e, item) => { - e.stopPropagation(); - e.preventDefault(); - const { onSelect, name } = this.props; - - this.setState({ query: item.value, ddOpen: false}) - onSelect(e, {name, ...item.toJS()}); - } - - render() { - const { ddOpen, query, loading, values } = this.state; - const { - optionMapping = defaultOptionMapping, - valueToText = defaultValueToText, - placeholder = 'Type to search...', - headerText = '', - fullWidth = false, - onRemoveValue = () => {}, - onAddValue = () => {}, - showCloseButton = false, - } = this.props; - - const options = optionMapping(values, valueToText) - - return ( - - {/* */} -
- this.setState({ddOpen: true})} - onChange={ this.onInputChange } - onBlur={ this.onBlur } - value={ query } - autoFocus={ true } - type="text" - placeholder={ placeholder } - onPaste={(e) => { - const text = e.clipboardData.getData('Text'); - this.hiddenInput.value = text; - pasted = true; // to use only the hidden input - } } - autocomplete="do-not-autofill-bad-chrome" - /> -
- { showCloseButton ? : or} -
-
- - {showCloseButton &&
or
} - {/* this.setState({ddOpen: true})} - value={ query } - // icon="search" - label={{ basic: true, content:
test
}} - labelPosition='right' - loading={ loading } - autoFocus={ true } - type="search" - placeholder={ placeholder } - onPaste={(e) => { - const text = e.clipboardData.getData('Text'); - this.hiddenInput.value = text; - pasted = true; // to use only the hidden input - } } - /> */} - - { ddOpen && options.length > 0 && -
- { headerText && headerText } - { - options.map(item => ( - this.onItemClick(e, item) } - /> - )) - } -
- } -
- ); - } -} - -export default AutoComplete; diff --git a/frontend/app/components/BugFinder/AutoComplete/DropdownItem.js b/frontend/app/components/BugFinder/AutoComplete/DropdownItem.js deleted file mode 100644 index dc2b97304..000000000 --- a/frontend/app/components/BugFinder/AutoComplete/DropdownItem.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; -import stl from './dropdownItem.module.css'; - -const DropdownItem = ({ value, onSelect }) => { - return ( -
{ value }
- ); -}; - -export default DropdownItem; diff --git a/frontend/app/components/BugFinder/AutoComplete/autoComplete.module.css b/frontend/app/components/BugFinder/AutoComplete/autoComplete.module.css deleted file mode 100644 index 09a9a6571..000000000 --- a/frontend/app/components/BugFinder/AutoComplete/autoComplete.module.css +++ /dev/null @@ -1,64 +0,0 @@ -.menu { - border-radius: 0 0 3px 3px; - box-shadow: 0 2px 10px 0 $gray-light; - padding: 20px; - background-color: white; - max-height: 350px; - overflow-y: auto; - position: absolute; - top: 28px; - left: 0; - width: 500px; - z-index: 99; -} - -.searchInput { - & input { - font-size: 13px !important; - padding: 5px !important; - color: $gray-darkest !important; - font-size: 14px !important; - background-color: rgba(255, 255, 255, 0.8) !important; - - & .label { - padding: 0px !important; - display: flex; - align-items: center; - justify-content: center; - } - } - height: 28px !important; - width: 280px; - color: $gray-darkest !important; -} - -.fullWidth { - width: 100% !important; -} - -.inputWrapper { - border: solid thin $gray-light !important; - border-radius: 3px; - border-radius: 3px; - display: flex; - align-items: center; - & input { - height: 28px; - font-size: 13px !important; - padding: 0 5px !important; - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; - } - - & .right { - height: 28px; - display: flex; - align-items: center; - padding: 0 5px; - background-color: $gray-lightest; - border-left: solid thin $gray-light !important; - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; - cursor: pointer; - } -} \ No newline at end of file diff --git a/frontend/app/components/BugFinder/AutoComplete/dropdownItem.module.css b/frontend/app/components/BugFinder/AutoComplete/dropdownItem.module.css deleted file mode 100644 index f5646a470..000000000 --- a/frontend/app/components/BugFinder/AutoComplete/dropdownItem.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.wrapper { - padding: 8px; - border-bottom: solid thin rgba(0, 0, 0, 0.05); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - cursor: pointer; - &:hover { - background-color: $active-blue; - } -} \ No newline at end of file diff --git a/frontend/app/components/BugFinder/AutoComplete/index.js b/frontend/app/components/BugFinder/AutoComplete/index.js deleted file mode 100644 index fa63241a4..000000000 --- a/frontend/app/components/BugFinder/AutoComplete/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './AutoComplete'; \ No newline at end of file diff --git a/frontend/app/components/BugFinder/BugFinder.js b/frontend/app/components/BugFinder/BugFinder.js deleted file mode 100644 index 2c31a3ca8..000000000 --- a/frontend/app/components/BugFinder/BugFinder.js +++ /dev/null @@ -1,131 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import { connect } from 'react-redux'; -import withPageTitle from 'HOCs/withPageTitle'; -import { fetchFavoriteList as fetchFavoriteSessionList } from 'Duck/sessions'; -import { applyFilter, clearEvents, addAttribute } from 'Duck/filters'; -import { KEYS } from 'Types/filter/customFilter'; -import SessionList from './SessionList'; -import stl from './bugFinder.module.css'; -import withLocationHandlers from 'HOCs/withLocationHandlers'; -import { fetch as fetchFilterVariables } from 'Duck/sources'; -import { fetchSources } from 'Duck/customField'; -import { setActiveTab } from 'Duck/search'; -import SessionsMenu from './SessionsMenu/SessionsMenu'; -import NoSessionsMessage from 'Shared/NoSessionsMessage'; -import SessionSearch from 'Shared/SessionSearch'; -import MainSearchBar from 'Shared/MainSearchBar'; -import { clearSearch, fetchSessions, addFilterByKeyAndValue } from 'Duck/search'; -import { FilterKey } from 'Types/filter/filterType'; - -const weakEqual = (val1, val2) => { - if (!!val1 === false && !!val2 === false) return true; - if (!val1 !== !val2) return false; - return `${val1}` === `${val2}`; -}; - -const allowedQueryKeys = [ - 'userOs', - 'userId', - 'userBrowser', - 'userDevice', - 'userCountry', - 'startDate', - 'endDate', - 'minDuration', - 'maxDuration', - 'referrer', - 'sort', - 'order', -]; - -@withLocationHandlers() -@connect( - (state) => ({ - filter: state.getIn(['filters', 'appliedFilter']), - variables: state.getIn(['customFields', 'list']), - sources: state.getIn(['customFields', 'sources']), - filterValues: state.get('filterValues'), - favoriteList: state.getIn(['sessions', 'favoriteList']), - currentProjectId: state.getIn(['site', 'siteId']), - sites: state.getIn(['site', 'list']), - watchdogs: state.getIn(['watchdogs', 'list']), - activeFlow: state.getIn(['filters', 'activeFlow']), - sessions: state.getIn(['sessions', 'list']), - }), - { - fetchFavoriteSessionList, - applyFilter, - addAttribute, - fetchFilterVariables, - fetchSources, - clearEvents, - setActiveTab, - clearSearch, - fetchSessions, - addFilterByKeyAndValue, - } -) -@withPageTitle('Sessions - OpenReplay') -export default class BugFinder extends React.PureComponent { - state = { showRehydratePanel: false }; - constructor(props) { - super(props); - - // TODO should cache the response - // props.fetchSources().then(() => { - // defaultFilters[6] = { - // category: 'Collaboration', - // type: 'CUSTOM', - // keys: this.props.sources.filter(({type}) => type === 'collaborationTool').map(({ label, key }) => ({ type: 'CUSTOM', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() - // }; - // defaultFilters[7] = { - // category: 'Logging Tools', - // type: 'ERROR', - // keys: this.props.sources.filter(({type}) => type === 'logTool').map(({ label, key }) => ({ type: 'ERROR', source: key, label: label, key, icon: 'integrations/' + key, isFilter: false })).toJS() - // }; - // }); - // if (props.sessions.size === 0) { - // props.fetchSessions(); - // } - - const queryFilter = this.props.query.all(allowedQueryKeys); - if (queryFilter.hasOwnProperty('userId')) { - props.addFilterByKeyAndValue(FilterKey.USERID, queryFilter.userId); - } else { - if (props.sessions.size === 0) { - props.fetchSessions(); - } - } - } - - toggleRehydratePanel = () => { - this.setState({ showRehydratePanel: !this.state.showRehydratePanel }); - }; - - setActiveTab = (tab) => { - this.props.setActiveTab(tab); - }; - - render() { - const { showRehydratePanel } = this.state; - - return ( -
-
-
- -
-
- -
- - -
- -
-
-
- ); - } -} diff --git a/frontend/app/components/BugFinder/DateRange.js b/frontend/app/components/BugFinder/DateRange.js deleted file mode 100644 index 9a6e77f12..000000000 --- a/frontend/app/components/BugFinder/DateRange.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { applyFilter } from 'Duck/search'; -import { fetchList as fetchFunnelsList } from 'Duck/funnels'; -import DateRangeDropdown from 'Shared/DateRangeDropdown'; - -@connect(state => ({ - filter: state.getIn([ 'search', 'instance' ]), -}), { - applyFilter, fetchFunnelsList -}) -export default class DateRange extends React.PureComponent { - onDateChange = (e) => { - // this.props.fetchFunnelsList(e.rangeValue) - this.props.applyFilter(e) - } - render() { - const { filter: { rangeValue, startDate, endDate }, className } = this.props; - - return ( - - ); - } -} \ No newline at end of file diff --git a/frontend/app/components/BugFinder/FilterSelectionButton.js b/frontend/app/components/BugFinder/FilterSelectionButton.js deleted file mode 100644 index 9854b29a3..000000000 --- a/frontend/app/components/BugFinder/FilterSelectionButton.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Icon } from 'UI'; -import stl from './filterSelectionButton.module.css'; - -const FilterSelectionButton = ({ label }) => { - return ( -
- { label } - -
- ); -}; - -export default FilterSelectionButton; diff --git a/frontend/app/components/BugFinder/Filters/SortDropdown.js b/frontend/app/components/BugFinder/Filters/SortDropdown.js deleted file mode 100644 index 398902ec5..000000000 --- a/frontend/app/components/BugFinder/Filters/SortDropdown.js +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Select from 'Shared/Select'; -import { Icon } from 'UI'; -import { sort } from 'Duck/sessions'; -import { applyFilter } from 'Duck/search'; -import stl from './sortDropdown.module.css'; - -@connect(null, { sort, applyFilter }) -export default class SortDropdown extends React.PureComponent { - state = { value: null } - sort = ({ value }) => { - value = value.value - this.setState({ value: value }) - const [ sort, order ] = value.split('-'); - const sign = order === 'desc' ? -1 : 1; - this.props.applyFilter({ order, sort }); - - this.props.sort(sort, sign) - setTimeout(() => this.props.sort(sort, sign), 3000); //AAA - } - - render() { - const { options } = this.props; - return ( - setEditing(true)} - /> - ) : ( -
{name && name.trim() === '' ? 'Series ' + (seriesIndex + 1) : name }
- )} - -
setEditing(true)}>
- - ); -} - -export default SeriesName; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts b/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts deleted file mode 100644 index 90e63cdb6..000000000 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/SeriesName/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './SeriesName'; \ No newline at end of file diff --git a/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts b/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts deleted file mode 100644 index 5882e382a..000000000 --- a/frontend/app/components/shared/CustomMetrics/FilterSeries/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FilterSeries' \ No newline at end of file diff --git a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx index 4719914ba..1c7e9c425 100644 --- a/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx +++ b/frontend/app/components/shared/DevTools/ConsolePanel/ConsolePanel.tsx @@ -12,7 +12,7 @@ import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorD import { useModal } from 'App/components/Modal'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter' -import useCellMeasurerCache from '../useCellMeasurerCache' +import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache' const ALL = 'ALL'; const INFO = 'INFO'; diff --git a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx index f34b3e04e..1f2212152 100644 --- a/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx +++ b/frontend/app/components/shared/DevTools/NetworkPanel/NetworkPanel.tsx @@ -384,5 +384,5 @@ function NetworkPanel({ startedAt }: { startedAt: number }) { } export default connect((state: any) => ({ - startedAt: state.getIn(['sessions', 'current', 'startedAt']), + startedAt: state.getIn(['sessions', 'current']).startedAt, }))(observer(NetworkPanel)); diff --git a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx index 366750b51..b97e82e97 100644 --- a/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx +++ b/frontend/app/components/shared/DevTools/StackEventPanel/StackEventPanel.tsx @@ -12,7 +12,7 @@ import StackEventRow from 'Shared/DevTools/StackEventRow'; import StackEventModal from '../StackEventModal'; import useAutoscroll, { getLastItemTime } from '../useAutoscroll'; import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter' -import useCellMeasurerCache from '../useCellMeasurerCache' +import useCellMeasurerCache from 'App/hooks/useCellMeasurerCache' const INDEX_KEY = 'stackEvent'; const ALL = 'ALL'; diff --git a/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx index 2fa0343e3..bbb2b204e 100644 --- a/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx +++ b/frontend/app/components/shared/DevTools/TimeTable/TimeTable.tsx @@ -223,7 +223,7 @@ export default class TimeTable extends React.PureComponent { {columns .filter((i: any) => !i.hidden) .map(({ dataKey, render, width, label }) => ( -
+
{render ? render(row) : row[dataKey || ''] || {'empty'}} @@ -327,7 +327,7 @@ export default class TimeTable extends React.PureComponent {
{columns.map(({ label, width, dataKey, onClick = null }) => (
{ ))} {visibleRefLines.map(({ time, color, onClick }) => (
new CellMeasurerCache({ - fixedWidth: true, - keyMapper: (index) => filteredListRef.current[index], - }), []) -} \ No newline at end of file diff --git a/frontend/app/components/shared/SessionItem/SessionItem.tsx b/frontend/app/components/shared/SessionItem/SessionItem.tsx index fbfd2202e..a59f8e5aa 100644 --- a/frontend/app/components/shared/SessionItem/SessionItem.tsx +++ b/frontend/app/components/shared/SessionItem/SessionItem.tsx @@ -38,7 +38,6 @@ interface Props { userNumericHash: number; live: boolean; metadata: Record; - userSessionsCount: number; issueTypes: []; active: boolean; isCallActive?: boolean; diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx index 5f279c394..57c41b411 100644 --- a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx +++ b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx @@ -169,7 +169,7 @@ function SessionList(props: Props) {
} - show={!loading && list.size === 0} + show={!loading && list.length === 0} > {list.map((session: any) => (
@@ -188,7 +188,7 @@ function SessionList(props: Props) {
Showing {(currentPage - 1) * pageSize + 1} to{' '} - {(currentPage - 1) * pageSize + list.size} of{' '} + {(currentPage - 1) * pageSize + list.length} of{' '} {numberWithCommas(total)} sessions.
( -
- 0 ? 'gray' : 'gray-medium'} /> -
0 ? 'color-gray' : 'color-gray-medium')}>{label}
-
-) - -function SessionStack({ flow = {}, applySavedFilter, setActiveTab, setActiveFlow }) { - const onAllClick = (flow) => { - setActiveFlow(flow) - applySavedFilter(flow.filter) - setActiveTab({ type: 'all', name: 'All'}) - } - return ( -
-
onAllClick(flow)}> - {flow.name} -
-
-
{flow.count} Sessions
-
- {flow.watchdogs.map(({type, count}) => ( - - ))} -
-
-
- ) -} - -export default connect(null, { applySavedFilter, setActiveTab, setActiveFlow })(SessionStack) diff --git a/frontend/app/components/shared/SessionStack/index.js b/frontend/app/components/shared/SessionStack/index.js deleted file mode 100644 index db3464728..000000000 --- a/frontend/app/components/shared/SessionStack/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './SessionStack'; \ No newline at end of file diff --git a/frontend/app/components/shared/SessionStack/sessionStack.module.css b/frontend/app/components/shared/SessionStack/sessionStack.module.css deleted file mode 100644 index 30b2a6eef..000000000 --- a/frontend/app/components/shared/SessionStack/sessionStack.module.css +++ /dev/null @@ -1,18 +0,0 @@ - -@import 'mixins.css'; - -.wrapper { - background: #fff; - border: solid thin $gray-light; - border-radius: 3px; - @mixin defaultHover; - box-shadow: - /* The top layer shadow */ - /* 0 1px 1px rgba(0,0,0,0.15), */ - /* The second layer */ - 4px 4px 1px 1px white, - /* The second layer shadow */ - 4px 4px 0px 1px rgba(0,0,0,0.4); - /* Padding for demo purposes */ - padding: 16px; -} \ No newline at end of file diff --git a/frontend/app/components/shared/SharePopup/SharePopup.js b/frontend/app/components/shared/SharePopup/SharePopup.js index 47f2b68b7..1df16cf6d 100644 --- a/frontend/app/components/shared/SharePopup/SharePopup.js +++ b/frontend/app/components/shared/SharePopup/SharePopup.js @@ -12,7 +12,7 @@ import { fetchList as fetchTeams, sendMsTeamsMsg } from 'Duck/integrations/teams @connect( (state) => ({ - sessionId: state.getIn(['sessions', 'current', 'sessionId']), + sessionId: state.getIn(['sessions', 'current']).sessionId, channels: state.getIn(['slack', 'list']), msTeamsChannels: state.getIn(['teams', 'list']), tenantId: state.getIn(['user', 'account', 'tenantId']), diff --git a/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx index 4acd355c2..2aca274ea 100644 --- a/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx +++ b/frontend/app/components/ui/ErrorDetails/ErrorDetails.tsx @@ -80,7 +80,7 @@ export default connect( (state: any) => ({ // errorStack: state.getIn(['sessions', 'errorStack']), errorStack: state.getIn(['errors', 'instanceTrace']), - sessionId: state.getIn(['sessions', 'current', 'sessionId']), + sessionId: state.getIn(['sessions', 'current']).sessionId, }), { fetchErrorStackList } )(ErrorDetails); diff --git a/frontend/app/duck/ReducerModule.js.dev b/frontend/app/duck/ReducerModule.js.dev deleted file mode 100644 index c80c65a37..000000000 --- a/frontend/app/duck/ReducerModule.js.dev +++ /dev/null @@ -1,55 +0,0 @@ - -redux -> other storage ::<< Entities + Lists + relations <|> methods:: crud. request declaration -> request realisation with middleware -< (uses) MODEL - - - -!request declaration - - - -action/request formatter => ReducerModule Fabrique => - - -class ReducerModule { - _ns = "common" - _switch = {} - _n = 0 - - constructor(namespace) { - this._ns = namespace - } - - /** - Action: state => newState | { reduce: state, action => newState, creator: () => {objects to action} } - */ - actions(actns): this { - Object.keys(actns).map(key => { - const type = `${this._namespace}/${key.toUpperCase()}`; - this._switch[ type ] = actns[ key ]; - }); - return this; - } - - requests(reqsts): this { - Object.keys(reqsts).map(key => { - const type = `${this._namespace}/${key.toUpperCase()}`; - this._switch[ type ] = actns[ key ]; - }); - return this; - } - - get actionTypes() { - - } - - get actionCreators() { - - } - - get reducer() { - return (state, action = {}) => { - const reduce = this._switch[ action.type ]; - return reduce ? reduce(state, action) : state; - } - } -} \ No newline at end of file diff --git a/frontend/app/duck/announcements.js b/frontend/app/duck/announcements.js deleted file mode 100644 index 3a7612ee7..000000000 --- a/frontend/app/duck/announcements.js +++ /dev/null @@ -1,45 +0,0 @@ -import { List, Map } from 'immutable'; -import Announcement from 'Types/announcement'; -import { RequestTypes } from './requestStateCreator'; - -import { mergeReducers } from './funcTools/tools'; -import { createRequestReducer } from './funcTools/request'; -import { - createCRUDReducer, - getCRUDRequestTypes, - createFetchList -} from './funcTools/crud'; - -const name = 'announcement'; -const idKey = 'id'; - -const SET_LAST_READ = new RequestTypes('announcement/SET_LAST_READ'); - -const initialState = Map({ - list: List() -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case SET_LAST_READ.SUCCESS: - return state.update('list', (list) => list.map(i => ({...i.toJS(), viewed: true }))); - } - return state; -}; - -export function setLastRead() { - return { - types: SET_LAST_READ.toArray(), - call: client => client.get(`/announcements/view`), - }; -} - -export const fetchList = createFetchList(name); - -export default mergeReducers( - reducer, - createCRUDReducer(name, Announcement, idKey), - createRequestReducer({ - ...getCRUDRequestTypes(name), - }), -); \ No newline at end of file diff --git a/frontend/app/duck/assignments.js b/frontend/app/duck/assignments.js index 3abb99fb7..427937e05 100644 --- a/frontend/app/duck/assignments.js +++ b/frontend/app/duck/assignments.js @@ -2,10 +2,9 @@ import { List, Map, Set } from 'immutable'; import Assignment from 'Types/session/assignment'; import Activity from 'Types/session/activity'; import withRequestState, { RequestTypes } from './requestStateCreator'; -import { createListUpdater, createItemInListUpdater } from './funcTools/tools'; +import { createListUpdater } from './funcTools/tools'; import { editType, initType } from './funcTools/crud/types'; import { createInit, createEdit } from './funcTools/crud'; -import IssuesType from 'Types/issue/issuesType' const idKey = 'id'; const name = 'assignment'; @@ -22,8 +21,8 @@ const INIT = initType(name); const initialState = Map({ list: List(), - instance: Assignment(), - activeIssue: Assignment(), + instance: new Assignment(), + activeIssue: new Assignment(), issueTypes: List(), issueTypeIcons: Set(), users: List(), @@ -33,22 +32,23 @@ const initialState = Map({ const reducer = (state = initialState, action = {}) => { const users = state.get('users'); - var issueTypes = [] + let issueTypes = [] switch (action.type) { case INIT: action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : ''; - return state.set('instance', Assignment(action.instance)); + return state.set('instance', new Assignment(action.instance)); case EDIT: - return state.mergeIn([ 'instance' ], action.instance); + const inst = state.get('instance') + return state.set('instance', new Assignment({ ...inst, ...action.instance })); case FETCH_PROJECTS.SUCCESS: return state.set('projects', List(action.data)).set('projectsFetched', true); case FETCH_ASSIGNMENTS.SUCCESS: - return state.set('list', List(action.data).map(Assignment)); + return state.set('list', List(action.data).map(as => new Assignment(as))); case FETCH_ASSIGNMENT.SUCCESS: - return state.set('activeIssue', Assignment({ ...action.data, users})); + return state.set('activeIssue', new Assignment({ ...action.data, users})); case FETCH_META.SUCCESS: issueTypes = action.data.issueTypes - var issueTypeIcons = {} + const issueTypeIcons = {} issueTypes.forEach(iss => { issueTypeIcons[iss.id] = iss.iconUrl }) @@ -56,12 +56,12 @@ const reducer = (state = initialState, action = {}) => { .set('users', List(action.data.users)) .set('issueTypeIcons', issueTypeIcons) case ADD_ACTIVITY.SUCCESS: - const instance = Assignment(action.data); + const instance = new Assignment(action.data); return listUpdater(state, instance); case ADD_MESSAGE.SUCCESS: const user = users.filter(user => user.id === action.data.author).first(); - const activity = Activity({ type: 'message', user, ...action.data,}); - return state.updateIn([ 'activeIssue', 'activities' ], list => list.push(activity)); + const activity = new Activity({ type: 'message', user, ...action.data,}); + return state.update([ 'activeIssue' ], issue => issue.activities.push(activity)); default: return state; } @@ -79,7 +79,7 @@ export default withRequestState({ export const init = createInit(name); export const edit = createEdit(name); -export function fetchProjects(sessionId) { +export function fetchProjects() { return { types: FETCH_PROJECTS.toArray(), call: client => client.get(`/integrations/issues/list_projects`) @@ -100,13 +100,6 @@ export function fetchAssignments(sessionId) { } } -export function fetchAssigment(sessionId, id) { - return { - types: FETCH_ASSIGNMENT.toArray(), - call: client => client.get(`/sessions/${ sessionId }/assign/${ id }`) - } -} - export function addActivity(sessionId, params) { const data = { ...params, assignee: params.assignee, issueType: params.issueType } return { diff --git a/frontend/app/duck/config.js b/frontend/app/duck/config.js deleted file mode 100644 index a445e3f50..000000000 --- a/frontend/app/duck/config.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Map } from 'immutable'; -import { saveType, fetchType, editType } from './funcTools/crud/types'; -import { mergeReducers, success, array } from './funcTools/tools'; -import { createRequestReducer } from './funcTools/request'; - -const name = 'config' - -const FETCH = fetchType(name); -const SAVE = saveType(name); -const EDIT = editType(name); - -const FETCH_SUCCESS = success(FETCH); -const SAVE_SUCCESS = success(SAVE); - -const initialState = Map({ - options: { - weeklyReport: false - }, -}); - -const reducer = (state = initialState, action = {}) => { - switch(action.type) { - case FETCH_SUCCESS: - return state.set('options', action.data) - case SAVE_SUCCESS: - return state - case EDIT: - return state.set('options', action.config) - default: - return state; - } -} - -export const fetch = () => { - return { - types: array(FETCH), - call: client => client.get(`/config/weekly_report`), - } -} - -export const save = (config) => { - return { - types: array(SAVE), - call: client => client.post(`/config/weekly_report`, config), - } -} - -export const edit = (config) => { - return { - type: EDIT, - config - } -} - -export default mergeReducers( - reducer, - createRequestReducer({ - fetchRequest: FETCH, - saveRequest: SAVE, - }), -) \ No newline at end of file diff --git a/frontend/app/duck/customMetrics.js b/frontend/app/duck/customMetrics.js index e6713acb4..c740f7a57 100644 --- a/frontend/app/duck/customMetrics.js +++ b/frontend/app/duck/customMetrics.js @@ -1,8 +1,8 @@ import { List, Map } from 'immutable'; import CustomMetric, { FilterSeries } from 'Types/customMetric' -import { createFetch, fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud'; +import { fetchListType, fetchType, saveType, removeType, editType, createRemove, createEdit } from './funcTools/crud'; import { createRequestReducer, ROOT_KEY } from './funcTools/request'; -import { array, request, success, failure, createListUpdater, mergeReducers } from './funcTools/tools'; +import { array, success, createListUpdater, mergeReducers } from './funcTools/tools'; import Filter from 'Types/filter'; import Session from 'Types/session'; @@ -28,11 +28,6 @@ const INIT = `${name}/INIT`; const SET_ACTIVE_WIDGET = `${name}/SET_ACTIVE_WIDGET`; const REMOVE = removeType(name); const UPDATE_SERIES = `${name}/UPDATE_SERIES`; -const SET_ALERT_METRIC_ID = `${name}/SET_ALERT_METRIC_ID`; - -function chartWrapper(chart = []) { - return chart.map(point => ({ ...point, count: Math.max(point.count, 0) })); -} const updateItemInList = createListUpdater(idKey); const updateInstance = (state, instance) => state.getIn([ "instance", idKey ]) === instance[ idKey ] @@ -97,7 +92,7 @@ function reducer(state = initialState, action = {}) { const { data } = action; return state.set("list", List(data.map(CustomMetric))); case success(FETCH_SESSION_LIST): - return state.set("sessionList", List(action.data.map(item => ({ ...item, sessions: item.sessions.map(Session) })))); + return state.set("sessionList", List(action.data.map(item => ({ ...item, sessions: item.sessions.map(s => new Session(s)) })))); case SET_ACTIVE_WIDGET: return state.set("activeWidget", action.widget).set('sessionList', List()); } @@ -117,12 +112,6 @@ export default mergeReducers( export const edit = createEdit(name); export const remove = createRemove(name); -export const updateSeries = (index, series) => ({ - type: UPDATE_SERIES, - index, - series, -}); - export function fetch(id) { return { id, @@ -147,34 +136,6 @@ export function fetchList() { }; } -export function setAlertMetricId(id) { - return { - type: SET_ALERT_METRIC_ID, - id, - }; -} - -export const addSeries = (series = null) => (dispatch, getState) => { - const instance = getState().getIn([ 'customMetrics', 'instance' ]); - const seriesIndex = instance.series.size; - const newSeries = series || { - name: `Series ${seriesIndex + 1}`, - filter: new Filter({ filters: [], eventsOrder: 'then' }), - }; - - dispatch({ - type: ADD_SERIES, - series: newSeries, - }); -} - -export const removeSeries = (index) => (dispatch, getState) => { - dispatch({ - type: REMOVE_SERIES, - index, - }); -} - export const init = (instance = null, forceNull = false) => (dispatch, getState) => { dispatch({ type: INIT, diff --git a/frontend/app/duck/environments.js b/frontend/app/duck/environments.js deleted file mode 100644 index 8356c4281..000000000 --- a/frontend/app/duck/environments.js +++ /dev/null @@ -1,7 +0,0 @@ -import Environment from 'Types/environment'; -import crudDuckGenerator from './tools/crudDuck'; - -const crudDuck = crudDuckGenerator('environment', Environment); -export const { fetchList, fetch, init, edit, save, remove } = crudDuck.actions; - -export default crudDuck.reducer; diff --git a/frontend/app/duck/errors.js b/frontend/app/duck/errors.js index 32cbf4c40..8a23875cc 100644 --- a/frontend/app/duck/errors.js +++ b/frontend/app/duck/errors.js @@ -66,6 +66,8 @@ function reducer(state = initialState, action = {}) { } else { return state.set("instance", ErrorInfo(action.data)); } + case failure(FETCH): + return state.set("instance", ErrorInfo()); case success(FETCH_TRACE): return state.set("instanceTrace", List(action.data.trace)).set('sourcemapUploaded', action.data.sourcemapUploaded); case success(FETCH_LIST): diff --git a/frontend/app/duck/events.js b/frontend/app/duck/events.js deleted file mode 100644 index 000bd24b7..000000000 --- a/frontend/app/duck/events.js +++ /dev/null @@ -1,81 +0,0 @@ -import { List, Map, Set } from 'immutable'; -import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; -import Event from 'Types/filter/event'; -import CustomFilter from 'Types/filter/customFilter'; -import { KEYS } from 'Types/filter/customFilter'; -import logger from 'App/logger'; -import { countries } from 'App/constants'; -import { getRE } from 'App/utils'; - -const FETCH_LIST = new RequestTypes('events/FETCH_LIST'); -const TOGGLE_SELECT = 'events/TOGGLE_SELECT'; -const SET_SELECTED = 'events/SET_SELECTED'; - -const countryOptions = Object.keys(countries).map(c => ({filterKey: KEYS.USER_COUNTRY, label: KEYS.USER_COUNTRY, type: KEYS.USER_COUNTRY, value: c, actualValue: countries[c], isFilter: true })); - -const initialState = Map({ - list: List(), - store: Set(), - - // replace? - selected: Set(), -}); - -const filterKeys = ['METADATA', KEYS.USERID, KEYS.USER_COUNTRY, KEYS.USER_BROWSER, KEYS.USER_OS, KEYS.USER_DEVICE, KEYS.REFERRER] - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_LIST.SUCCESS: { - const regCountry = getRE(action.params.q, 'i'); - const countryOptionsFiltered = List(countryOptions).filter(({ actualValue }) => regCountry.test(actualValue)).take(5); - - const eventList = List(action.data).concat(countryOptionsFiltered).map(item => ( - filterKeys.includes(item.type) ? - CustomFilter({...item, isFilter: true }) : - Event({...item, key: item.type, filterKey: item.type, label: item.type}) ) - ); - - return state - .set('list', eventList) - .update('store', store => store.concat(eventList)); - } - // TODO: use ids. or make a set-hoc? - case TOGGLE_SELECT: { - const { event, flag } = action; - const shouldBeInSet = typeof flag === 'boolean' - ? flag - : !state.get('selected').contains(event); - return state.update('selected', set => (shouldBeInSet - ? set.add(event) - : set.remove(event))); - } - case SET_SELECTED: - return state.set('selected', Set(action.events)); - } - return state; -}; - -export default withRequestState(FETCH_LIST, reducer); - -export function fetchList(params) { - return { - types: FETCH_LIST.toArray(), - call: client => client.get('/events/search', params), - params, - }; -} - -export function toggleSelect(event, flag) { - return { - type: TOGGLE_SELECT, - event, - flag, - }; -} - -export function setSelected(events) { - return { - type: SET_SELECTED, - events, - }; -} diff --git a/frontend/app/duck/funcTools/tools.js b/frontend/app/duck/funcTools/tools.js index e2be77371..139c824ac 100644 --- a/frontend/app/duck/funcTools/tools.js +++ b/frontend/app/duck/funcTools/tools.js @@ -27,11 +27,6 @@ export function createItemInListUpdater(idKey = 'id', shouldAdd = true) { } } -export function createItemInListFilter(idKey = 'id') { - return id => - list => list.filter(item => item[ idKey ] !== id) -} - export const request = type => `${ type }_REQUEST`; export const success = type => `${ type }_SUCCESS`; export const failure = type => `${ type }_FAILURE`; diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js index 3abfcc450..d175b64d5 100644 --- a/frontend/app/duck/funnels.js +++ b/frontend/app/duck/funnels.js @@ -99,12 +99,12 @@ const reducer = (state = initialState, action = {}) => { .set('criticalIssuesCount', action.data.issues.criticalIssuesCount) case FETCH_SESSIONS_SUCCESS: return state - .set('sessions', List(action.data.sessions).map(Session)) + .set('sessions', List(action.data.sessions).map(s => new Session(s))) .set('total', action.data.total) case FETCH_ISSUE_SUCCESS: return state .set('issue', FunnelIssue(action.data.issue)) - .set('sessions', List(action.data.sessions.sessions).map(Session)) + .set('sessions', List(action.data.sessions.sessions).map(s => new Session(s))) .set('sessionsTotal', action.data.sessions.total) case RESET_ISSUE: return state.set('isses', FunnelIssue()) diff --git a/frontend/app/duck/index.ts b/frontend/app/duck/index.ts index 0371396bf..4e8e24d8f 100644 --- a/frontend/app/duck/index.ts +++ b/frontend/app/duck/index.ts @@ -1,18 +1,13 @@ +// @ts-ignore import { combineReducers } from 'redux-immutable'; import user from './user'; import sessions from './sessions'; import assignments from './assignments'; -import target from './target'; -import targetCustom from './targetCustom'; import filters from './filters'; import funnelFilters from './funnelFilters'; -import events from './events'; -import environments from './environments'; -import variables from './variables'; import templates from './templates'; import alerts from './alerts'; -import notifications from './notifications'; import dashboard from './dashboard'; import components from './components'; import sources from './sources'; @@ -21,12 +16,9 @@ import site from './site'; import customFields from './customField'; import webhooks from './webhook'; import integrations from './integrations'; -import watchdogs from './watchdogs'; import rehydrate from './rehydrate'; -import announcements from './announcements'; import errors from './errors'; import funnels from './funnels'; -import config from './config'; import roles from './roles'; import customMetrics from './customMetrics'; import search from './search'; @@ -36,29 +28,20 @@ const rootReducer = combineReducers({ user, sessions, assignments, - target, - targetCustom, filters, funnelFilters, - events, - environments, - variables, templates, alerts, - notifications, dashboard, components, members, site, customFields, webhooks, - watchdogs, rehydrate, - announcements, errors, funnels, - config, roles, customMetrics, search, diff --git a/frontend/app/duck/issues.js b/frontend/app/duck/issues.js index 6c2c70733..6cc97a96f 100644 --- a/frontend/app/duck/issues.js +++ b/frontend/app/duck/issues.js @@ -9,7 +9,6 @@ import { createInit, createEdit } from './funcTools/crud'; const idKey = 'id'; const name = 'assignment'; const listUpdater = createListUpdater(idKey); -const itemInListUpdater = createItemInListUpdater(idKey); const FETCH_ASSIGNMENTS = new RequestTypes('asignment/FETCH_ASSIGNMENTS'); const FETCH_ISSUE = new RequestTypes('asignment/FETCH_ISSUE'); @@ -23,8 +22,8 @@ const RESET_ACTIVE_ISSUE = 'assignment/RESET_ACTIVE_ISSUE'; const initialState = Map({ list: List(), - instance: Assignment(), - activeIssue: Assignment(), + instance: new Assignment(), + activeIssue: new Assignment(), issueTypes: List(), issueTypeIcons: Set(), users: List(), @@ -39,9 +38,9 @@ const reducer = (state = initialState, action = {}) => { case FETCH_PROJECTS.SUCCESS: return state.set('projects', List(action.data)); case FETCH_ASSIGNMENTS.SUCCESS: - return state.set('list', List(action.data).map(Assignment)); + return state.set('list', action.data.map(as => new Assignment(as))); case ADD_ACTIVITY.SUCCESS: - const instance = Assignment(action.data); + const instance = new Assignment(action.data); return listUpdater(state, instance); case FETCH_META.SUCCESS: issueTypes = action.data.issueTypes; @@ -53,16 +52,16 @@ const reducer = (state = initialState, action = {}) => { .set('users', List(action.data.users)) .set('issueTypeIcons', issueTypeIcons) case FETCH_ISSUE.SUCCESS: - return state.set('activeIssue', Assignment({ ...action.data, users})); + return state.set('activeIssue', new Assignment({ ...action.data, users})); case RESET_ACTIVE_ISSUE: - return state.set('activeIssue', Assignment()); + return state.set('activeIssue', new Assignment()); case ADD_MESSAGE.SUCCESS: const user = users.filter(user => user.id === action.data.author).first(); - const activity = Activity({ type: 'message', user, ...action.data,}); + const activity = new Activity({ type: 'message', user, ...action.data,}); return state.updateIn([ 'activeIssue', 'activities' ], list => list.push(activity)); case INIT: action.instance.issueType = issueTypes.length > 0 ? issueTypes[0].id : ''; - return state.set('instance', Assignment(action.instance)); + return state.set('instance', new Assignment(action.instance)); case EDIT: return state.mergeIn([ 'instance' ], action.instance); default: @@ -101,13 +100,6 @@ export function fetchProjects() { } } -export function fetchIssue(sessionId, id) { - return { - types: FETCH_ISSUE.toArray(), - call: client => client.get(`/sessions/${ sessionId }/assign/jira/${ id }`) - } -} - export function fetchMeta(projectId) { return { types: FETCH_META.toArray(), diff --git a/frontend/app/duck/liveSearch.js b/frontend/app/duck/liveSearch.js index 591dd7084..8500158bd 100644 --- a/frontend/app/duck/liveSearch.js +++ b/frontend/app/duck/liveSearch.js @@ -36,7 +36,7 @@ function reducer(state = initialState, action = {}) { return state.set('currentPage', action.page); case success(FETCH_SESSION_LIST): const { sessions, total } = action.data; - const list = List(sessions).map(Session); + const list = List(sessions).map(s => new Session(s)); return state .set('list', list) .set('total', total); diff --git a/frontend/app/duck/member.js b/frontend/app/duck/member.js index 31cccb395..ce12c1659 100644 --- a/frontend/app/duck/member.js +++ b/frontend/app/duck/member.js @@ -1,7 +1,7 @@ import { Map } from 'immutable'; import Member from 'Types/member'; import crudDuckGenerator from './tools/crudDuck'; -import withRequestState, { RequestTypes } from 'Duck/requestStateCreator'; +import { RequestTypes } from 'Duck/requestStateCreator'; import { reduceDucks } from 'Duck/tools'; const GENERATE_LINK = new RequestTypes('member/GENERATE_LINK'); @@ -37,12 +37,4 @@ export function save(instance) { }; } -export function generateInviteLink(instance) { - return { - types: GENERATE_LINK.toArray(), - call: client => client.get(`/client/members/${ instance.id }/reset`), - id: instance.id - }; -} - export default reduceDucks(crudDuck, { initialState, reducer }).reducer; diff --git a/frontend/app/duck/notifications.js b/frontend/app/duck/notifications.js deleted file mode 100644 index 129349792..000000000 --- a/frontend/app/duck/notifications.js +++ /dev/null @@ -1,63 +0,0 @@ -import { List, Map } from 'immutable'; -import Notification from 'Types/notification'; -import { mergeReducers, success, array, request, createListUpdater } from './funcTools/tools'; -import { createRequestReducer } from './funcTools/request'; -import { - createCRUDReducer, - getCRUDRequestTypes, - createFetchList, -} from './funcTools/crud'; - -const name = 'notification'; -const idKey = 'notificationId'; -const SET_VIEWED = 'notifications/SET_VIEWED'; -const CLEAR_ALL = 'notifications/CLEAR_ALL'; -const SET_VIEWED_SUCCESS = success(SET_VIEWED); -const CLEAR_ALL_SUCCESS = success(CLEAR_ALL); - -const listUpdater = createListUpdater(idKey); - -const initialState = Map({ - list: List(), -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case SET_VIEWED_SUCCESS: - if (!action.data) return state; - const item = state.get('list').find(item => item[ idKey ] === action.id) - return listUpdater(state, Notification({...item.toJS(), createdAt: item.createdAt.ts, viewed: true })); - case CLEAR_ALL_SUCCESS: - if (!action.data) return state; - return state.update('list', list => list.map(l => Notification({...l.toJS(), createdAt: l.createdAt.ts, viewed: true }))) - } - return state; -}; - -export const fetchList = createFetchList(name); - -export default mergeReducers( - reducer, - createCRUDReducer(name, Notification, idKey), - createRequestReducer({ - setViewed: SET_VIEWED, - clearAll: CLEAR_ALL, - ...getCRUDRequestTypes(name), - }), -); - -export function setViewed(id) { - return { - types: array(SET_VIEWED), - call: client => client.get(`/notifications/${ id }/view`), - id, - }; -} - -export function clearAll(params) { - return { - types: array(CLEAR_ALL), - call: client => client.post('/notifications/view', params), - }; -} - diff --git a/frontend/app/duck/sessions.js b/frontend/app/duck/sessions.ts similarity index 77% rename from frontend/app/duck/sessions.js rename to frontend/app/duck/sessions.ts index 8afb6d073..11191f9cd 100644 --- a/frontend/app/duck/sessions.js +++ b/frontend/app/duck/sessions.ts @@ -1,6 +1,7 @@ import { List, Map } from 'immutable'; import Session from 'Types/session'; import ErrorStack from 'Types/session/errorStack'; +import { Location, InjectedEvent } from 'Types/session/event' import Watchdog from 'Types/watchdog'; import { clean as cleanParams } from 'App/api_client'; import withRequestState, { RequestTypes } from './requestStateCreator'; @@ -9,7 +10,6 @@ import { LAST_7_DAYS } from 'Types/app/period'; import { getDateRangeFromValue } from 'App/dateRange'; const name = 'sessions'; -const INIT = 'sessions/INIT'; const FETCH_LIST = new RequestTypes('sessions/FETCH_LIST'); const FETCH_AUTOPLAY_LIST = new RequestTypes('sessions/FETCH_AUTOPLAY_LIST'); const FETCH = new RequestTypes('sessions/FETCH'); @@ -46,10 +46,10 @@ const defaultDateFilters = { endDate: range.end.unix() * 1000, }; -const initialState = Map({ - list: List(), +const initObj = { + list: [], sessionIds: [], - current: Session(), + current: new Session(), total: 0, keyMap: Map(), wdTypeCount: Map(), @@ -60,8 +60,9 @@ const initialState = Map({ eventsIndex: [], sourcemapUploaded: true, filteredEvents: null, + eventsQuery: '', showChatWindow: false, - liveSessions: List(), + liveSessions: [], visitedEvents: List(), insights: List(), insightFilters: defaultDateFilters, @@ -72,30 +73,31 @@ const initialState = Map({ lastPlayedSessionId: null, timeLineTooltip: { time: 0, offset: 0, isVisible: false, timeStr: '' }, createNoteTooltip: { time: 0, isVisible: false, isEdit: false, note: null }, -}); +} -const reducer = (state = initialState, action = {}) => { +const initialState = Map(initObj); + +interface IAction extends Record{ + type: string; + data: any; +} + +const reducer = (state = initialState, action: IAction) => { switch (action.type) { - case INIT: - return state.set('current', Session(action.session)); - // case FETCH_LIST.REQUEST: - // return action.clear ? state.set('list', List()) : state; case FETCH_ERROR_STACK.SUCCESS: - return state.set('errorStack', List(action.data.trace).map(ErrorStack)).set('sourcemapUploaded', action.data.sourcemapUploaded); + return state.set('errorStack', List(action.data.trace).map(es => new ErrorStack(es))).set('sourcemapUploaded', action.data.sourcemapUploaded); case FETCH_LIVE_LIST.SUCCESS: - const liveList = List(action.data.sessions).map((s) => new Session({ ...s, live: true })); + const liveList = action.data.sessions.map((s) => new Session({ ...s, live: true })); return state.set('liveSessions', liveList); case FETCH_LIST.SUCCESS: const { sessions, total } = action.data; - const list = List(sessions).map(Session); + const list = sessions.map(s => new Session(s)); + console.log(sessions, list, action) return state .set('list', list) - .set('sessionIds', list.map(({ sessionId }) => sessionId).toJS()) - .set( - 'favoriteList', - list.filter(({ favorite }) => favorite) - ) + .set('sessionIds', list.map(({ sessionId }) => sessionId)) + .set('favoriteList', list.filter(({ favorite }) => favorite)) .set('total', total); case FETCH_AUTOPLAY_LIST.SUCCESS: let sessionIds = state.get('sessionIds'); @@ -110,29 +112,30 @@ const reducer = (state = initialState, action = {}) => { case SET_EVENT_QUERY: { const events = state.get('current').events; const query = action.filter.query; - // const filter = action.filter.filter; const searchRe = getRE(query, 'i'); - let filteredEvents = query ? events.filter((e) => searchRe.test(e.url) || searchRe.test(e.value) || searchRe.test(e.label)) : null; - // if (filter) { - // filteredEvents = filteredEvents ? filteredEvents.filter(e => e.type === filter) : events.filter(e => e.type === filter); - // } - return state.set('filteredEvents', filteredEvents); + const filteredEvents = query ? events.filter( + (e) => searchRe.test(e.url) + || searchRe.test(e.value) + || searchRe.test(e.label) + || searchRe.test(e.type) + || (e.type === 'LOCATION' && searchRe.test('visited')) + ) : null; + + return state.set('filteredEvents', filteredEvents).set('eventsQuery', query); } case FETCH.SUCCESS: { // TODO: more common.. or TEMP const events = action.filter.events; - // const filters = action.filter.filters; - const current = state.get('list').find(({ sessionId }) => sessionId === action.data.sessionId) || Session(); - const session = Session(action.data); + const session = new Session(action.data); - const matching = []; + const matching: number[] = []; - const visitedEvents = []; - const tmpMap = {}; + const visitedEvents: Location[] = []; + const tmpMap = new Set(); session.events.forEach((event) => { - if (event.type === 'LOCATION' && !tmpMap.hasOwnProperty(event.url)) { - tmpMap[event.url] = event.url; + if (event.type === 'LOCATION' && !tmpMap.has(event.url)) { + tmpMap.add(event.url); visitedEvents.push(event); } }); @@ -151,23 +154,34 @@ const reducer = (state = initialState, action = {}) => { }); }); return state - .set('current', current.merge(session)) + .set('current', session) .set('eventsIndex', matching) .set('visitedEvents', visitedEvents) .set('host', visitedEvents[0] && visitedEvents[0].host); } case FETCH_FAVORITE_LIST.SUCCESS: - return state.set('favoriteList', List(action.data).map(Session)); + return state.set('favoriteList', action.data.map(s => new Session(s))); case TOGGLE_FAVORITE.SUCCESS: { const id = action.sessionId; - const session = state.get('list').find(({ sessionId }) => sessionId === id); + let mutableState = state + const list = state.get('list') as unknown as Session[] + const sessionIdx = list.findIndex(({ sessionId }) => sessionId === id); + const session = list[sessionIdx] + const current = state.get('current') as unknown as Session; const wasInFavorite = state.get('favoriteList').findIndex(({ sessionId }) => sessionId === id) > -1; - return state - .update('current', (currentSession) => (currentSession.sessionId === id ? currentSession.set('favorite', !wasInFavorite) : currentSession)) - .update('list', (list) => list.map((listSession) => (listSession.sessionId === id ? listSession.set('favorite', !wasInFavorite) : listSession))) - .update('favoriteList', (list) => session ? - wasInFavorite ? list.filter(({ sessionId }) => sessionId !== id) : list.push(session.set('favorite', true)) : list + if (session && !wasInFavorite) { + session.favorite = true + mutableState = mutableState.updateIn(['list', sessionIdx], () => session) + } + if (current.sessionId === id) { + mutableState = mutableState.update('current', + (s: Session) => ({ ...s, favorite: !wasInFavorite}) + ) + } + return mutableState + .update('favoriteList', (list: Session[]) => session ? + wasInFavorite ? list.filter(({ sessionId }) => sessionId !== id) : list.push(session) : list ); } case SORT: { @@ -176,7 +190,7 @@ const reducer = (state = initialState, action = {}) => { diff = diff === 0 ? s1.startedAt - s2.startedAt : diff; return action.sign * diff; }; - return state.update('list', (list) => list.sort(comparator)).update('favoriteList', (list) => list.sort(comparator)); + return state.update('list', (list: Session[]) => list.sort(comparator)).update('favoriteList', (list: Session[]) => list.sort(comparator)); } case REDEFINE_TARGET: { // TODO: update for list @@ -209,8 +223,16 @@ const reducer = (state = initialState, action = {}) => { case SET_EDIT_NOTE_TOOLTIP: return state.set('createNoteTooltip', action.noteTooltip); case FILTER_OUT_NOTE: - return state.updateIn(['current', 'notesWithEvents'], (list) => - list.filter(evt => !evt.noteId || evt.noteId !== action.noteId) + return state.updateIn(['current'], + (session: Session) => ({ + ...session, + notesWithEvents: session.notesWithEvents.filter(item => { + if ('noteId' in item) { + return item.noteId !== action.noteId + } + return true + }) + }) ) case ADD_NOTE: return state.updateIn(['current', 'notesWithEvents'], (list) => @@ -222,25 +244,21 @@ const reducer = (state = initialState, action = {}) => { }) ) case UPDATE_NOTE: - const index = state.getIn(['current', 'notesWithEvents']).findIndex(item => item.noteId === action.note.noteId) - return state.setIn(['current', 'notesWithEvents', index], action.note) + const noteIndex = state.getIn(['current']).notesWithEvents.findIndex(item => item.noteId === action.note.noteId) + return state.setIn(['current', 'notesWithEvents', noteIndex], action.note) case SET_SESSION_PATH: return state.set('sessionPath', action.path); case LAST_PLAYED_SESSION_ID: - return updateListItem(state, action.sessionId, { viewed: true }).set('lastPlayedSessionId', action.sessionId); + const sessionList = state.get('list') as unknown as Session[]; + const sIndex = sessionList.findIndex(({ sessionId }) => sessionId === action.sessionId); + if (sIndex === -1) return state; + + return state.updateIn(['list', sIndex], (session: Session) => ({ ...session, viewed: true })); default: return state; } }; -function updateListItem(state, sourceSessionId, instance) { - const list = state.get('list'); - const index = list.findIndex(({ sessionId }) => sessionId === sourceSessionId); - if (index === -1) return state; - - return state.updateIn(['list', index], (session) => session.merge(instance)); -} - export default withRequestState( { _: [FETCH, FETCH_LIST], @@ -253,13 +271,6 @@ export default withRequestState( reducer ); -function init(session) { - return { - type: INIT, - session, - }; -} - export const fetchList = (params = {}, force = false) => (dispatch) => { @@ -316,13 +327,6 @@ export function toggleFavorite(sessionId) { }; } -export function fetchFavoriteList() { - return { - types: FETCH_FAVORITE_LIST.toArray(), - call: (client) => client.get('/sessions/favorite'), - }; -} - export function fetchInsights(params) { return { types: FETCH_INSIGHTS.toArray(), @@ -353,13 +357,6 @@ export function sort(sortKey, sign = 1, listName = 'list') { }; } -export function redefineTarget(target) { - return { - type: REDEFINE_TARGET, - target, - }; -} - export const setAutoplayValues = (sessionId) => { return { type: SET_AUTOPLAY_VALUES, diff --git a/frontend/app/duck/target.js b/frontend/app/duck/target.js deleted file mode 100644 index b1f0e337b..000000000 --- a/frontend/app/duck/target.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Map } from 'immutable'; -import Target from 'Types/target'; -import { RequestTypes } from 'Duck/requestStateCreator'; -import crudDuckGenerator from 'Duck/tools/crudDuck'; -import { reduceDucks } from 'Duck/tools'; - -const FETCH_DEFINED = new RequestTypes('targets/FETCH_DEFINED'); - -const initialState = Map({ - definedPercent: 0, -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_DEFINED.SUCCESS: - return state.set( - 'definedPercent', - Math.round((action.data.labeled / action.data.total) * 100), - ); - } - return state; -}; - -const crudDuck = crudDuckGenerator('target', Target); -export const { fetchList, init, edit, save, remove } = crudDuck.actions; -export default reduceDucks(crudDuck, { initialState, reducer }).reducer; - -export function fetchDefinedTargetsCount() { - return { - types: FETCH_DEFINED.toArray(), - call: client => client.get('/targets/count'), - }; -} diff --git a/frontend/app/duck/targetCustom.js b/frontend/app/duck/targetCustom.js deleted file mode 100644 index fd63ed657..000000000 --- a/frontend/app/duck/targetCustom.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Map } from 'immutable'; -import TargetCustom from 'Types/targetCustom'; -import crudDuckGenerator from 'Duck/tools/crudDuck'; -import { reduceDucks } from 'Duck/tools'; - - -const crudDuck = crudDuckGenerator('customTarget', TargetCustom, { endpoints: { - fetchList: '/targets_temp', - save: '/targets_temp', - remove: '/targets_temp', -}}); -export const { fetchList, init, edit, save, remove } = crudDuck.actions; -export default crudDuck.reducer; diff --git a/frontend/app/duck/variables.js b/frontend/app/duck/variables.js deleted file mode 100644 index 21a0131c4..000000000 --- a/frontend/app/duck/variables.js +++ /dev/null @@ -1,9 +0,0 @@ -import Variable from 'Types/variable'; -import crudDuckGenerator from './tools/crudDuck'; - -const crudDuck = crudDuckGenerator('variable', Variable); -export const { - fetchList, fetch, init, edit, save, remove, -} = crudDuck.actions; - -export default crudDuck.reducer; diff --git a/frontend/app/duck/watchdogs.js b/frontend/app/duck/watchdogs.js deleted file mode 100644 index 87966264a..000000000 --- a/frontend/app/duck/watchdogs.js +++ /dev/null @@ -1,101 +0,0 @@ -import { List, Map } from 'immutable'; -import Watchdog from 'Types/watchdog'; -import { mergeReducers, success, array, request } from './funcTools/tools'; -import { createRequestReducer } from './funcTools/request'; -import { - createCRUDReducer, - getCRUDRequestTypes, - createFetchList, - createInit, - createEdit, - createRemove, - createSave, -} from './funcTools/crud'; - -const name = 'issue_type'; -const idKey = 'id'; -const SET_ACTIVE_TAB = 'watchdogs/SET_ACTIVE_TAB'; -const FETCH_WATCHDOG_STATUS = 'watchdogs/FETCH_WATCHDOG_STATUS'; -const FETCH_WATCHDOG_STATUS_SUCCESS = success(FETCH_WATCHDOG_STATUS); -const FETCH_RULES = 'watchdogs/FETCH_RULES'; -const FETCH_RULES_SUCCESS = success(FETCH_RULES); -const SAVE_CAPTURE_RATE = 'watchdogs/SAVE_CAPTURE_RATE'; -const EDIT_CAPTURE_RATE = 'watchdogs/SAVE_CAPTURE_RATE'; - -const initialState = Map({ - activeTab: Map(), - instance: Watchdog(), - list: List(), - rules: List(), - captureRate: Map() -}); - -const reducer = (state = initialState, action = {}) => { - switch (action.type) { - case SET_ACTIVE_TAB: - return state.set('activeTab', action.instance); - case FETCH_RULES_SUCCESS: - return state.set('rules', action.data); - case FETCH_WATCHDOG_STATUS_SUCCESS: - case success(SAVE_CAPTURE_RATE): - return state.set('captureRate', Map(action.data)); - case request(SAVE_CAPTURE_RATE): - return state.mergeIn(['captureRate'], action.params); - case EDIT_CAPTURE_RATE: - return state.mergeIn(['captureRate'], {rate: action.rate}); - } - return state; -}; - - -export const fetchList = createFetchList(name); -export const init = createInit(name); -export const edit = createEdit(name); -export const save = createSave(name); -export const remove = createRemove(name); - -export function setActiveTab(instance) { - return { - type: SET_ACTIVE_TAB, - instance, - }; -} - -export const fetchRules = () => { - return { - types: array(FETCH_RULES), - call: client => client.get(`/watchdogs/rules`), - }; -} - -export default mergeReducers( - reducer, - createCRUDReducer(name, Watchdog, idKey), - createRequestReducer({ - fetchWatchdogStatus: FETCH_WATCHDOG_STATUS, - savingCaptureRate: SAVE_CAPTURE_RATE, - ...getCRUDRequestTypes(name), - }), -); - -export const saveCaptureRate = (params) => { - return { - params, - types: array(SAVE_CAPTURE_RATE), - call: client => client.post(`/sample_rate`, params), - } -} - -export const editCaptureRate = rate => { - return { - type: EDIT_CAPTURE_RATE, - rate - } -} - -export const fetchWatchdogStatus = () => { - return { - types: array(FETCH_WATCHDOG_STATUS), - call: client => client.get('/sample_rate'), - }; -} diff --git a/frontend/app/hooks/useCellMeasurerCache.ts b/frontend/app/hooks/useCellMeasurerCache.ts new file mode 100644 index 000000000..692f2629f --- /dev/null +++ b/frontend/app/hooks/useCellMeasurerCache.ts @@ -0,0 +1,12 @@ +import { useMemo } from 'react' +import { CellMeasurerCache, CellMeasurerCacheParams } from 'react-virtualized'; +import useLatestRef from 'App/hooks/useLatestRef' + +export default function useCellMeasurerCache(itemList?: any[], options?: CellMeasurerCacheParams) { + const filteredListRef = itemList ? useLatestRef(itemList) : undefined + return useMemo(() => new CellMeasurerCache({ + fixedWidth: true, + keyMapper: filteredListRef ? (index) => filteredListRef.current[index] : undefined, + ...options + }), []) +} \ No newline at end of file diff --git a/frontend/app/mstore/assistMultiviewStore.ts b/frontend/app/mstore/assistMultiviewStore.ts index e0895918f..379f852cb 100644 --- a/frontend/app/mstore/assistMultiviewStore.ts +++ b/frontend/app/mstore/assistMultiviewStore.ts @@ -95,7 +95,7 @@ export default class AssistMultiviewStore { const matchingSessions = data.sessions.filter( (s: Record) => ids.includes(s.sessionID) || ids.includes(s.sessionId) ); - const immutMatchingSessions = List(matchingSessions).map(Session); + const immutMatchingSessions = List(matchingSessions).map(s => new Session(s)); immutMatchingSessions.forEach((session: Record) => { this.addSession(session); this.fetchAgentTokenInfo(session.sessionId); diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index a4fdbe884..707fb175a 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -15,6 +15,7 @@ import { errorService, notesService, recordingsService, + configService, } from 'App/services'; import SettingsStore from './settingsStore'; import AuditStore from './auditStore'; @@ -25,6 +26,7 @@ import NotesStore from './notesStore'; import BugReportStore from './bugReportStore' import RecordingsStore from './recordingsStore' import AssistMultiviewStore from './assistMultiviewStore'; +import WeeklyReportStore from './weeklyReportConfigStore' export class RootStore { dashboardStore: DashboardStore; @@ -41,6 +43,7 @@ export class RootStore { bugReportStore: BugReportStore; recordingsStore: RecordingsStore; assistMultiviewStore: AssistMultiviewStore; + weeklyReportStore: WeeklyReportStore constructor() { this.dashboardStore = new DashboardStore(); @@ -57,6 +60,7 @@ export class RootStore { this.bugReportStore = new BugReportStore(); this.recordingsStore = new RecordingsStore(); this.assistMultiviewStore = new AssistMultiviewStore(); + this.weeklyReportStore = new WeeklyReportStore(); } initClient() { @@ -70,6 +74,7 @@ export class RootStore { errorService.initClient(client); notesService.initClient(client) recordingsService.initClient(client); + configService.initClient(client); } } diff --git a/frontend/app/mstore/types/session.ts b/frontend/app/mstore/types/session.ts index 12b031d8a..d0984593e 100644 --- a/frontend/app/mstore/types/session.ts +++ b/frontend/app/mstore/types/session.ts @@ -1,6 +1,6 @@ import { runInAction, makeAutoObservable, observable } from 'mobx' -import { List, Map } from 'immutable'; -import { DateTime, Duration } from 'luxon'; +import { Map } from 'immutable'; +import { Duration } from 'luxon'; const HASH_MOD = 1610612741; const HASH_P = 53; diff --git a/frontend/app/mstore/userStore.ts b/frontend/app/mstore/userStore.ts index 8e8913de6..b46f776a3 100644 --- a/frontend/app/mstore/userStore.ts +++ b/frontend/app/mstore/userStore.ts @@ -44,7 +44,7 @@ export default class UserStore { } initUser(user?: any ): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { if (user) { this.instance = new User().fromJson(user.toJson()); } else { @@ -54,7 +54,7 @@ export default class UserStore { }) } - updateKey(key: string, value: any) { + updateKey(key: keyof this, value: any) { this[key] = value if (key === 'searchQuery') { diff --git a/frontend/app/mstore/weeklyReportConfigStore.ts b/frontend/app/mstore/weeklyReportConfigStore.ts new file mode 100644 index 000000000..7b0eec8a6 --- /dev/null +++ b/frontend/app/mstore/weeklyReportConfigStore.ts @@ -0,0 +1,32 @@ +import { makeAutoObservable }from "mobx" +import { configService } from "App/services"; + +export default class weeklyReportConfigStore { + public weeklyReport = false + + constructor() { + makeAutoObservable(this) + } + + setReport(value: boolean) { + this.weeklyReport = value + } + + async fetchReport() { + try { + const { weeklyReport } = await configService.fetchWeeklyReport() + return this.setReport(weeklyReport) + } catch (e) { + console.error(e) + } + } + + async fetchEditReport(value: boolean) { + try { + const { weeklyReport } = await configService.editWeeklyReport({ weeklyReport: value }) + return this.setReport(weeklyReport) + } catch (e) { + console.error(e) + } + } +} \ No newline at end of file diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index 83b82d0b6..6158ae0f9 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -26,11 +26,12 @@ export default class WebPlayer extends Player { private targetMarker: TargetMarker constructor(protected wpState: Store, session: any, live: boolean) { + console.log(session.events, session.stackEvents, session.resources, session.errors) let initialLists = live ? {} : { - event: session.events.toJSON(), - stack: session.stackEvents.toJSON(), - resource: session.resources.toJSON(), // MBTODO: put ResourceTiming in file - exceptions: session.errors.toJSON().map(({ time, errorId, name }: any) => + event: session.events, + stack: session.stackEvents, + resource: session.resources, // MBTODO: put ResourceTiming in file + exceptions: session.errors.map(({ time, errorId, name }: any) => Log({ level: LogLevel.ERROR, value: name, diff --git a/frontend/app/services/BaseService.ts b/frontend/app/services/BaseService.ts index 2af1897f8..5cca28d4e 100644 --- a/frontend/app/services/BaseService.ts +++ b/frontend/app/services/BaseService.ts @@ -1,4 +1,5 @@ import APIClient from 'App/api_client'; + export default class BaseService { client: APIClient; diff --git a/frontend/app/services/ConfigService.ts b/frontend/app/services/ConfigService.ts new file mode 100644 index 000000000..6afda4858 --- /dev/null +++ b/frontend/app/services/ConfigService.ts @@ -0,0 +1,17 @@ +import BaseService from './BaseService'; + +export interface WeeklyReport { + weeklyReport: boolean +} + +export default class ConfigService extends BaseService { + async fetchWeeklyReport(): Promise { + return this.client.get('/config/weekly_report') + .then(r => r.json()).then(j => j.data) + } + + async editWeeklyReport(config: WeeklyReport): Promise { + return this.client.post('/config/weekly_report', config) + .then(r => r.json()).then(j => j.data) + } +} \ No newline at end of file diff --git a/frontend/app/services/index.ts b/frontend/app/services/index.ts index 2bcf5981e..5033c4ed3 100644 --- a/frontend/app/services/index.ts +++ b/frontend/app/services/index.ts @@ -7,6 +7,7 @@ import AuditService from './AuditService'; import ErrorService from "./ErrorService"; import NotesService from "./NotesService"; import RecordingsService from "./RecordingsService"; +import ConfigService from './ConfigService' export const dashboardService = new DashboardService(); export const metricService = new MetricService(); @@ -17,3 +18,4 @@ export const auditService = new AuditService(); export const errorService = new ErrorService(); export const notesService = new NotesService(); export const recordingsService = new RecordingsService(); +export const configService = new ConfigService(); \ No newline at end of file diff --git a/frontend/app/types/address.js b/frontend/app/types/address.js deleted file mode 100644 index 8d8e2ccf2..000000000 --- a/frontend/app/types/address.js +++ /dev/null @@ -1,20 +0,0 @@ -import Record from 'Types/Record'; - -export default Record({ - line1: '', - postal_code: '', - city: '', - state: '', - country: '', -}, { - methods: { - validate() { - return true; - }, - toData() { - const js = this.toJS(); - delete js.key; - return js; - }, - }, -}); diff --git a/frontend/app/types/appTest.js b/frontend/app/types/appTest.js deleted file mode 100644 index 248b759b7..000000000 --- a/frontend/app/types/appTest.js +++ /dev/null @@ -1,87 +0,0 @@ -import { Record, List, Set } from 'immutable'; -import { validateName } from 'App/validate'; -import { DateTime } from 'luxon'; -import Run from './run'; -import Step from './step'; - -class Test extends Record({ - testId: undefined, - name: 'Unnamed Test', - steps: List(), - stepsCount: undefined, - framework: 'selenium', - sessionId: undefined, - generated: false, - tags: Set(), - runHistory: List(), - editedAt: undefined, - seqId: undefined, - seqChange: false, - uptime: 0, -}) { - // ???TODO - // idKey = "testId" - - exists() { - return this.testId !== undefined; - } - - validate() { - if (this.steps.size === 0) return false; - - return validateName(this.name, { - empty: false, - admissibleChars: ':-', - }); - } - - isComplete() { - return this.stepsCount === this.steps.size; - } - - // not the best code - toData() { - const js = this - .update('steps', steps => steps.map(step => step.toData())) - .toJS(); - - if (js.seqChange) { - const { testId, seqId } = js; - return { testId, seqId }; - } - - delete js.stepsCount; - delete js.seqChange; - - return js; - } - // not the best code -} - -const fromJS = (test = {}) => { - if (test instanceof Test) return test; - - const stepsLength = test.steps && test.steps.length; // - const editedAt = test.editedAt ? DateTime.fromMillis(test.editedAt) : undefined; - - const lastRun = Run(test.lastRun); - const runHistory = List(test.runHistory) // TODO: GOOD ENDPOINTS - .map(run => { - if (typeof run === 'string') { - return run === lastRun.runId - ? lastRun - : Run({ runId: run }) - } - return Run(run); - }); - - return new Test({ ...test, editedAt, uptime: parseInt(test.passed / test.count * 100) || 0 }) - .set('stepsCount', typeof test.stepsCount === 'number' - ? test.stepsCount - : stepsLength) // - .set('runHistory', runHistory) - .set('steps', List(test.steps).map(Step)) - .set('tags', test.tags && Set(test.tags.map(t => t.toLowerCase()))); -}; - -export default fromJS; diff --git a/frontend/app/types/dashboard/index.js b/frontend/app/types/dashboard/index.js index c250d64d6..670f44a2c 100644 --- a/frontend/app/types/dashboard/index.js +++ b/frontend/app/types/dashboard/index.js @@ -97,14 +97,14 @@ export const WIDGET_LIST = [{ name: "Recent Frustrations", description: "List of recent sessions where users experienced some kind of frustrations, such as click rage.", thumb: 'recent_frustrations.png', - dataWrapper: list => List(list).map(Session), + dataWrapper: list => List(list).map(s => new Session(s)), }, { key: "sessionsFeedback", name: "Recent Negative Feedback", description: "List of recent sessions where users reported an issue or a bad experience.", thumb: 'negative_feedback.png', - dataWrapper: list => List(list).map(Session), + dataWrapper: list => List(list).map(s => new Session(s)), }, { key: "missingResources", diff --git a/frontend/app/types/errorInfo.js b/frontend/app/types/errorInfo.js index b3bb48e1d..7da9c3a9b 100644 --- a/frontend/app/types/errorInfo.js +++ b/frontend/app/types/errorInfo.js @@ -37,12 +37,12 @@ const ErrorInfo = Record({ chart30: [], tags: [], customTags: [], - lastHydratedSession: Session(), + lastHydratedSession: new Session(), disabled: false, }, { fromJS: ({ stack, lastHydratedSession, ...other }) => ({ ...other, - lastHydratedSession: Session(lastHydratedSession), + lastHydratedSession: new Session(lastHydratedSession), stack0InfoString: getStck0InfoString(stack || []), }) }); diff --git a/frontend/app/types/filter/savedFilter.js b/frontend/app/types/filter/savedFilter.js index f10b0686b..7b414430f 100644 --- a/frontend/app/types/filter/savedFilter.js +++ b/frontend/app/types/filter/savedFilter.js @@ -11,7 +11,6 @@ export default Record({ filter: Filter(), createdAt: undefined, count: 0, - watchdogs: List(), isPublic: false, }, { idKey: 'searchId', diff --git a/frontend/app/types/member.ts b/frontend/app/types/member.ts index d3914eac9..0a88d51ba 100644 --- a/frontend/app/types/member.ts +++ b/frontend/app/types/member.ts @@ -16,6 +16,20 @@ export interface IMember { invitationLink: string } +export interface IMemberApiRes { + userId: string + name: string + email: string + createdAt: string + admin: boolean + superAdmin: boolean + joined: boolean + expiredInvitation: boolean + roleId: string + roleName: string + invitationLink: string +} + export default Record({ id: undefined, name: '', @@ -42,9 +56,9 @@ export default Record({ return js; }, }, - fromJS: ({ createdAt, ...rest }) => ({ + fromJS: ({ createdAt, ...rest }: IMemberApiRes) => ({ ...rest, - createdAt: createdAt && DateTime.fromISO(createdAt || 0), + createdAt: createdAt && DateTime.fromISO(createdAt || '0'), id: rest.userId, }), }); diff --git a/frontend/app/types/session/activity.js b/frontend/app/types/session/activity.js deleted file mode 100644 index bb9be45fa..000000000 --- a/frontend/app/types/session/activity.js +++ /dev/null @@ -1,53 +0,0 @@ -import Record from 'Types/Record'; -import { DateTime } from 'luxon'; - -const ASSIGN = 'assign'; -const MESSAGE = 'message'; -const OPEN = 'open'; -const CLOSE = 'close'; - -export const TYPES = { ASSIGN, MESSAGE, OPEN, CLOSE }; - -const Activity = Record({ - id: undefined, - type: '', - author: '', - // thread_id: undefined, - createdAt: undefined, - // assigned_to: undefined, - // user_id: undefined, - message: '', - user: '' -}) - -// const Assign = Activity.extend({ -// type: ASSIGN, -// }) - -// const Message = Activity.extend({ -// type: MESSAGE, -// }) - -// const Open = Activity.extend({ -// type: OPEN, -// }) - -// const Close = Activity.extend({ -// type: CLOSE, -// }) - -// const Open = Activity.extend({ -// type: OPEN, -// }) - -export default function(activity = {}) { - // if (activity.type === ASSIGN) return Assign(activity); - // if (activity.type === MESSAGE) return Message(activity); - // if (activity.type === OPEN) return Open(activity); - // if (activity.type === CLOSE) return Close(activity); - return Activity({ - ...activity, - createdAt: activity.createdAt ? DateTime.fromMillis(activity.createdAt, {}).toUTC() : undefined, - }); -} - diff --git a/frontend/app/types/session/activity.ts b/frontend/app/types/session/activity.ts new file mode 100644 index 000000000..97e5e2a5c --- /dev/null +++ b/frontend/app/types/session/activity.ts @@ -0,0 +1,49 @@ +import { DateTime } from 'luxon'; + +const ASSIGN = 'assign'; +const MESSAGE = 'message'; +const OPEN = 'open'; +const CLOSE = 'close'; + +export const TYPES = { ASSIGN, MESSAGE, OPEN, CLOSE } as const; + + +type TypeKeys = keyof typeof TYPES +type TypeValues = typeof TYPES[TypeKeys] + + +export interface IActivity { + id: string; + type: TypeValues; + author: string; + createdAt: number; + message: string; + user: string; +} + +export default class Activity { + id: IActivity["id"]; + type: IActivity["type"]; + author: IActivity["author"]; + createdAt?: DateTime; + message: IActivity["message"]; + user: IActivity["user"]; + + constructor(activity?: IActivity) { + if (activity) { + Object.assign(this, { + ...activity, + createdAt: activity.createdAt ? DateTime.fromMillis(activity.createdAt, {}).toUTC() : undefined, + }) + } else { + Object.assign(this, { + id: undefined, + type: '', + author: '', + createdAt: undefined, + message: '', + user: '' + }) + } + } +} diff --git a/frontend/app/types/session/assignment.js b/frontend/app/types/session/assignment.js deleted file mode 100644 index 0c599c85c..000000000 --- a/frontend/app/types/session/assignment.js +++ /dev/null @@ -1,47 +0,0 @@ -import Record from 'Types/Record'; -import Activity from './activity'; -import { List } from 'immutable'; -import { DateTime } from 'luxon'; -import { validateName, notEmptyString } from 'App/validate'; - -export default Record({ - id: undefined, - title: '', - timestamp: undefined, - creatorId: undefined, - sessionId: undefined, - projectId: '', - siteId: undefined, - activities: List(), - closed: false, - assignee: '', - commentsCount: undefined, - issueType: '', - description: '', - iconUrl: '' -}, { - fromJS: (assignment) => ({ - ...assignment, - timestamp: assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : undefined, - activities: assignment.comments ? List(assignment.comments).map(activity => { - if (assignment.users) { - activity.user = assignment.users.filter(user => user.id === activity.author).first(); - } - return Activity(activity) - }) : List() - }), - methods: { - validate: function() { - return !!this.projectId && !!this.issueType && - notEmptyString(this.title) && notEmptyString(this.description) - }, - toCreate: function() { - return { - title: this.title, - description: this.description, - assignee: this.assignee, - issueType: this.issueType - } - } - } -}) diff --git a/frontend/app/types/session/assignment.ts b/frontend/app/types/session/assignment.ts new file mode 100644 index 000000000..a16361ee8 --- /dev/null +++ b/frontend/app/types/session/assignment.ts @@ -0,0 +1,77 @@ +import Activity, { IActivity } from './activity'; +import { DateTime } from 'luxon'; +import { notEmptyString } from 'App/validate'; + +interface IAssignment { + id: string; + title: string; + timestamp: number; + creatorId: string; + sessionId: string; + projectId: string; + siteId: string; + activities: []; + closed: boolean; + assignee: string; + commentsCount: number; + issueType: string; + description: string; + iconUrl: string; + createdAt?: string; + comments: IActivity[] + users: { id: string }[] +} + +export default class Assignment { + id: IAssignment["id"]; + title: IAssignment["title"] = ''; + timestamp: IAssignment["timestamp"]; + creatorId: IAssignment["creatorId"]; + sessionId: IAssignment["sessionId"]; + projectId: IAssignment["projectId"] = ''; + siteId: IAssignment["siteId"]; + activities: IAssignment["activities"]; + closed: IAssignment["closed"]; + assignee: IAssignment["assignee"] = ''; + commentsCount: IAssignment["commentsCount"]; + issueType: IAssignment["issueType"] = ''; + description: IAssignment["description"] = ''; + iconUrl: IAssignment["iconUrl"] = ''; + + constructor(assignment?: IAssignment) { + if (assignment) { + Object.assign(this, { + ...assignment, + timestamp: assignment.createdAt ? DateTime.fromISO(assignment.createdAt) : undefined, + activities: assignment.comments ? assignment.comments.map(activity => { + if (assignment.users) { + // @ts-ignore ??? + activity.user = assignment.users.filter(user => user.id === activity.author)[0]; + } + return new Activity(activity) + }) : [] + }) + } + } + + toJS() { + return this + } + + validate() { + return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description) + } + + get isValid() { + return !!this.projectId && !!this.issueType && notEmptyString(this.title) && notEmptyString(this.description) + } + + toCreate() { + return { + title: this.title, + description: this.description, + assignee: this.assignee, + issueType: this.issueType + } + } +} \ No newline at end of file diff --git a/frontend/app/types/session/author.js b/frontend/app/types/session/author.js deleted file mode 100644 index 12edc3c1e..000000000 --- a/frontend/app/types/session/author.js +++ /dev/null @@ -1,11 +0,0 @@ -import Record from 'Types/Record'; - -export default Record({ - id: undefined, - avatarUrls: undefined, - name: undefined, -}, { - fromJS: author => ({ - ...author, - }) -}) diff --git a/frontend/app/types/session/customField.js b/frontend/app/types/session/customField.js deleted file mode 100644 index 79f26b914..000000000 --- a/frontend/app/types/session/customField.js +++ /dev/null @@ -1,5 +0,0 @@ -import CustomField from 'Types/customField'; - -export default CustomField.extend({ - value: undefined, -}); \ No newline at end of file diff --git a/frontend/app/types/session/error.js b/frontend/app/types/session/error.js deleted file mode 100644 index 9b0fcb278..000000000 --- a/frontend/app/types/session/error.js +++ /dev/null @@ -1,32 +0,0 @@ -import Record from 'Types/Record'; - - -function getStck0InfoString(stack) { - const stack0 = stack[0]; - if (!stack0) return ""; - let s = stack0.function || ""; - if (stack0.url) { - s += ` (${stack0.url})`; - } - return s; -} - - -export default Record({ - sessionId: undefined, - messageId: undefined, - timestamp: undefined, - errorId: undefined, - projectId: undefined, - source: undefined, - name: undefined, - message: undefined, - time: undefined, - function: '?', -}, { - fromJS: ({ stack, ...rest }) => ({ - ...rest, - stack0InfoString: getStck0InfoString(stack || []), - function: (stack && stack[0] && stack[0].function) || '?', - }), -}); diff --git a/frontend/app/types/session/error.ts b/frontend/app/types/session/error.ts new file mode 100644 index 000000000..66d2db096 --- /dev/null +++ b/frontend/app/types/session/error.ts @@ -0,0 +1,48 @@ +import Record from 'Types/Record'; + +function getStck0InfoString(stack: Stack) { + const stack0 = stack[0]; + if (!stack0) return ""; + let s = stack0.function || ""; + if (stack0.url) { + s += ` (${stack0.url})`; + } + return s; +} + +type Stack = { function: string; url: string }[] + +export interface IError { + sessionId: string + messageId: string + timestamp: number + errorId: string + projectId: string + source: string + name: string + message: string + time: number + function: string + stack: Stack +} + +export default class Error { + sessionId: IError["sessionId"]; + messageId: IError["messageId"]; + timestamp: IError["timestamp"]; + errorId: IError["errorId"]; + projectId: IError["projectId"]; + source: IError["source"]; + name: IError["name"]; + message: IError["message"]; + time: IError["time"]; + function: IError["function"]; + + constructor({ stack, ...rest }: IError) { + Object.assign(this, { + ...rest, + stack0InfoString: getStck0InfoString(stack || []), + function: (stack && stack[0] && stack[0].function) || '?', + }) + } +} diff --git a/frontend/app/types/session/errorStack.js b/frontend/app/types/session/errorStack.js deleted file mode 100644 index b92610229..000000000 --- a/frontend/app/types/session/errorStack.js +++ /dev/null @@ -1,13 +0,0 @@ -import Record from 'Types/Record'; - -export default Record({ - // url: undefined, - absPath: undefined, - filename: undefined, - // args: [], - function: undefined, - lineNo: undefined, - colNo: undefined, - offset: 0, - context: undefined -}); diff --git a/frontend/app/types/session/errorStack.ts b/frontend/app/types/session/errorStack.ts new file mode 100644 index 000000000..59e63d16f --- /dev/null +++ b/frontend/app/types/session/errorStack.ts @@ -0,0 +1,26 @@ +interface IErrorStack { + absPath?: string, + filename?: string, + function?: string, + lineNo?: number, + colNo?: number, + offset?: number, + context?: string +} + +export default class ErrorStack { + absPath: IErrorStack["absPath"] + filename: IErrorStack["filename"] + function: IErrorStack["function"] + lineNo: IErrorStack["lineNo"] + colNo: IErrorStack["colNo"] + offset: IErrorStack["offset"] + context: IErrorStack["context"] + + constructor(es: IErrorStack) { + Object.assign(this, { + ...es, + offset: es.offset || 0, + }) + } +} \ No newline at end of file diff --git a/frontend/app/types/session/event.js b/frontend/app/types/session/event.js deleted file mode 100644 index 537de1724..000000000 --- a/frontend/app/types/session/event.js +++ /dev/null @@ -1,91 +0,0 @@ -import Record from 'Types/Record'; -import Target from 'Types/target'; - -const CONSOLE = 'CONSOLE'; -const CLICK = 'CLICK'; -const INPUT = 'INPUT'; -const LOCATION = 'LOCATION'; -const CUSTOM = 'CUSTOM'; -const CLICKRAGE = 'CLICKRAGE'; -const IOS_VIEW = 'VIEW'; -export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW}; - - -const Event = Record({ - time: 0, - label: '' -}, { - fromJS: event => ({ - ...event, - target: Target(event.target || { path: event.targetPath }), - }) -}) - -const Console = Event.extend({ - type: CONSOLE, - subtype: '', // level ??? - value: '', -},{ - name: 'Console' -}) - -const Click = Event.extend({ - type: CLICK, - targetContent: '', - target: Target(), - count: undefined -}, { - name: 'Click' -}); - -const Input = Event.extend({ - type: INPUT, - target: Target(), - value: '', -},{ - name: 'Input' -}); - -const View = Event.extend({ - type: IOS_VIEW, - name: '', -},{ - name: 'View' -}) - -const Location = Event.extend({ - type: LOCATION, - url: '', - host: '', - pageLoad: false, - fcpTime: undefined, - //fpTime: undefined, - loadTime: undefined, - domContentLoadedTime: undefined, - domBuildingTime: undefined, - speedIndex: undefined, - visuallyComplete: undefined, - timeToInteractive: undefined, - referrer: '', -}, { - fromJS: event => ({ - ...event, - //fpTime: event.firstPaintTime, - fcpTime: event.firstContentfulPaintTime || event.firstPaintTime, - }), - name: 'Location' -}); - -const TYPE_CONSTRUCTOR_MAP = { - [CONSOLE]: Console, - [CLICK]: Click, - [INPUT]: Input, - [LOCATION]: Location, - [CLICKRAGE]: Click, - [IOS_VIEW]: View, -} - -export default function(event = {}) { - return (TYPE_CONSTRUCTOR_MAP[event.type] || Event)(event); -} - diff --git a/frontend/app/types/session/event.ts b/frontend/app/types/session/event.ts new file mode 100644 index 000000000..575183b93 --- /dev/null +++ b/frontend/app/types/session/event.ts @@ -0,0 +1,162 @@ +import Record from 'Types/Record'; +import Target from 'Types/target'; + +const CONSOLE = 'CONSOLE'; +const CLICK = 'CLICK'; +const INPUT = 'INPUT'; +const LOCATION = 'LOCATION'; +const CUSTOM = 'CUSTOM'; +const CLICKRAGE = 'CLICKRAGE'; +const IOS_VIEW = 'VIEW'; +export const TYPES = { CONSOLE, CLICK, INPUT, LOCATION, CUSTOM, CLICKRAGE, IOS_VIEW}; + +interface IEvent { + time: number; + timestamp: number; + type: typeof CONSOLE | typeof CLICK | typeof INPUT | typeof LOCATION | typeof CUSTOM | typeof CLICKRAGE; + name: string; + key: number; + label: string; + targetPath: string; + target: { + path: string; + label: string; + } +} +interface ConsoleEvent extends IEvent { + subtype: string + value: string +} +interface ClickEvent extends IEvent { + targetContent: string; + count: number; +} +interface InputEvent extends IEvent { + value: string; +} + +interface LocationEvent extends IEvent { + url: string; + host: string; + pageLoad: boolean; + fcpTime: number; + loadTime: number; + domContentLoadedTime: number; + domBuildingTime: number; + speedIndex: number; + visuallyComplete: number; + timeToInteractive: number; + referrer: string; + firstContentfulPaintTime: number; + firstPaintTime: number; +} + +export type EventData = ConsoleEvent | ClickEvent | InputEvent | LocationEvent | IEvent; + +class Event { + key: IEvent["key"] + time: IEvent["time"]; + label: IEvent["label"]; + target: IEvent["target"]; + + + constructor(event: IEvent) { + Object.assign(this, { + time: event.time, + label: event.label, + key: event.key, + target: { + path: event.target?.path || event.targetPath, + label: event.target?.label + } + }) + } +} + +class Console extends Event { + readonly type = CONSOLE; + readonly name = 'Console' + subtype: string; + value: string; + + constructor(evt: ConsoleEvent) { + super(evt); + this.subtype = evt.subtype + this.value = evt.value + } +} + +class Click extends Event { + readonly type = CLICK; + readonly name = 'Click' + targetContent = ''; + count: number + + constructor(evt: ClickEvent) { + super(evt); + this.targetContent = evt.targetContent + this.count = evt.count + } +} + +class Input extends Event { + readonly type = INPUT; + readonly name = 'Input' + value = '' + + constructor(evt: InputEvent) { + super(evt); + this.value = evt.value + } +} + + +export class Location extends Event { + readonly name = 'Location'; + readonly type = LOCATION; + url: LocationEvent["url"] + host: LocationEvent["host"]; + pageLoad: LocationEvent["pageLoad"]; + fcpTime: LocationEvent["fcpTime"]; + loadTime: LocationEvent["loadTime"]; + domContentLoadedTime: LocationEvent["domContentLoadedTime"]; + domBuildingTime: LocationEvent["domBuildingTime"]; + speedIndex: LocationEvent["speedIndex"]; + visuallyComplete: LocationEvent["visuallyComplete"]; + timeToInteractive: LocationEvent["timeToInteractive"]; + referrer: LocationEvent["referrer"]; + + constructor(evt: LocationEvent) { + super(evt); + Object.assign(this, { + ...evt, + fcpTime: evt.firstContentfulPaintTime || evt.firstPaintTime + }); + } +} + +export type InjectedEvent = Console | Click | Input | Location; + +export default function(event: EventData) { + if (event.type && event.type === CONSOLE) { + return new Console(event as ConsoleEvent) + } + if (event.type && event.type === CLICK) { + return new Click(event as ClickEvent) + } + if (event.type && event.type === INPUT) { + return new Input(event as InputEvent) + } + if (event.type && event.type === LOCATION) { + return new Location(event as LocationEvent) + } + if (event.type && event.type === CLICKRAGE) { + return new Click(event as ClickEvent) + } + // not used right now? + // if (event.type === CUSTOM || !event.type) { + // return new Event(event) + // } + console.error(`Unknown event type: ${event.type}`) +} + diff --git a/frontend/app/types/session/issue.js b/frontend/app/types/session/issue.js deleted file mode 100644 index 308bf32be..000000000 --- a/frontend/app/types/session/issue.js +++ /dev/null @@ -1,46 +0,0 @@ -import Record from 'Types/Record'; -import { List } from 'immutable'; -import Watchdog from 'Types/watchdog' -export const issues_types = List([ - { 'type': 'all', 'visible': true, 'order': 0, 'name': 'All', 'icon': '' }, - { 'type': 'js_exception', 'visible': true, 'order': 1, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' }, - { 'type': 'bad_request', 'visible': true, 'order': 2, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, - { 'type': 'click_rage', 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' }, - { 'type': 'crash', 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' }, - // { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, - // { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' }, - // { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' }, - // { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, - // { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, - // { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' }, - // { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, - // { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } -]).map(Watchdog) - -export const issues_types_map = {} -issues_types.forEach(i => { - issues_types_map[i.type] = { type: i.type, visible: i.visible, order: i.order, name: i.name, } -}); - -export default Record({ - issueId: undefined, - name: '', - visible: true, - sessionId: undefined, - time: undefined, - seqIndex: undefined, - payload: {}, - projectId: undefined, - type: '', - contextString: '', - context: '', - icon: 'info' -}, { - idKey: 'issueId', - fromJS: ({ type, ...rest }) => ({ - ...rest, - type, - icon: issues_types_map[type]?.icon, - name: issues_types_map[type]?.name, - }), -}); diff --git a/frontend/app/types/session/issue.ts b/frontend/app/types/session/issue.ts new file mode 100644 index 000000000..68ab64001 --- /dev/null +++ b/frontend/app/types/session/issue.ts @@ -0,0 +1,85 @@ +import Record from 'Types/Record'; + +const types = { + ALL: 'all', + JS_EXCEPTION: 'js_exception', + BAD_REQUEST: 'bad_request', + CRASH: 'crash', + CLICK_RAGE: 'click_rage' +} as const + +type TypeKeys = keyof typeof types +type TypeValues = typeof types[TypeKeys] + +type IssueType = { + [issueTypeKey in TypeValues]: { type: issueTypeKey; visible: boolean; order: number; name: string; icon: string }; +}; + +export const issues_types = [ + { 'type': types.ALL, 'visible': true, 'order': 0, 'name': 'All', 'icon': '' }, + { 'type': types.JS_EXCEPTION, 'visible': true, 'order': 1, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' }, + { 'type': types.BAD_REQUEST, 'visible': true, 'order': 2, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, + { 'type': types.CLICK_RAGE, 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' }, + { 'type': types.CRASH, 'visible': true, 'order': 4, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' }, + // { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, + // { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' }, + // { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' }, + // { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, + // { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, + // { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' }, + // { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, + // { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } +] as const + +const issues_types_map = {} +issues_types.forEach((i) => { + Object.assign(issues_types_map, { + [i.type]: { + type: i.type, + visible: i.visible, + order: i.order, + name: i.name, + icon: i.icon, + } + }) +}); + +export interface IIssue { + issueId: string + name: string + visible: boolean + sessionId: string + time: number + payload: Record + projectId: string + type: TypeValues + contextString: string + context: string + icon: string + timestamp: number + startedAt: number +} + +export default class Issue { + issueId: IIssue["issueId"] + name: IIssue["name"] + visible: IIssue["visible"] + sessionId: IIssue["sessionId"] + time: IIssue["time"] + payload: IIssue["payload"] + projectId: IIssue["projectId"] + type: IIssue["type"] + contextString: IIssue["contextString"] + context: IIssue["context"] + icon: IIssue["icon"] + key: number + + constructor({ type, ...rest }: IIssue & { key: number }) { + Object.assign(this, { + ...rest, + type, + icon: issues_types_map[type]?.icon, + name: issues_types_map[type]?.name + }) + } +} \ No newline at end of file diff --git a/frontend/app/types/session/profile.js b/frontend/app/types/session/profile.js deleted file mode 100644 index 98b9bc345..000000000 --- a/frontend/app/types/session/profile.js +++ /dev/null @@ -1,20 +0,0 @@ -import { List } from 'immutable'; -import Record from 'Types/Record'; - -export default Record({ - name: '', - args: List(), - result: undefined, - time: undefined, - index: undefined, - duration: undefined, -}, { - fromJS: ({ start_time, end_time, args, ...profile }) => ({ - ...profile, - args: List(args), - time: Math.round(start_time), - duration: Math.round(end_time - start_time || 0), - }), -}); - - diff --git a/frontend/app/types/session/reduxAction.js b/frontend/app/types/session/reduxAction.js deleted file mode 100644 index e03ab9ce9..000000000 --- a/frontend/app/types/session/reduxAction.js +++ /dev/null @@ -1,12 +0,0 @@ -import Record from 'Types/Record'; - -export default Record({ - time: undefined, - index: undefined, - action: {}, - state: undefined, - diff: [], - duration: undefined, -}); - - diff --git a/frontend/app/types/session/resource.js b/frontend/app/types/session/resource.js deleted file mode 100644 index 3b0523f78..000000000 --- a/frontend/app/types/session/resource.js +++ /dev/null @@ -1,118 +0,0 @@ -import { List } from 'immutable'; -import Record from 'Types/Record'; -import { getResourceName } from 'App/utils'; - -const XHR = 'xhr'; -const FETCH = 'fetch'; -const JS = 'script'; -const CSS = 'css'; -const IMG = 'img'; -const MEDIA = 'media'; -const OTHER = 'other'; - - -// -// const IMG_EXTENTIONS = [ "png", "gif", "jpg", "jpeg", "svg" ]; -// const MEDIA_EXTENTIONS = [ 'mp4', 'mkv', 'ogg', 'webm', 'avi', 'mp3' ]; -// -// function getResourceType(type, initiator, url) { -// if (type === 'xmlhttprequest') return XHR; // bad? -// if (type !== undefined) return type; -// if (initiator === 'xmlhttprequest' || initiator === 'fetch') return XHR; -// if (initiator === 'img') return IMG; -// const pathnameSplit = new URL(url).pathname.split('.'); -// if (pathnameSplit.length > 1) { -// const extention = pathnameSplit.pop(); -// if (extention === 'css') return CSS; -// if (extention === 'js') return JS; -// if (IMG_EXTENTIONS.includes(extention)) return IMG -// if (MEDIA_EXTENTIONS.includes(extention)) return MEDIA; -// } -// return OTHER; -// } - -const TYPES_MAP = { - "stylesheet": CSS, -} - -function getResourceStatus(status, success) { - if (status != null) return String(status); - if (typeof success === 'boolean' || typeof success === 'number') { - return !!success - ? '2xx-3xx' - : '4xx-5xx'; - } - return '2xx-3xx'; -} - -function getResourceSuccess(success, status) { - if (success != null) { return !!success } - if (status != null) { return status < 400 } - return true -} - -export const TYPES = { - XHR, - FETCH, - JS, - CSS, - IMG, - MEDIA, - OTHER, -} - -const YELLOW_BOUND = 10; -const RED_BOUND = 80; - -export function isRed(r) { - return !r.success || r.score >= RED_BOUND; -} -export function isYellow(r) { - return r.score < RED_BOUND && r.score >= YELLOW_BOUND; -} - -export default Record({ - type: OTHER, - url: '', - name: '', - status: '2xx-3xx', - duration: 0, - index: undefined, - time: undefined, - ttfb: 0, - timewidth: 0, - success: true, - score: 0, - // initiator: "other", - // pagePath: "", - method: '', - request:'', - response: '', - headerSize: 0, - encodedBodySize: 0, - decodedBodySize: 0, - responseBodySize: 0, - timings: List(), -}, { - fromJS: ({ type, initiator, status, success, time, datetime, timestamp, timings, ...resource }) => ({ - ...resource, - type: TYPES_MAP[type] || type, - name: getResourceName(resource.url), - status: getResourceStatus(status, success), - success: getResourceSuccess(success, status), - time: typeof time === 'number' ? time : datetime || timestamp, - ttfb: timings && timings.ttfb, - timewidth: timings && timings.timewidth, - timings, - }), - name: 'Resource', - methods: { - isRed() { - return isRed(this); - }, - isYellow() { - return isYellow(this); - } - } -}); - diff --git a/frontend/app/types/session/resource.ts b/frontend/app/types/session/resource.ts new file mode 100644 index 000000000..170e0713d --- /dev/null +++ b/frontend/app/types/session/resource.ts @@ -0,0 +1,117 @@ +import Record from 'Types/Record'; +import { getResourceName } from 'App/utils'; + +const XHR = 'xhr' as const; +const FETCH = 'fetch' as const; +const JS = 'script' as const; +const CSS = 'css' as const; +const IMG = 'img' as const; +const MEDIA = 'media' as const; +const OTHER = 'other' as const; + +function getResourceStatus(status: number, success: boolean) { + if (status != null) return String(status); + if (typeof success === 'boolean' || typeof success === 'number') { + return !!success + ? '2xx-3xx' + : '4xx-5xx'; + } + return '2xx-3xx'; +} + +function getResourceSuccess(success: boolean, status: number) { + if (success != null) { + return !!success + } + if (status != null) { + return status < 400 + } + return true +} + +export const TYPES = { + XHR, + FETCH, + JS, + CSS, + IMG, + MEDIA, + OTHER, + "stylesheet": CSS, +} + +const YELLOW_BOUND = 10; +const RED_BOUND = 80; + +export function isRed(r: IResource) { + return !r.success || r.score >= RED_BOUND; +} + +interface IResource { + type: keyof typeof TYPES, + url: string, + name: string, + status: number, + duration: number, + index: number, + time: number, + ttfb: number, + timewidth: number, + success: boolean, + score: number, + method: string, + request: string, + response: string, + headerSize: number, + encodedBodySize: number, + decodedBodySize: number, + responseBodySize: number, + timings: Record + datetime: number + timestamp: number +} + +export default class Resource { + name = 'Resource' + type: IResource["type"] + status: string + success: IResource["success"] + time: IResource["time"] + ttfb: IResource["ttfb"] + url: IResource["url"] + duration: IResource["duration"] + index: IResource["index"] + timewidth: IResource["timewidth"] + score: IResource["score"] + method: IResource["method"] + request: IResource["request"] + response: IResource["response"] + headerSize: IResource["headerSize"] + encodedBodySize: IResource["encodedBodySize"] + decodedBodySize: IResource["decodedBodySize"] + responseBodySize: IResource["responseBodySize"] + timings: IResource["timings"] + + constructor({ status, success, time, datetime, timestamp, timings, ...resource }: IResource) { + + Object.assign(this, { + ...resource, + name: getResourceName(resource.url), + status: getResourceStatus(status, success), + success: getResourceSuccess(success, status), + time: typeof time === 'number' ? time : datetime || timestamp, + ttfb: timings && timings.ttfb, + timewidth: timings && timings.timewidth, + timings, + }) + } + + isRed() { + return !this.success || this.score >= RED_BOUND; + } + + isYellow() { + return this.score < RED_BOUND && this.score >= YELLOW_BOUND; + } +} + diff --git a/frontend/app/types/session/session.ts b/frontend/app/types/session/session.ts index 49d5f6962..d0bf42936 100644 --- a/frontend/app/types/session/session.ts +++ b/frontend/app/types/session/session.ts @@ -1,14 +1,14 @@ -import Record from 'Types/Record'; -import { List, Map } from 'immutable'; import { Duration } from 'luxon'; -import SessionEvent, { TYPES } from './event'; +import SessionEvent, { TYPES, EventData, InjectedEvent } from './event'; import StackEvent from './stackEvent'; import Resource from './resource'; -import SessionError from './error'; -import Issue from './issue'; +import SessionError, { IError } from './error'; +import Issue, { IIssue } from './issue'; +import { Note } from 'App/services/NotesService' const HASH_MOD = 1610612741; const HASH_P = 53; + function hashString(s: string): number { let mul = 1; let hash = 0; @@ -19,164 +19,257 @@ function hashString(s: string): number { return hash; } -export default Record( - { - sessionId: '', - pageTitle: '', - active: false, - siteId: '', - projectKey: '', - peerId: '', - live: false, - startedAt: 0, - duration: 0, - events: List(), - stackEvents: List(), - resources: List(), - missedResources: List(), - metadata: Map(), - favorite: false, - filterId: '', - messagesUrl: '', - domURL: [], - devtoolsURL: [], - mobsUrl: [], // @depricated - userBrowser: '', - userBrowserVersion: '?', - userCountry: '', - userDevice: '', - userDeviceType: '', - isMobile: false, - userOs: '', - userOsVersion: '', - userId: '', - userAnonymousId: '', - userUuid: undefined, - userDisplayName: '', - userNumericHash: 0, - viewed: false, - consoleLogCount: '?', - eventsCount: '?', - pagesCount: '?', - clickRage: undefined, - clickRageTime: undefined, - resourcesScore: 0, - consoleError: undefined, - resourceError: undefined, - returningLocation: undefined, - returningLocationTime: undefined, - errorsCount: 0, - watchdogs: [], - issueTypes: [], - issues: [], - userDeviceHeapSize: 0, - userDeviceMemorySize: 0, - errors: List(), - crashes: [], - socket: null, - isIOS: false, - revId: '', - userSessionsCount: 0, - agentIds: [], - isCallActive: false, - agentToken: '', - notes: [], - notesWithEvents: [], - fileKey: '', - }, - { - fromJS: ({ +export interface ISession { + sessionId: string, + pageTitle: string, + active: boolean, + siteId: string, + projectKey: string, + peerId: string, + live: boolean, + startedAt: number, + duration: number, + events: InjectedEvent[], + stackEvents: StackEvent[], + resources: Resource[], + missedResources: Resource[], + metadata: [], + favorite: boolean, + filterId?: string, + domURL: string[], + devtoolsURL: string[], + /** + * @deprecated + */ + mobsUrl: string[], + userBrowser: string, + userBrowserVersion: string, + userCountry: string, + userDevice: string, + userDeviceType: string, + isMobile: boolean, + userOs: string, + userOsVersion: string, + userId: string, + userAnonymousId: string, + userUuid: string, + userDisplayName: string, + userNumericHash: number, + viewed: boolean, + consoleLogCount: number, + eventsCount: number, + pagesCount: number, + errorsCount: number, + issueTypes: [], + issues: [], + referrer: string | null, + userDeviceHeapSize: number, + userDeviceMemorySize: number, + errors: SessionError[], + crashes?: [], + socket: string, + isIOS: boolean, + revId: string | null, + agentIds?: string[], + isCallActive?: boolean, + agentToken: string, + notes: Note[], + notesWithEvents: Array, + fileKey: string, + platform: string, + projectId: string, + startTs: number, + timestamp: number, + backendErrors: number, + consoleErrors: number, + sessionID?: string, + userID: string, + userUUID: string, + userEvents: any[], +} + +const emptyValues = { + startTs: 0, + timestamp: 0, + backendErrors: 0, + consoleErrors: 0, + sessionID: '', + projectId: '', + errors: [], + stackEvents: [], + issues: [], + sessionId: '', + domURL: [], + devtoolsURL: [], + mobsUrl: [], + notes: [], + metadata: {}, + startedAt: 0, +} + +export default class Session { + sessionId: ISession["sessionId"] + pageTitle: ISession["pageTitle"] + active: ISession["active"] + siteId: ISession["siteId"] + projectKey: ISession["projectKey"] + peerId: ISession["peerId"] + live: ISession["live"] + startedAt: ISession["startedAt"] + duration: ISession["duration"] + events: ISession["events"] + stackEvents: ISession["stackEvents"] + resources: ISession["resources"] + metadata: ISession["metadata"] + favorite: ISession["favorite"] + filterId?: ISession["filterId"] + domURL: ISession["domURL"] + devtoolsURL: ISession["devtoolsURL"] + /** + * @deprecated + */ + mobsUrl: ISession["mobsUrl"] + userBrowser: ISession["userBrowser"] + userBrowserVersion: ISession["userBrowserVersion"] + userCountry: ISession["userCountry"] + userDevice: ISession["userDevice"] + userDeviceType: ISession["userDeviceType"] + isMobile: ISession["isMobile"] + userOs: ISession["userOs"] + userOsVersion: ISession["userOsVersion"] + userId: ISession["userId"] + userAnonymousId: ISession["userAnonymousId"] + userUuid: ISession["userUuid"] + userDisplayName: ISession["userDisplayName"] + userNumericHash: ISession["userNumericHash"] + viewed: ISession["viewed"] + consoleLogCount: ISession["consoleLogCount"] + eventsCount: ISession["eventsCount"] + pagesCount: ISession["pagesCount"] + errorsCount: ISession["errorsCount"] + issueTypes: ISession["issueTypes"] + issues: ISession["issues"] + referrer: ISession["referrer"] + userDeviceHeapSize: ISession["userDeviceHeapSize"] + userDeviceMemorySize: ISession["userDeviceMemorySize"] + errors: ISession["errors"] + crashes?: ISession["crashes"] + socket: ISession["socket"] + isIOS: ISession["isIOS"] + revId: ISession["revId"] + agentIds?: ISession["agentIds"] + isCallActive?: ISession["isCallActive"] + agentToken: ISession["agentToken"] + notes: ISession["notes"] + notesWithEvents: ISession["notesWithEvents"] + fileKey: ISession["fileKey"] + + constructor(plainSession?: ISession) { + const sessionData = plainSession || (emptyValues as unknown as ISession) + const { startTs = 0, timestamp = 0, backendErrors = 0, consoleErrors = 0, - projectId, - errors, + sessionID = '', + projectId = '', + errors = [], stackEvents = [], issues = [], - sessionId, - sessionID, + sessionId = '', domURL = [], devtoolsURL = [], mobsUrl = [], notes = [], + resources = [], ...session - }) => { - const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration); - const durationSeconds = duration.valueOf(); - const startedAt = +startTs || +timestamp; + } = sessionData + const duration = Duration.fromMillis(session.duration < 1000 ? 1000 : session.duration); + const durationSeconds = duration.valueOf(); + const startedAt = +startTs || +timestamp; - const userDevice = session.userDevice || session.userDeviceType || 'Other'; - const userDeviceType = session.userDeviceType || 'other'; - const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType); + const userDevice = session.userDevice || session.userDeviceType || 'Other'; + const userDeviceType = session.userDeviceType || 'other'; + const isMobile = ['console', 'mobile', 'tablet'].includes(userDeviceType); - const events = List(session.events) - .map((e) => SessionEvent({ ...e, time: e.timestamp - startedAt })) - .filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds); + const events: InjectedEvent[] = [] + const rawEvents: (EventData & { key: number })[] = [] - let resources = List(session.resources).map(Resource); - resources = resources - .map((r) => r.set('time', Math.max(0, r.time - startedAt))) - .sort((r1, r2) => r1.time - r2.time); - const missedResources = resources.filter(({ success }) => !success); + if (session.events?.length) { + (session.events as EventData[]).forEach((event: EventData, k) => { + const time = event.timestamp - startedAt + if (event.type !== TYPES.CONSOLE && time <= durationSeconds) { + const EventClass = SessionEvent({ ...event, time, key: k }) + if (EventClass) { + events.push(EventClass); + } + rawEvents.push({ ...event, time, key: k }); + } + }) + } - const stackEventsList = List(stackEvents) - .concat(List(session.userEvents)) - .sortBy((se) => se.timestamp) - .map((se) => StackEvent({ ...se, time: se.timestamp - startedAt })); - const exceptions = List(errors).map(SessionError); + let resourcesList = resources.map((r) => new Resource(r as any)); + resourcesList.forEach((r: Resource) => { + r.time = Math.max(0, r.time - startedAt) + }) + resourcesList = resourcesList.sort((r1, r2) => r1.time - r2.time); + const missedResources = resourcesList.filter(({ success }) => !success); - const issuesList = List(issues).map((e) => Issue({ ...e, time: e.timestamp - startedAt })); + const stackEventsList: StackEvent[] = [] + if (stackEvents?.length || session.userEvents?.length) { + const mergedArrays = [...stackEvents, ...session.userEvents] + .sort((a, b) => a.timestamp - b.timestamp) + .map((se) => new StackEvent({ ...se, time: se.timestamp - startedAt })) + stackEventsList.push(...mergedArrays); + } - const rawEvents = !session.events - ? [] - : // @ts-ignore - session.events - .map((evt) => ({ ...evt, time: evt.timestamp - startedAt })) - .filter(({ type, time }) => type !== TYPES.CONSOLE && time <= durationSeconds) || []; - const rawNotes = notes; - const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => { - const aTs = a.time || a.timestamp; - const bTs = b.time || b.timestamp; + const exceptions = (errors as IError[]).map(e => new SessionError(e)) || []; - return aTs - bTs; - }); + const issuesList = (issues as IIssue[]).map( + (i, k) => new Issue({ ...i, time: i.timestamp - startedAt, key: k })) || []; - return { - ...session, - isIOS: session.platform === 'ios', - watchdogs: session.watchdogs || [], - errors: exceptions, - siteId: projectId, - events, - stackEvents: stackEventsList, - resources, - missedResources, - userDevice, - userDeviceType, - isMobile, - startedAt, - duration, - userNumericHash: hashString( - session.userId || - session.userAnonymousId || - session.userUuid || - session.userID || - session.userUUID || - '' - ), - userDisplayName: - session.userId || session.userAnonymousId || session.userID || 'Anonymous User', - issues: issuesList, - sessionId: sessionId || sessionID, - userId: session.userId || session.userID, - mobsUrl: Array.isArray(mobsUrl) ? mobsUrl : [mobsUrl], - domURL, - devtoolsURL, - notes, - notesWithEvents: List(notesWithEvents), - }; - }, - idKey: 'sessionId', + const rawNotes = notes; + const notesWithEvents = [...rawEvents, ...rawNotes].sort((a, b) => { + // @ts-ignore just in case + const aTs = a.timestamp || a.time; + // @ts-ignore + const bTs = b.timestamp || b.time; + + return aTs - bTs; + }) || []; + + Object.assign(this, { + ...session, + isIOS: session.platform === 'ios', + errors: exceptions, + siteId: projectId, + events, + stackEvents: stackEventsList, + resources: resourcesList, + missedResources, + userDevice, + userDeviceType, + isMobile, + startedAt, + duration, + userNumericHash: hashString( + session.userId || + session.userAnonymousId || + session.userUuid || + session.userID || + session.userUUID || + '' + ), + userDisplayName: + session.userId || session.userAnonymousId || session.userID || 'Anonymous User', + issues: issuesList, + sessionId: sessionId || sessionID, + userId: session.userId || session.userID, + mobsUrl: Array.isArray(mobsUrl) ? mobsUrl : [mobsUrl], + domURL, + devtoolsURL, + notes, + notesWithEvents: notesWithEvents, + }) } -); +} \ No newline at end of file diff --git a/frontend/app/types/session/stackEvent.js b/frontend/app/types/session/stackEvent.js deleted file mode 100644 index 762407916..000000000 --- a/frontend/app/types/session/stackEvent.js +++ /dev/null @@ -1,59 +0,0 @@ -import Record from 'Types/Record'; - -export const OPENREPLAY = 'openreplay'; -export const SENTRY = 'sentry'; -export const DATADOG = 'datadog'; -export const STACKDRIVER = 'stackdriver'; -export const ROLLBAR = 'rollbar'; -export const NEWRELIC = 'newrelic'; -export const BUGSNAG = 'bugsnag'; -export const CLOUDWATCH = 'cloudwatch'; -export const ELASTICSEARCH = 'elasticsearch'; -export const SUMOLOGIC = 'sumologic'; - -export const typeList = [ OPENREPLAY, SENTRY, DATADOG, STACKDRIVER, ROLLBAR, BUGSNAG, CLOUDWATCH, ELASTICSEARCH, SUMOLOGIC ]; - -export function isRed(event) { - if (!event.payload) return false; - switch(event.source) { - case SENTRY: - return event.payload['event.type'] === 'error'; - case DATADOG: - return true; - case STACKDRIVER: - return false; - case ROLLBAR: - return true; - case NEWRELIC: - return true; - case BUGSNAG: - return true; - case CLOUDWATCH: - return true; - case SUMOLOGIC: - return false; - default: - return event.level==='error'; - } -} - -export default Record({ - time: undefined, - index: undefined, - name: '', - message: "", - payload: null, - source: null, - level: "", -}, { - fromJS: ue => ({ - ...ue, - source: ue.source || OPENREPLAY, - }), - methods: { - isRed() { - return isRed(this); - } - } -}); - diff --git a/frontend/app/types/session/stackEvent.ts b/frontend/app/types/session/stackEvent.ts new file mode 100644 index 000000000..06910e7d4 --- /dev/null +++ b/frontend/app/types/session/stackEvent.ts @@ -0,0 +1,70 @@ +export const OPENREPLAY = 'openreplay'; +export const SENTRY = 'sentry'; +export const DATADOG = 'datadog'; +export const STACKDRIVER = 'stackdriver'; +export const ROLLBAR = 'rollbar'; +export const NEWRELIC = 'newrelic'; +export const BUGSNAG = 'bugsnag'; +export const CLOUDWATCH = 'cloudwatch'; +export const ELASTICSEARCH = 'elasticsearch'; +export const SUMOLOGIC = 'sumologic'; + +export const typeList = [OPENREPLAY, SENTRY, DATADOG, STACKDRIVER, ROLLBAR, BUGSNAG, CLOUDWATCH, ELASTICSEARCH, SUMOLOGIC]; + +export function isRed(event: StackEvent) { + if (!event.payload) return false; + switch (event.source) { + case SENTRY: + return event.payload['event.type'] === 'error'; + case DATADOG: + return true; + case STACKDRIVER: + return false; + case ROLLBAR: + return true; + case NEWRELIC: + return true; + case BUGSNAG: + return true; + case CLOUDWATCH: + return true; + case SUMOLOGIC: + return false; + default: + return event.level === 'error'; + } +} + +export interface IStackEvent { + time: number; + timestamp: number; + index: number; + name: string; + message: string; + payload: any; + source: any; + level: string; + + isRed: () => boolean; +} + +export default class StackEvent { + time: IStackEvent["time"] + index: IStackEvent["index"]; + name: IStackEvent["name"]; + message: IStackEvent["message"]; + payload: IStackEvent["payload"]; + source: IStackEvent["source"]; + level: IStackEvent["level"]; + + constructor(evt: IStackEvent) { + Object.assign(this, { + ...evt, + source: evt.source || OPENREPLAY + }); + } + + isRed() { + return isRed(this); + } +} diff --git a/frontend/app/types/ts/search.ts b/frontend/app/types/ts/search.ts index 1c45bbe56..32660818b 100644 --- a/frontend/app/types/ts/search.ts +++ b/frontend/app/types/ts/search.ts @@ -10,5 +10,4 @@ export interface SavedSearch { projectId: number; searchId: number; userId: number; - watchdogs: List }