diff --git a/frontend/app/Router.js b/frontend/app/Router.js
index f5dc4c593..acbdd9cbb 100644
--- a/frontend/app/Router.js
+++ b/frontend/app/Router.js
@@ -31,7 +31,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/BugFinder/BugFinder'));
+const BugFinderPure = 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'));
diff --git a/frontend/app/components/Overview/Overview.tsx b/frontend/app/components/Overview/Overview.tsx
new file mode 100644
index 000000000..78b4bfe2b
--- /dev/null
+++ b/frontend/app/components/Overview/Overview.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import withPageTitle from 'HOCs/withPageTitle';
+import NoSessionsMessage from 'Shared/NoSessionsMessage';
+import MainSearchBar from 'Shared/MainSearchBar';
+import SessionSearch from 'Shared/SessionSearch';
+import SessionListContainer from 'Shared/SessionListContainer/SessionListContainer';
+
+function Overview() {
+ return (
+
+ );
+}
+
+export default withPageTitle('Sessions - OpenReplay')(Overview);
diff --git a/frontend/app/components/Overview/index.ts b/frontend/app/components/Overview/index.ts
new file mode 100644
index 000000000..44bcc2216
--- /dev/null
+++ b/frontend/app/components/Overview/index.ts
@@ -0,0 +1 @@
+export { default } from './Overview';
\ No newline at end of file
diff --git a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
index f4bb1f45d..fae2e40cf 100644
--- a/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
+++ b/frontend/app/components/shared/LiveSessionList/LiveSessionList.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { Fragment, useEffect } from 'react';
import { connect } from 'react-redux';
import { NoContent, Loader, Pagination } from 'UI';
import { List } from 'immutable';
diff --git a/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx
new file mode 100644
index 000000000..156f845c4
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/SessionListContainer.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import SessionList from './components/SessionList';
+import SessionHeader from './components/SessionHeader';
+
+interface Props {}
+function SessionListContainer(props: Props) {
+ return (
+
+ );
+}
+
+export default SessionListContainer;
diff --git a/frontend/app/components/shared/SessionListContainer/components/NoContentMessage.tsx b/frontend/app/components/shared/SessionListContainer/components/NoContentMessage.tsx
new file mode 100644
index 000000000..c80ec6555
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/NoContentMessage.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { connect } from 'react-redux';
+
+function NoContentMessage({ activeTab }: any) {
+ return {getNoContentMessage(activeTab)}
;
+}
+
+export default connect((state: any) => ({
+ activeTab: state.getIn(['search', 'activeTab']),
+}))(NoContentMessage);
+
+function getNoContentMessage(activeTab: any) {
+ let str = 'No recordings found';
+ if (activeTab.type !== 'all') {
+ str += ' with ' + activeTab.name;
+ return str;
+ }
+
+ return str + '!';
+}
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionHeader/SessionHeader.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionHeader/SessionHeader.tsx
new file mode 100644
index 000000000..5c5107310
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionHeader/SessionHeader.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { numberWithCommas } from 'App/utils';
+import { applyFilter } from 'Duck/search';
+import Period from 'Types/app/period';
+import SelectDateRange from 'Shared/SelectDateRange';
+import SessionTags from '../SessionTags';
+import { connect } from 'react-redux';
+import SessionSort from '../SessionSort';
+
+interface Props {
+ listCount: number;
+ filter: any;
+ applyFilter: (filter: any) => void;
+}
+function SessionHeader(props: Props) {
+ const { listCount, filter: { startDate, endDate, rangeValue } } = props;
+ const period = Period({ start: startDate, end: endDate, rangeName: rangeValue });
+
+ const onDateChange = (e: any) => {
+ const dateValues = e.toJSON();
+ props.applyFilter(dateValues);
+ };
+
+ return (
+
+
+
+ Sessions {listCount}
+
+
+
+
+
+
+ );
+}
+
+export default connect(
+ (state: any) => ({
+ filter: state.getIn(['search', 'instance']),
+ listCount: numberWithCommas(state.getIn(['sessions', 'total'])),
+ }),
+ { applyFilter }
+)(SessionHeader);
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionHeader/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionHeader/index.ts
new file mode 100644
index 000000000..ad3beb4fd
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionHeader/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionHeader';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx
new file mode 100644
index 000000000..60d1aec6c
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionList/SessionList.tsx
@@ -0,0 +1,103 @@
+import React, { useEffect } from 'react';
+import { connect } from 'react-redux';
+import { FilterKey } from 'Types/filter/filterType';
+import SessionItem from 'Shared/SessionItem';
+import { NoContent, Loader, Pagination } from 'UI';
+import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
+import NoContentMessage from '../NoContentMessage';
+import { fetchSessions, addFilterByKeyAndValue, updateCurrentPage, setScrollPosition } from 'Duck/search';
+
+interface Props {
+ loading: boolean;
+ list: any;
+ currentPage: number;
+ total: number;
+ filters: any;
+ lastPlayedSessionId: string;
+ metaList: any;
+ scrollY: number;
+ addFilterByKeyAndValue: (key: string, value: any, operator?: string) => void;
+ updateCurrentPage: (page: number) => void;
+ setScrollPosition: (scrollPosition: number) => void;
+ fetchSessions: () => void;
+}
+function SessionList(props: Props) {
+ const { loading, list, currentPage, total, filters, lastPlayedSessionId, metaList } = props;
+ const _filterKeys = filters.map((i: any) => i.key);
+ const hasUserFilter = _filterKeys.includes(FilterKey.USERID) || _filterKeys.includes(FilterKey.USERANONYMOUSID);
+
+ useEffect(() => {
+ const { scrollY } = props;
+ window.scrollTo(0, scrollY);
+ if (total === 0) {
+ props.fetchSessions()
+ }
+
+ return () => {
+ props.setScrollPosition(window.scrollY);
+ };
+ }, []);
+
+ const onUserClick = (userId: any) => {
+ if (userId) {
+ props.addFilterByKeyAndValue(FilterKey.USERID, userId);
+ } else {
+ props.addFilterByKeyAndValue(FilterKey.USERID, '', 'isUndefined');
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ }
+ subtext={Please try changing your search parameters.
}
+ show={!loading && list.size === 0}
+ >
+ {list.map((session: any) => (
+
+
+
+
+ ))}
+
+
+ {total > 0 && (
+
+
props.updateCurrentPage(page)}
+ limit={10}
+ debounceRequest={1000}
+ />
+
+ )}
+
+ );
+}
+
+export default connect(
+ (state: any) => ({
+ list: state.getIn(['sessions', 'list']),
+ filters: state.getIn(['search', 'instance', 'filters']),
+ lastPlayedSessionId: state.getIn(['sessions', 'lastPlayedSessionId']),
+ metaList: state.getIn(['customFields', 'list']).map((i: any) => i.key),
+ loading: state.getIn(['sessions', 'loading']),
+ currentPage: state.getIn(['search', 'currentPage']) || 1,
+ total: state.getIn(['sessions', 'total']) || 0,
+ scrollY: state.getIn(['search', 'scrollY']),
+ }),
+ { updateCurrentPage, addFilterByKeyAndValue, setScrollPosition, fetchSessions }
+)(SessionList);
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionList/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionList/index.ts
new file mode 100644
index 000000000..779c9df2a
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionList/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionList';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/SessionSort.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionSort/SessionSort.tsx
new file mode 100644
index 000000000..01e770310
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionSort/SessionSort.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import Select from 'Shared/Select';
+import { sort } from 'Duck/sessions';
+import { applyFilter } from 'Duck/search';
+
+const sortOptionsMap = {
+ 'startTs-desc': 'Newest',
+ 'startTs-asc': 'Oldest',
+ 'eventsCount-asc': 'Events Ascending',
+ 'eventsCount-desc': 'Events Descending',
+};
+
+const sortOptions = Object.entries(sortOptionsMap).map(([value, label]) => ({ value, label }));
+
+interface Props {
+ filter: any;
+ options: any;
+ applyFilter: (filter: any) => void;
+ sort: (sort: string, sign: number) => void;
+}
+
+function SessionSort(props: Props) {
+ const { sort, order } = props.filter;
+ const onSort = ({ value }: any) => {
+ value = value.value;
+ const [sort, order] = value.split('-');
+ const sign = order === 'desc' ? -1 : 1;
+ props.applyFilter({ order, sort });
+ props.sort(sort, sign);
+ };
+
+ const defaultOption = `${sort}-${order}`;
+ return ;
+}
+
+export default connect(
+ (state: any) => ({
+ filter: state.getIn(['search', 'instance']),
+ }),
+ { sort, applyFilter }
+)(SessionSort);
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts
new file mode 100644
index 000000000..b0c0489be
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionSort/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionSort';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css b/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css
new file mode 100644
index 000000000..87e26bc68
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionSort/sortDropdown.module.css
@@ -0,0 +1,23 @@
+.dropdown {
+ display: flex !important;
+ padding: 4px 6px;
+ border-radius: 3px;
+ color: $gray-darkest;
+ font-weight: 500;
+ &:hover {
+ background-color: $gray-light;
+ }
+}
+
+.dropdownTrigger {
+ padding: 4px 8px;
+ border-radius: 3px;
+ &:hover {
+ background-color: $gray-light;
+ }
+}
+
+.dropdownIcon {
+ margin-top: 2px;
+ margin-left: 3px;
+}
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx b/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx
new file mode 100644
index 000000000..67c1e5e83
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionTags/SessionTags.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { setActiveTab } from 'Duck/search';
+import { connect } from 'react-redux';
+import { issues_types } from 'Types/session/issue';
+import { Icon } from 'UI';
+import cn from 'classnames';
+
+interface Props {
+ setActiveTab: typeof setActiveTab;
+ activeTab: any;
+ tags: any;
+ total: number;
+}
+function SessionTags(props: Props) {
+ const { activeTab, tags, total } = props;
+ const disable = activeTab.type === 'all' && total === 0;
+
+ return (
+
+ {tags &&
+ tags.map((tag: any, index: any) => (
+
+ props.setActiveTab(tag)}
+ label={tag.name}
+ isActive={activeTab.type === tag.type}
+ icon={tag.icon}
+ disabled={disable && tag.type !== 'all'}
+ />
+
+ ))}
+
+ );
+}
+
+export default connect(
+ (state: any) => {
+ const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
+ return {
+ activeTab: state.getIn(['search', 'activeTab']),
+ tags: issues_types.filter((tag: any) => (isEnterprise ? tag.type !== 'bookmark' : tag.type !== 'vault')),
+ total: state.getIn(['sessions', 'total']) || 0,
+ };
+ },
+ {
+ setActiveTab,
+ }
+)(SessionTags);
+
+function TagItem({ isActive, onClick, label, icon = '', disabled = false }: any) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts b/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts
new file mode 100644
index 000000000..4f5e62f6c
--- /dev/null
+++ b/frontend/app/components/shared/SessionListContainer/components/SessionTags/index.ts
@@ -0,0 +1 @@
+export { default } from './SessionTags';
\ No newline at end of file
diff --git a/frontend/app/components/shared/SessionListContainer/index.ts b/frontend/app/components/shared/SessionListContainer/index.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/app/components/ui/NoContent/NoContent.js b/frontend/app/components/ui/NoContent/NoContent.js
deleted file mode 100644
index a82b8b99b..000000000
--- a/frontend/app/components/ui/NoContent/NoContent.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-import { Icon } from 'UI';
-import styles from './noContent.module.css';
-
-export default ({
- title = No data available.
,
- subtext,
- icon,
- iconSize = 100,
- size,
- show = true,
- children = null,
- empty = false,
- image = null,
- style = {},
-}) => (!show ? children :
-
- {
- icon &&
- }
- { title &&
{ title }
}
- {
- subtext &&
-
{ subtext }
- }
- {
- image &&
{ image }
- }
-
-);
diff --git a/frontend/app/components/ui/NoContent/NoContent.tsx b/frontend/app/components/ui/NoContent/NoContent.tsx
new file mode 100644
index 000000000..10a7e72e7
--- /dev/null
+++ b/frontend/app/components/ui/NoContent/NoContent.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Icon } from 'UI';
+import styles from './noContent.module.css';
+
+interface Props {
+ title?: any;
+ subtext?: any;
+ icon?: string;
+ iconSize?: number;
+ size?: number;
+ show?: boolean;
+ children?: any;
+ image?: any;
+ style?: any;
+}
+export default function NoContent(props: Props) {
+ const { title = '', subtext = '', icon, iconSize, size, show, children, image, style } = props;
+
+ return !show ? (
+ children
+ ) : (
+
+ {icon &&
}
+ {title &&
{title}
}
+ {subtext &&
{subtext}
}
+ {image &&
{image}
}
+
+ );
+}
diff --git a/frontend/app/components/ui/NoContent/index.js b/frontend/app/components/ui/NoContent/index.ts
similarity index 100%
rename from frontend/app/components/ui/NoContent/index.js
rename to frontend/app/components/ui/NoContent/index.ts
diff --git a/frontend/app/components/ui/NoContent/noContent.module.css b/frontend/app/components/ui/NoContent/noContent.module.css
index 5cf7a0d24..33c63c916 100644
--- a/frontend/app/components/ui/NoContent/noContent.module.css
+++ b/frontend/app/components/ui/NoContent/noContent.module.css
@@ -45,15 +45,3 @@
height: 166px;
margin-bottom: 20px;
}
-
-.empty-state {
- display: block;
- margin: auto;
- background-image: svg-load(empty-state.svg, fill=#CCC);
- background-repeat: no-repeat;
- background-size: contain;
- background-position: center center;
- width: 166px;
- height: 166px;
- margin-bottom: 20px;
-}
diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js
index c49b00b26..fde7a9623 100644
--- a/frontend/app/duck/search.js
+++ b/frontend/app/duck/search.js
@@ -137,13 +137,13 @@ export const reduceThenFetchResource =
const filter = getState().getIn(['search', 'instance']).toData();
const activeTab = getState().getIn(['search', 'activeTab']);
- if (activeTab.type !== 'all' && activeTab.type !== 'bookmark') {
+ if (activeTab.type !== 'all' && activeTab.type !== 'bookmark' && activeTab.type !== 'vault') {
const tmpFilter = filtersMap[FilterKey.ISSUE];
tmpFilter.value = [activeTab.type];
filter.filters = filter.filters.concat(tmpFilter);
}
- if (activeTab.type === 'bookmark') {
+ if (activeTab.type === 'bookmark' || activeTab.type === 'vault') {
filter.bookmarked = true;
}
diff --git a/frontend/app/styles/global.scss b/frontend/app/styles/global.scss
index 040831c9e..014cfce5f 100644
--- a/frontend/app/styles/global.scss
+++ b/frontend/app/styles/global.scss
@@ -11,4 +11,8 @@
input.no-focus:focus {
outline: none !important;
border: solid thin transparent !important;
+}
+
+.widget-wrapper {
+ @apply rounded border bg-white;
}
\ No newline at end of file
diff --git a/frontend/app/types/session/issue.js b/frontend/app/types/session/issue.js
index 31214ae3e..db220f8cc 100644
--- a/frontend/app/types/session/issue.js
+++ b/frontend/app/types/session/issue.js
@@ -3,15 +3,18 @@ import { List } from 'immutable';
import Watchdog from 'Types/watchdog'
export const issues_types = List([
- { 'type': 'js_exception', 'visible': true, 'order': 0, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' },
- { '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': 'click_rage', 'visible': true, 'order': 3, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
- { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' },
- { 'type': 'memory', 'visible': true, 'order': 5, 'name': 'High Memory', 'icon': 'funnel/sd-card' },
- { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' },
- { 'type': 'crash', 'visible': true, 'order': 7, 'name': 'Crashes', 'icon': 'funnel/file-earmark-break' },
- { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' }
+ { 'type': 'all', 'visible': true, 'order': 0, 'name': 'All', 'icon': '' },
+ { 'type': 'js_exception', 'visible': true, 'order': 1, 'name': 'Errors', 'icon': 'funnel/exclamation-circle' },
+ { 'type': 'click_rage', 'visible': true, 'order': 2, 'name': 'Click Rage', 'icon': 'funnel/emoji-angry' },
+ { 'type': 'crash', 'visible': true, 'order': 3, '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 = {}
diff --git a/frontend/env.js b/frontend/env.js
deleted file mode 100644
index e8332bd82..000000000
--- a/frontend/env.js
+++ /dev/null
@@ -1,29 +0,0 @@
-require('dotenv').config()
-
-// TODO: (the problem is during the build time the frontend is isolated)
-//const trackerInfo = require('../tracker/tracker/package.json');
-
-const oss = {
- name: 'oss',
- PRODUCTION: true,
- SENTRY_ENABLED: false,
- SENTRY_URL: "",
- CAPTCHA_ENABLED: process.env.CAPTCHA_ENABLED === 'true',
- CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY,
- ORIGIN: () => 'window.location.origin',
- API_EDP: () => 'window.location.origin + "/api"',
- ASSETS_HOST: () => 'window.location.origin + "/assets"',
- VERSION: '1.7.0',
- SOURCEMAP: true,
- MINIO_ENDPOINT: process.env.MINIO_ENDPOINT,
- MINIO_PORT: process.env.MINIO_PORT,
- MINIO_USE_SSL: process.env.MINIO_USE_SSL,
- MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
- MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
- ICE_SERVERS: process.env.ICE_SERVERS,
- TRACKER_VERSION: '3.5.15' // trackerInfo.version,
-}
-
-module.exports = {
- oss,
-};