Merge pull request #441 from openreplay/session-settings

Session settings
This commit is contained in:
Shekar Siri 2022-05-03 12:50:54 +02:00 committed by GitHub
commit 0bbd27e856
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 1458 additions and 431 deletions

View file

@ -26,7 +26,6 @@ import { fetchList as fetchMetadata } from 'Duck/customField';
import { fetchList as fetchSiteList } from 'Duck/site';
import { fetchList as fetchAnnouncements } from 'Duck/announcements';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { dashboardService } from "App/services";
import { withStore } from 'App/mstore'
@ -107,7 +106,6 @@ const ONBOARDING_REDIRECT_PATH = routes.onboarding(OB_DEFAULT_TAB);
fetchSiteList,
fetchAnnouncements,
fetchAlerts,
fetchWatchdogStatus,
})
class Router extends React.Component {
state = {
@ -132,7 +130,6 @@ class Router extends React.Component {
this.props.fetchMetadata()
this.props.fetchAnnouncements();
this.props.fetchAlerts();
this.props.fetchWatchdogStatus();
}, 100);
})
})

View file

@ -20,7 +20,6 @@ import { LAST_7_DAYS } from 'Types/app/period';
import { resetFunnel } from 'Duck/funnels';
import { resetFunnelFilters } from 'Duck/funnelFilters'
import NoSessionsMessage from 'Shared/NoSessionsMessage';
// import TrackerUpdateMessage from 'Shared/TrackerUpdateMessage';
import SessionSearch from 'Shared/SessionSearch';
import MainSearchBar from 'Shared/MainSearchBar';
import { clearSearch, fetchSessions } from 'Duck/search';
@ -130,7 +129,6 @@ export default class BugFinder extends React.PureComponent {
/>
</div>
<div className={cn("side-menu-margined", stl.searchWrapper) }>
{/* <TrackerUpdateMessage /> */}
<NoSessionsMessage />
<div className="mb-5">
<MainSearchBar />

View file

@ -3,29 +3,28 @@ import { connect } from 'react-redux';
import cn from 'classnames';
import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI'
import stl from './sessionMenu.css';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { clearEvents } from 'Duck/filters';
import { issues_types } from 'Types/session/issue'
import { fetchList as fetchSessionList } from 'Duck/sessions';
import { useModal } from 'App/components/Modal';
import SessionSettings from 'Shared/SessionSettings/SessionSettings'
function SessionsMenu(props) {
const { activeTab, keyMap, wdTypeCount, toggleRehydratePanel } = props;
const { activeTab } = props;
const { showModal } = useModal();
const onMenuItemClick = (filter) => {
props.onMenuItemClick(filter)
}
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
return (
<div className={stl.wrapper}>
<div className={ cn(stl.header, 'flex items-center') }>
<div className={ stl.label }>
<span>Sessions</span>
</div>
{capturingAll && <span className={ cn(stl.manageButton, 'mr-2') } onClick={ toggleRehydratePanel }>Manage</span>}
{ !capturingAll && (
<span className={ cn(stl.manageButton, 'mr-2') } onClick={() => showModal(<SessionSettings />, { right: true })}>Manage</span>
{/* { !capturingAll && (
<Popup
trigger={
<div
@ -41,7 +40,7 @@ function SessionsMenu(props) {
inverted
position="top right"
/>
)}
)} */}
</div>
<div>
@ -87,5 +86,5 @@ export default connect(state => ({
filters: state.getIn([ 'filters', 'appliedFilter' ]),
sessionsLoading: state.getIn([ 'sessions', 'fetchLiveListRequest', 'loading' ]),
}), {
fetchWatchdogStatus, clearEvents, fetchSessionList
clearEvents, fetchSessionList
})(SessionsMenu);

View file

@ -42,7 +42,6 @@ export default class NewSiteForm extends React.PureComponent {
const { sites } = this.props;
const site = sites.last();
if (!pathname.includes('/client')) {
console.log('site', site)
this.props.setSiteId(site.get('id'))
}
this.props.onClose(null, site)

View file

@ -38,7 +38,7 @@ function DashboardModal(props) {
return useObserver(() => (
<div
className="fixed border-r shadow p-4 h-screen"
style={{ backgroundColor: '#FAFAFA', zIndex: '999', width: '85%', maxWidth: '1300px' }}
style={{ backgroundColor: '#FAFAFA', zIndex: 999, width: '85%', maxWidth: '1300px' }}
>
<div className="mb-6 flex items-end justify-between">
<div>

View file

@ -12,7 +12,6 @@ import NewSiteForm from '../Client/Sites/NewSiteForm';
import { clearSearch } from 'Duck/search';
import { fetchList as fetchIntegrationVariables } from 'Duck/customField';
import { fetchList as fetchAlerts } from 'Duck/alerts';
import { fetchWatchdogStatus } from 'Duck/watchdogs';
import { withStore } from 'App/mstore'
@withStore
@ -28,7 +27,6 @@ import { withStore } from 'App/mstore'
clearSearch,
fetchIntegrationVariables,
fetchAlerts,
fetchWatchdogStatus,
})
export default class SiteDropdown extends React.PureComponent {
state = { showProductModal: false }
@ -54,7 +52,6 @@ export default class SiteDropdown extends React.PureComponent {
this.props.clearSearch();
this.props.fetchIntegrationVariables();
this.props.fetchAlerts();
this.props.fetchWatchdogStatus();
mstore.initClient();
}

View file

@ -4,10 +4,10 @@ import { useModal } from '.';
import ModalOverlay from './ModalOverlay';
export default function Modal({ children }){
const { component } = useModal();
const { component, props} = useModal();
return component ? ReactDOM.createPortal(
<ModalOverlay>
<ModalOverlay left={!props.right} right={props.right}>
{component}
</ModalOverlay>,
document.querySelector("#modal-root"),

View file

@ -7,13 +7,26 @@
/* transition: all 0.3s ease-in-out; */
animation: fade 1s forwards;
}
.slide {
position: absolute;
/* left: -100%; */
/* -webkit-animation: slide 0.5s forwards;
animation: slide 0.5s forwards; */
}
.slideLeft {
left: -100%;
-webkit-animation: slide 0.5s forwards;
animation: slide 0.5s forwards;
}
.slideRight {
right: -100%;
-webkit-animation: slideRight 0.5s forwards;
animation: slideRight 0.5s forwards;
}
@keyframes fade {
0% {
opacity: 0;
@ -29,4 +42,12 @@
@keyframes slide {
100% { left: 0; }
}
@-webkit-keyframes slideRight {
100% { right: 0; }
}
@keyframes slideRight {
100% { right: 0%; }
}

View file

@ -1,18 +1,19 @@
import React from 'react';
import { useModal } from 'App/components/Modal';
import stl from './ModalOverlay.css'
import cn from 'classnames';
function ModalOverlay({ children }) {
function ModalOverlay({ children, left = false, right = false }) {
let modal = useModal();
return (
<div className="fixed w-full h-screen" style={{ zIndex: '999' }}>
<div className="fixed w-full h-screen" style={{ zIndex: 999 }}>
<div
onClick={() => modal.hideModal()}
className={stl.overlay}
style={{ background: "rgba(0,0,0,0.5)" }}
/>
<div className={stl.slide}>{children}</div>
<div className={cn(stl.slide, { [stl.slideLeft] : left, [stl.slideRight] : right })}>{children}</div>
</div>
);
}

View file

@ -4,7 +4,9 @@ import Modal from './Modal';
const ModalContext = createContext({
component: null,
props: {},
props: {
right: false,
},
showModal: (component: any, props: any) => {},
hideModal: () => {}
});

View file

@ -276,13 +276,13 @@ export default class Controls extends React.Component {
label="Back"
icon="replay-10"
/>
<ControlButton
{/* <ControlButton
disabled={ disabled }
onClick={ this.props.toggleSkipToIssue }
active={ skipToIssue }
label="Skip to Issue"
icon={skipToIssue ? 'skip-forward-fill' : 'skip-forward'}
/>
/> */}
</div>
)}

View file

@ -19,7 +19,7 @@ function getStyles(
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
height: isDragging ? 0 : '',
zIndex: '99',
zIndex: 99,
cursor: 'move'
}
}

View file

@ -0,0 +1,64 @@
import React from 'react';
import Select from 'react-select';
interface Props {
options: any[];
isSearchable?: boolean;
defaultValue?: string;
plain?: boolean;
[x:string]: any;
}
export default function({ plain = false, options, isSearchable = false, defaultValue = '', ...rest }: Props) {
const customStyles = {
option: (provided, state) => ({
...provided,
whiteSpace: 'nowrap',
}),
menu: (provided, state) => ({
...provided,
top: 31,
}),
control: (provided) => {
const obj = {
...provided,
border: 'solid thin #ddd'
}
if (plain) {
obj['border'] = '1px solid transparent'
}
return obj;
},
valueContainer: (provided) => ({
...provided,
paddingRight: '0px',
}),
singleValue: (provided, state) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return { ...provided, opacity, transition };
}
}
const defaultSelected = defaultValue ? options.find(x => x.value === defaultValue) : options[0];
return (
<Select
options={options}
isSearchable={isSearchable}
defaultValue={defaultSelected}
components={{
IndicatorSeparator: () => null
}}
styles={customStyles}
theme={(theme) => ({
...theme,
colors: {
...theme.colors,
primary: '#394EFF',
}
})}
{...rest}
/>
);
}
// export default Select;

View file

@ -0,0 +1 @@
export { default } from './Select';

View file

@ -23,9 +23,6 @@ const ASSIST_ROUTE = assistRoute();
const ASSIST_LIVE_SESSION = liveSession()
const SESSIONS_ROUTE = sessionsRoute();
// const Label = ({ label = '', color = 'color-gray-medium'}) => (
// <div className={ cn('font-light text-sm', color)}>{label}</div>
// )
@connect(state => ({
timezone: state.getIn(['sessions', 'timezone']),
siteId: state.getIn([ 'site', 'siteId' ]),

View file

@ -0,0 +1,164 @@
import React from 'react';
import { connect } from 'react-redux';
import cn from 'classnames';
import {
Link,
Icon,
CountryFlag,
Avatar,
TextEllipsis,
Label,
} from 'UI';
import { toggleFavorite, setSessionPath } from 'Duck/sessions';
import { session as sessionRoute, liveSession as liveSessionRoute, withSiteId } from 'App/routes';
import { durationFormatted, formatTimeOrDate } from 'App/date';
import stl from './sessionItem.css';
import Counter from './Counter'
import { withRouter } from 'react-router-dom';
import SessionMetaList from './SessionMetaList';
import ErrorBars from './ErrorBars';
import { assist as assistRoute, liveSession, sessions as sessionsRoute, isRoute } from "App/routes";
import { capitalize } from 'App/utils';
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
const ASSIST_ROUTE = assistRoute();
const ASSIST_LIVE_SESSION = liveSession()
const SESSIONS_ROUTE = sessionsRoute();
// @connect(state => ({
// timezone: state.getIn(['sessions', 'timezone']),
// siteId: state.getIn([ 'site', 'siteId' ]),
// }), { toggleFavorite, setSessionPath })
// @withRouter
function SessionItem(props) {
// render() {
const {
session: {
sessionId,
userBrowser,
userOs,
userId,
userAnonymousId,
userDisplayName,
userCountry,
startedAt,
duration,
eventsCount,
errorsCount,
pagesCount,
viewed,
favorite,
userDeviceType,
userUuid,
userNumericHash,
live,
metadata,
userSessionsCount,
issueTypes,
active,
},
timezone,
onUserClick = () => null,
hasUserFilter = false,
disableUser = false,
metaList = [],
showActive = false,
lastPlayedSessionId,
} = props;
const formattedDuration = durationFormatted(duration);
const hasUserId = userId || userAnonymousId;
const isSessions = isRoute(SESSIONS_ROUTE, props.location.pathname);
const isAssist = isRoute(ASSIST_ROUTE, props.location.pathname) || isRoute(ASSIST_LIVE_SESSION, props.location.pathname);
const isLastPlayed = lastPlayedSessionId === sessionId;
const _metaList = Object.keys(metadata).filter(i => metaList.includes(i)).map(key => {
const value = metadata[key];
return { label: key, value };
});
return (
<div className={ cn(stl.sessionItem, "flex flex-col bg-white p-3 mb-3") } id="session-item" >
<div className="flex items-start">
<div className={ cn('flex items-center w-full')}>
<div className="flex items-center pr-2" style={{ width: "30%"}}>
<div><Avatar seed={ userNumericHash } isAssist={isAssist} /></div>
<div className="flex flex-col overflow-hidden color-gray-medium ml-3 justify-between items-center">
<div
className={cn('text-lg', {'color-teal cursor-pointer': !disableUser && hasUserId, 'color-gray-medium' : disableUser || !hasUserId})}
onClick={() => (!disableUser && !hasUserFilter) && onUserClick(userId, userAnonymousId)}
>
<TextEllipsis text={userDisplayName} maxWidth={200} popupProps={{ inverted: true, size: 'tiny' }} />
</div>
</div>
</div>
<div style={{ width: "20%", height: "38px" }} className="px-2 flex flex-col justify-between">
<div>{formatTimeOrDate(startedAt, timezone) }</div>
<div className="flex items-center color-gray-medium">
{!isAssist && (
<>
<div className="color-gray-medium">
<span className="mr-1">{ eventsCount }</span>
<span>{ eventsCount === 0 || eventsCount > 1 ? 'Events' : 'Event' }</span>
</div>
<div className="mx-2 text-4xl">·</div>
</>
)}
<div>{ live ? <Counter startTime={startedAt} /> : formattedDuration }</div>
</div>
</div>
<div style={{ width: "30%", height: "38px" }} className="px-2 flex flex-col justify-between">
<CountryFlag country={ userCountry } className="mr-2" label />
<div className="color-gray-medium flex items-center">
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userBrowser) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userOs) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
<div className="mx-2 text-4xl">·</div>
<span className="capitalize" style={{ maxWidth: '70px'}}>
<TextEllipsis text={ capitalize(userDeviceType) } popupProps={{ inverted: true, size: "tiny" }} />
</span>
</div>
</div>
{ isSessions && (
<div style={{ width: "10%"}} className="self-center px-2 flex items-center">
<ErrorBars count={issueTypes.length} />
</div>
)}
</div>
<div className="flex items-center">
{ isAssist && showActive && (
<Label success className={cn("bg-green color-white text-right mr-4", { 'opacity-0' : !active})}>
<span className="color-white">ACTIVE</span>
</Label>
)}
<div className={ stl.playLink } id="play-button" data-viewed={ viewed }>
{ isSessions && (
<div className="mr-4 flex-shrink-0 w-24">
{ isLastPlayed && (
<Label className="bg-gray-lightest p-1 px-2 rounded-lg">
<span className="color-gray-medium text-xs" style={{ whiteSpace: 'nowrap'}}>LAST PLAYED</span>
</Label>
)}
</div>
)}
<Link to={ isAssist ? liveSessionRoute(sessionId) : sessionRoute(sessionId) }>
<Icon name={ !viewed && !isAssist ? 'play-fill' : 'play-circle-light' } size="42" color={isAssist ? "tealx" : "teal"} />
</Link>
</div>
</div>
</div>
{ _metaList.length > 0 && (
<SessionMetaList className="mt-4" metaList={_metaList} />
)}
</div>
);
}
export default connect(state => ({
timezone: localStorage.getItem(TIMEZONE) || '',
siteId: state.getIn([ 'site', 'siteId' ]),
}), { toggleFavorite, setSessionPath })(withRouter(SessionItem));

View file

@ -6,7 +6,7 @@ import MetaMoreButton from '../MetaMoreButton';
interface Props {
className?: string,
metaList: [],
metaList: any[],
maxLength?: number,
}

View file

@ -1,23 +1,7 @@
@import 'icons.css';
@import 'mixins.css';
@keyframes fade {
0% { opacity: 1}
50% { opacity: 0}
100% { opacity: 1}
}
.sessionItem {
user-select: none;
@mixin defaultHover;
border-radius: 3px;
/* padding: 10px 10px; */
/* padding-right: 15px; */
/* margin-bottom: 15px; */
/* background-color: white; */
/* display: flex; */
/* align-items: center; */
border: solid thin #EEEEEE;
& .favorite {
@ -39,7 +23,7 @@
}
}
& .iconStack {
/* & .iconStack {
min-width: 200px;
display: flex;
& .icons {
@ -48,7 +32,7 @@
margin-bottom: 5px;
align-items: center;
}
}
} */
& .left {
& > div {
@ -114,7 +98,4 @@
text-transform: uppercase;
font-size: 10px;
letter-spacing: 1px;
& svg {
animation: fade 1s infinite;
}
}

View file

@ -0,0 +1,36 @@
import React, { useEffect } from 'react';
import { Input, Button, Toggler, Icon } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
import ListingVisibility from './components/ListingVisibility';
import DefaultPlaying from './components/DefaultPlaying';
import DefaultTimezone from './components/DefaultTimezone';
import CaptureRate from './components/CaptureRate';
function SessionSettings(props) {
return useObserver(() => (
<div className="bg-white box-shadow h-screen" style={{ width: '450px'}}>
<div className="p-6">
<h1 className="text-2xl">Sessions Settings</h1>
</div>
<div className="p-6 border-b py-8">
<ListingVisibility />
</div>
<div className="p-6 border-b py-8">
<DefaultPlaying />
</div>
<div className="p-6 border-b py-8">
<DefaultTimezone />
</div>
<div className="p-6 py-8">
<CaptureRate />
</div>
</div>
));
}
export default SessionSettings;

View file

@ -0,0 +1,67 @@
import React, { useEffect } from 'react';
import { Icon, Toggler, Button, Input, Loader } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function CaptureRate(props) {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
const loading = useObserver(() => settingsStore.loadingCaptureRate)
const [captureRate, setCaptureRate] = React.useState(sessionSettings.captureRate);
const [captureAll, setCaptureAll] = React.useState(sessionSettings.captureAll);
useEffect(() => {
settingsStore.fetchCaptureRate().then(() => {
setCaptureRate(sessionSettings.captureRate);
setCaptureAll(sessionSettings.captureAll);
});
}, [])
return useObserver(() => (
<Loader loading={loading}>
<h3 className="text-lg">Capture Rate</h3>
<div className="my-1">What percentage of your user sessions do you want to record and monitor?</div>
<div className="mt-2 mb-4">
<Toggler
checked={captureAll}
name="test"
onChange={() => {
setCaptureAll(!captureAll)
setChanged(true)
}}
label="Capture 100% of the sessions"
/>
</div>
<div className="flex items-center">
<div className="relative">
<Input
type="number"
value={captureRate}
style={{ height: '38px', width: '100px'}}
onChange={(e, { value }) => {
setCaptureRate(value)
setChanged(true);
}}
disabled={captureAll}
min={0}
minValue={0}
/>
<Icon className="absolute right-0 mr-6 top-0 bottom-0 m-auto" name="percent" color="gray-medium" size="18" />
</div>
<span className="mx-3">of the sessions</span>
<Button
disabled={!changed}
outline
size="medium"
onClick={() => settingsStore.saveCaptureRate({
rate: captureRate,
captureAll,
}).finally(() => setChanged(false))}
>Update</Button>
</div>
</Loader>
));
}
export default CaptureRate;

View file

@ -0,0 +1,25 @@
import React from 'react';
import { Toggler } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
function DefaultPlaying(props) {
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
return useObserver(() => (
<>
<h3 className="text-lg">Default Playing Options</h3>
<div className="my-1">Always start playing the session from the first issue.</div>
<div className="mt-2">
<Toggler
checked={sessionSettings.skipToIssue}
name="test"
onChange={() => sessionSettings.updateKey('skipToIssue', !sessionSettings.skipToIssue)}
/>
</div>
</>
));
}
export default DefaultPlaying;

View file

@ -0,0 +1,47 @@
import React, { useEffect } from 'react';
import { Toggler, Button } from 'UI';
import Select from 'Shared/Select';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const str = new Date().toString().match(/([A-Z]+[\+-][0-9]+)/)
const d = str && str[1] || 'UTC';
const timezoneOptions = [
{ label: d, value: 'local' },
{ label: 'UTC', value: 'UTC' },
]
function DefaultTimezone(props) {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const [timezone, setTimezone] = React.useState(settingsStore.sessionSettings.timezone);
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
console.log('timezone', timezone)
return (
<>
<h3 className="text-lg">Default Timezone</h3>
<div className="my-1">Session Time</div>
<div className="mt-2 flex items-center" style={{ width: "220px"}}>
<Select
options={timezoneOptions}
defaultValue={timezone}
className="w-full"
onChange={({ value }) => {
setTimezone(value);
setChanged(true);
}}
/>
<div className="col-span-3 ml-3">
<Button disabled={!changed} outline size="medium" onClick={() => {
setChanged(false);
sessionSettings.updateKey('timezone', timezone);
}}>Update</Button>
</div>
</div>
<div className="text-sm mt-3">This change will impact the timestamp on session card and player.</div>
</>
);
}
export default DefaultTimezone;

View file

@ -0,0 +1,70 @@
import React from 'react';
import Select from 'Shared/Select';
import { Button, Input } from 'UI';
import { useStore } from 'App/mstore';
import { useObserver } from 'mobx-react-lite';
const numberOptions = [
{ label: 'Less than', value: '<' },
{ label: 'Greater than', value: '>' },
]
const periodOptions = [
{ label: 'Mins', value: 'min' },
{ label: 'Secs', value: 'sec' },
]
function ListingVisibility(props) {
const [changed, setChanged] = React.useState(false);
const { settingsStore } = useStore();
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
const [durationSettings, setDurationSettings] = React.useState(sessionSettings.durationFilter);
return (
<>
<h3 className="text-lg">Listing Visibility</h3>
<div className="my-1">Do not show sessions duration with.</div>
<div className="grid grid-cols-12 gap-2 mt-2">
<div className="col-span-4">
<Select
options={numberOptions}
defaultValue={durationSettings.operator}
onChange={({ value }) => {
setDurationSettings({ ...durationSettings, operator: value });
setChanged(true);
}}
/>
</div>
<div className="col-span-2">
<Input
value={durationSettings.count}
type="number"
name="count"
style={{ height: '38px', width: '100%'}}
onChange={(e, { value }) => {
setDurationSettings({ ...durationSettings, count: value });
setChanged(true);
}}
/>
</div>
<div className="col-span-3">
<Select
defaultValue={durationSettings.countType}
options={periodOptions}
onChange={({ value }) => {
setDurationSettings({ ...durationSettings, countType: value });
setChanged(true);
}}
/>
</div>
<div className="col-span-3">
<Button outline size="medium" disabled={!changed} onClick={() => {
sessionSettings.updateKey('durationFilter', durationSettings);
setChanged(false);
}}>Update</Button>
</div>
</div>
</>
);
}
export default ListingVisibility;

View file

@ -0,0 +1 @@
export { default } from './SessionSettings';

View file

@ -0,0 +1,84 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
const CheckedIcon = () => <>🌜</>;
const UncheckedIcon = () => <>🌞</>;
const ToggleButton = ( props ) => {
const [toggle, setToggle] = useState(false);
const { defaultChecked, onChange, disabled, className } = props;
useEffect(() => {
if (defaultChecked) {
setToggle(defaultChecked)
}
}, [defaultChecked]);
const triggerToggle = () => {
if ( disabled ) {
return;
}
setToggle(!toggle);
if ( typeof onChange === 'function' ) {
onChange(!toggle);
}
}
const getIcon = (type) => {
const { icons } = props;
if ( ! icons ) {
return null;
}
return icons[type] === undefined ?
ToggleButton.defaultProps.icons[type] :
icons[type];
}
const toggleClasses = classNames('wrg-toggle', {
'wrg-toggle--checked': toggle,
'wrg-toggle--disabled': disabled
}, className);
return (
<div onClick={triggerToggle} className={toggleClasses}>
<div className="wrg-toggle-container">
<div className="wrg-toggle-check">
<span>{ getIcon('checked') }</span>
</div>
<div className="wrg-toggle-uncheck">
<span>{ getIcon('unchecked') }</span>
</div>
</div>
<div className="wrg-toggle-circle"></div>
<input type="checkbox" aria-label="Toggle Button" className="wrg-toggle-input" />
</div>
);
}
ToggleButton.defaultProps = {
icons: {
checked: <CheckedIcon />,
unchecked: <UncheckedIcon />
}
};
ToggleButton.propTypes = {
disabled: PropTypes.bool,
defaultChecked: PropTypes.bool,
className: PropTypes.string,
onChange: PropTypes.func,
icons: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
checked: PropTypes.node,
unchecked: PropTypes.node
})
])
};
export default ToggleButton;

View file

@ -3,18 +3,22 @@ import styles from './toggler.css';
export default ({
onChange,
name,
className,
className = '',
checked,
label = '',
}) => (
<div className={ className }>
<label className={ styles.switch }>
<input
type={ styles.checkbox }
onClick={ onChange }
name={ name }
checked={ checked }
/>
<span className={ `${ styles.slider } ${ checked ? styles.checked : '' }` } />
<label className={styles.label}>
<div className={ styles.switch }>
<input
type={ styles.checkbox }
onClick={ onChange }
name={ name }
checked={ checked }
/>
<span className={ `${ styles.slider } ${ checked ? styles.checked : '' }` } />
</div>
{ label && <span>{ label }</span> }
</label>
</div>
);

View file

@ -6,6 +6,16 @@
height: 16px;
}
.label {
display: flex;
align-items: center;
cursor: pointer;
& span {
padding-left: 10px;
color: $gray-medium;
}
}
.switch input {
display:none;
}
@ -29,18 +39,23 @@
width: 20px;
left: 0;
bottom: -2px;
background-color: white;
/* background-color: white; */
transition: .4s;
border-radius: 50%;
border: solid 1px rgba(0, 0, 0, 0.2);
background: #394EFF;
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px rgba(0, 0, 0, 0.14), 0px 1px 3px rgba(0, 0, 0, 0.12);
}
.slider.checked {
background-color: $teal !important;
/* background-color: $teal !important; */
background-color: #b2bcff !important;
}
.slider.checked:before {
border: solid 1px $teal;
transform: translateX(15px);
}
.slider.checked:before {

View file

@ -58,5 +58,6 @@ export { default as HelpText } from './HelpText';
export { default as SideMenuHeader } from './SideMenuHeader';
export { default as PageTitle } from './PageTitle';
export { default as Pagination } from './Pagination';
export { default as Toggler } from './Toggler';
export { Input, Modal, Form, Message, Card } from 'semantic-ui-react';

View file

@ -20,3 +20,4 @@ export {
WEBHOOK as CHANNEL_WEBHOOK
} from './schedule';
export { default } from './filterOptions';
export { default as storageKeys } from './storageKeys';

View file

@ -0,0 +1,3 @@
export const SKIP_TO_ISSUE = "__$session-skipToIssue$__"
export const TIMEZONE = "__$session-timezone$__"
export const DURATION_FILTER = "__$session-durationFilter$__"

View file

@ -9,6 +9,7 @@ import { fetchList as fetchSessionList } from './sessions';
import { fetchList as fetchErrorsList } from './errors';
import { FilterCategory, FilterKey, IssueType } from 'Types/filter/filterType';
import { filtersMap, liveFiltersMap, generateFilterOptions, generateLiveFilterOptions } from 'Types/filter/newFilter';
import { DURATION_FILTER } from 'App/constants/storageKeys'
const ERRORS_ROUTE = errorsRoute();
@ -149,6 +150,28 @@ export const reduceThenFetchResource = actionCreator => (...args) => (dispatch,
filter.limit = 10;
filter.page = getState().getIn([ 'search', 'currentPage']);
// duration filter from local storage
if (!filter.filters.find(f => f.type === FilterKey.DURATION)) {
const durationFilter = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{"count": 0}');
let durationValue = parseInt(durationFilter.count)
if (durationValue > 0) {
const value = [0];
durationValue = durationFilter.countType === 'min' ? durationValue * 60 * 1000 : durationValue * 1000;
if (durationFilter.operator === '<') {
value[0] = durationValue;
} else if (durationFilter.operator === '>') {
value[1] = durationValue;
}
filter.filters = filter.filters.concat({
type: FilterKey.DURATION,
operator: 'is',
value,
});
}
}
return isRoute(ERRORS_ROUTE, window.location.pathname)
? dispatch(fetchErrorsList(filter))
: dispatch(fetchSessionList(filter));

View file

@ -2,21 +2,25 @@ import React from 'react';
import DashboardStore, { IDashboardSotre } from './dashboardStore';
import MetricStore, { IMetricStore } from './metricStore';
import APIClient from 'App/api_client';
import { dashboardService, metricService } from 'App/services';
import { dashboardService, metricService, sessionService } from 'App/services';
import SettingsStore from './settingsStore';
export class RootStore {
dashboardStore: IDashboardSotre;
metricStore: IMetricStore;
settingsStore: SettingsStore;
constructor() {
this.dashboardStore = new DashboardStore();
this.metricStore = new MetricStore();
this.settingsStore = new SettingsStore();
}
initClient() {
const client = new APIClient();
dashboardService.initClient(client)
metricService.initClient(client)
sessionService.initClient(client)
}
}

View file

@ -0,0 +1,42 @@
import { makeAutoObservable, observable, action } from "mobx"
import SessionSettings from "./types/sessionSettings"
import { sessionService } from "App/services"
import { toast } from 'react-toastify';
export default class SettingsStore {
loadingCaptureRate: boolean = false;
sessionSettings: SessionSettings = new SessionSettings()
captureRateFetched: boolean = false;
constructor() {
makeAutoObservable(this, {
sessionSettings: observable,
})
}
saveCaptureRate(data: any) {
return sessionService.saveCaptureRate(data)
.then(data => {
this.sessionSettings.merge({
captureRate: data.rate,
captureAll: data.captureAll
})
toast.success("Capture rate saved successfully");
}).catch(err => {
toast.error("Error saving capture rate");
})
}
fetchCaptureRate(): Promise<any> {
this.loadingCaptureRate = true;
return sessionService.fetchCaptureRate()
.then(data => {
this.sessionSettings.merge({
captureRate: data.rate,
captureAll: data.captureAll
})
this.captureRateFetched = true;
}).finally(() => {
this.loadingCaptureRate = false;
})
}
}

View file

@ -0,0 +1,38 @@
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
import { SKIP_TO_ISSUE, TIMEZONE, DURATION_FILTER } from 'App/constants/storageKeys'
export default class SessionSettings {
skipToIssue: boolean = localStorage.getItem(SKIP_TO_ISSUE) === 'true';
timezone: string = localStorage.getItem(TIMEZONE) || 'UTC';
durationFilter: any = JSON.parse(localStorage.getItem(DURATION_FILTER) || '{}');
captureRate: number = 0
captureAll: boolean = false
constructor() {
makeAutoObservable(this, {
updateKey: action
})
}
merge(settings: any) {
for (const key in settings) {
if (settings.hasOwnProperty(key)) {
this.updateKey(key, settings[key]);
}
}
}
updateKey(key: string, value: any) {
runInAction(() => {
this[key] = value
})
if (key === 'captureRate' || key === 'captureAll') return
if (key === 'durationFilter') {
localStorage.setItem(`__$session-${key}$__`, JSON.stringify(value));
} else {
localStorage.setItem(`__$session-${key}$__`, value);
}
}
}

View file

@ -27,7 +27,6 @@ export default class Cursor {
}
click() {
console.log("clickong ", styles.clicked)
this.cursor.classList.add(styles.clicked)
setTimeout(() => {
this.cursor.classList.remove(styles.clicked)

View file

@ -25,7 +25,7 @@ const HIGHEST_SPEED = 16;
const SPEED_STORAGE_KEY = "__$player-speed$__";
const SKIP_STORAGE_KEY = "__$player-skip$__";
const SKIP_TO_ISSUE_STORAGE_KEY = "__$player-skip-to-issue$__";
const SKIP_TO_ISSUE_STORAGE_KEY = "__$session-skipToIssue$__";
const AUTOPLAY_STORAGE_KEY = "__$player-autoplay$__";
const SHOW_EVENTS_STORAGE_KEY = "__$player-show-events$__";
const storedSpeed: number = parseInt(localStorage.getItem(SPEED_STORAGE_KEY) || "") ;

View file

@ -0,0 +1,23 @@
import APIClient from 'App/api_client';
export default class SettingsService {
private client: APIClient;
constructor(client?: APIClient) {
this.client = client ? client : new APIClient();
}
initClient(client?: APIClient) {
this.client = client || new APIClient();
}
saveCaptureRate(data: any) {
return this.client.post('/sample_rate', data);
}
fetchCaptureRate() {
return this.client.get('/sample_rate')
.then(response => response.json())
.then(response => response.data || 0);
}
}

View file

@ -1,5 +1,7 @@
import DashboardService, { IDashboardService } from "./DashboardService";
import MetricService, { IMetricService } from "./MetricService";
import SessionSerivce from "./SessionService";
export const dashboardService: IDashboardService = new DashboardService();
export const metricService: IMetricService = new MetricService();
export const metricService: IMetricService = new MetricService();
export const sessionService: SessionSerivce = new SessionSerivce();

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" class="bi bi-percent" viewBox="0 0 16 16">
<path d="M13.442 2.558a.625.625 0 0 1 0 .884l-10 10a.625.625 0 1 1-.884-.884l10-10a.625.625 0 0 1 .884 0zM4.5 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zm7 6a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 1a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

File diff suppressed because it is too large Load diff

View file

@ -51,6 +51,7 @@
"react-redux": "^5.1.2",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-select": "^5.3.1",
"react-svg-map": "^2.2.0",
"react-tippy": "^1.4.0",
"react-toastify": "^8.2.0",