feat(ui) - session settings - ui and state
This commit is contained in:
parent
122705b4c7
commit
a1b656dc6a
18 changed files with 442 additions and 15 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import { SideMenuitem, SavedSearchList, Progress, Popup } from 'UI'
|
||||
|
|
@ -7,17 +7,23 @@ 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 { hideModal, showModal } = useModal();
|
||||
|
||||
const onMenuItemClick = (filter) => {
|
||||
props.onMenuItemClick(filter)
|
||||
}
|
||||
|
||||
|
||||
const capturingAll = props.captureRate && props.captureRate.get('captureAll');
|
||||
|
||||
useEffect(() => {
|
||||
showModal(<SessionSettings />, {});
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={stl.wrapper}>
|
||||
<div className={ cn(stl.header, 'flex items-center') }>
|
||||
|
|
|
|||
63
frontend/app/components/shared/Select/Select.tsx
Normal file
63
frontend/app/components/shared/Select/Select.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
|
||||
interface Props {
|
||||
options: any[];
|
||||
isSearchable?: boolean;
|
||||
defaultValue?: any;
|
||||
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 };
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
options={options}
|
||||
isSearchable={isSearchable}
|
||||
defaultValue={defaultValue ? options.find(i => i.value === defaultValue) : options[0]}
|
||||
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';
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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) {
|
||||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
|
||||
|
||||
return useObserver(() => (
|
||||
<div className="bg-white box-shadow h-screen" style={{ width: '450px'}}>
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl">Session 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,43 @@
|
|||
import React from 'react';
|
||||
import { Icon, Toggler, Button, Input } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { useObserver } from 'mobx-react-lite';
|
||||
|
||||
function CaptureRate(props) {
|
||||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const [captureRate, setCaptureRate] = React.useState(sessionSettings.captureRate);
|
||||
const [captureAll, setCaptureAll] = React.useState(captureRate === 100);
|
||||
return (
|
||||
<>
|
||||
<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)}
|
||||
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)}
|
||||
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 outline size="medium" onClick={settingsStore.updateCaptureRate(captureRate)}>Update</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,42 @@
|
|||
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 timezoneOptions = [
|
||||
{ label: 'UTC', value: 'UTC' },
|
||||
{ label: 'EST', value: 'EST' },
|
||||
]
|
||||
|
||||
function DefaultTimezone(props) {
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-lg">Default Timezone</h3>
|
||||
<div className="my-1">Session Time</div>
|
||||
<div className="mt-2 flex items-center" style={{ width: "200px"}}>
|
||||
<Select
|
||||
options={timezoneOptions}
|
||||
defaultValue={sessionSettings.timezone}
|
||||
className="w-4/6"
|
||||
onChange={(e, { value }) => {
|
||||
sessionSettings.updateKey('timezone', value);
|
||||
setChanged(true);
|
||||
}}
|
||||
/>
|
||||
<div className="col-span-3 ml-3">
|
||||
<Button disabled={!changed} outline size="medium" onClick={() => {
|
||||
setChanged(false);
|
||||
}}>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,57 @@
|
|||
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 { settingsStore } = useStore();
|
||||
const sessionSettings = useObserver(() => settingsStore.sessionSettings)
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
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-3">
|
||||
<Select
|
||||
options={numberOptions}
|
||||
defaultValue={durationSettings.operator}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
value={durationSettings.count}
|
||||
type="number"
|
||||
name="count"
|
||||
style={{ height: '38px', width: '100%'}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
defaultValue={durationSettings.countType}
|
||||
options={periodOptions}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<Button outline size="medium" disabled={!changed} onClick={() => {
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -3,14 +3,17 @@ import DashboardStore, { IDashboardSotre } from './dashboardStore';
|
|||
import MetricStore, { IMetricStore } from './metricStore';
|
||||
import APIClient from 'App/api_client';
|
||||
import { dashboardService, metricService } 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() {
|
||||
|
|
|
|||
15
frontend/app/mstore/settingsStore.ts
Normal file
15
frontend/app/mstore/settingsStore.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
import SessionSettings from "./types/sessionSettings"
|
||||
|
||||
export default class SettingsStore {
|
||||
sessionSettings: SessionSettings = new SessionSettings()
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
sessionSettings: observable,
|
||||
})
|
||||
}
|
||||
|
||||
updateCaptureRate(value: number) {
|
||||
this.sessionSettings.updateKey('captureRate', value);
|
||||
}
|
||||
}
|
||||
26
frontend/app/mstore/types/sessionSettings.ts
Normal file
26
frontend/app/mstore/types/sessionSettings.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { makeAutoObservable, runInAction, observable, action, reaction } from "mobx"
|
||||
|
||||
export default class SessionSettings {
|
||||
skipToIssue: boolean = false
|
||||
timezone: string = "EST"
|
||||
durationFilter: any = {
|
||||
count: 0,
|
||||
countType: 'min',
|
||||
operator: '>'
|
||||
}
|
||||
captureRate: number = 0
|
||||
captureAll: boolean = false
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this, {
|
||||
updateKey: action
|
||||
})
|
||||
}
|
||||
|
||||
updateKey(key: string, value: any) {
|
||||
console.log(`SessionSettings.updateKey(${key}, ${value})`)
|
||||
runInAction(() => {
|
||||
this[key] = value
|
||||
})
|
||||
}
|
||||
}
|
||||
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 |
Loading…
Add table
Reference in a new issue