Merge pull request #441 from openreplay/session-settings
Session settings
This commit is contained in:
commit
0bbd27e856
41 changed files with 1458 additions and 431 deletions
|
|
@ -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);
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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%; }
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import Modal from './Modal';
|
|||
|
||||
const ModalContext = createContext({
|
||||
component: null,
|
||||
props: {},
|
||||
props: {
|
||||
right: false,
|
||||
},
|
||||
showModal: (component: any, props: any) => {},
|
||||
hideModal: () => {}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
frontend/app/components/shared/Select/Select.tsx
Normal file
64
frontend/app/components/shared/Select/Select.tsx
Normal 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;
|
||||
1
frontend/app/components/shared/Select/index.ts
Normal file
1
frontend/app/components/shared/Select/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Select';
|
||||
|
|
@ -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' ]),
|
||||
|
|
|
|||
164
frontend/app/components/shared/SessionItem/SessionItem.tsx
Normal file
164
frontend/app/components/shared/SessionItem/SessionItem.tsx
Normal 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));
|
||||
|
|
@ -6,7 +6,7 @@ import MetaMoreButton from '../MetaMoreButton';
|
|||
|
||||
interface Props {
|
||||
className?: string,
|
||||
metaList: [],
|
||||
metaList: any[],
|
||||
maxLength?: number,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
1
frontend/app/components/shared/SessionSettings/index.ts
Normal file
1
frontend/app/components/shared/SessionSettings/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './SessionSettings';
|
||||
84
frontend/app/components/ui/ToggleButton/ToggleButton.tsx
Normal file
84
frontend/app/components/ui/ToggleButton/ToggleButton.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ export {
|
|||
WEBHOOK as CHANNEL_WEBHOOK
|
||||
} from './schedule';
|
||||
export { default } from './filterOptions';
|
||||
export { default as storageKeys } from './storageKeys';
|
||||
3
frontend/app/constants/storageKeys.ts
Normal file
3
frontend/app/constants/storageKeys.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export const SKIP_TO_ISSUE = "__$session-skipToIssue$__"
|
||||
export const TIMEZONE = "__$session-timezone$__"
|
||||
export const DURATION_FILTER = "__$session-durationFilter$__"
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
42
frontend/app/mstore/settingsStore.ts
Normal file
42
frontend/app/mstore/settingsStore.ts
Normal 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;
|
||||
})
|
||||
}
|
||||
}
|
||||
38
frontend/app/mstore/types/sessionSettings.ts
Normal file
38
frontend/app/mstore/types/sessionSettings.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) || "") ;
|
||||
|
|
|
|||
23
frontend/app/services/SessionService.ts
Normal file
23
frontend/app/services/SessionService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
3
frontend/app/svg/icons/percent.svg
Normal file
3
frontend/app/svg/icons/percent.svg
Normal 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 |
1043
frontend/package-lock.json
generated
1043
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue