feat(ui) - session copy and live list load more button

This commit is contained in:
Shekar Siri 2022-02-11 02:04:36 +01:00
parent adb742c0d9
commit d08cef7570
8 changed files with 105 additions and 43 deletions

View file

@ -1,16 +1,17 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { fetchLiveList } from 'Duck/sessions'; import { fetchLiveList } from 'Duck/sessions';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { NoContent, Loader } from 'UI'; import { NoContent, Loader, LoadMoreButton } from 'UI';
import { List, Map } from 'immutable'; import { List, Map } from 'immutable';
import SessionItem from 'Shared/SessionItem'; import SessionItem from 'Shared/SessionItem';
import withPermissions from 'HOCs/withPermissions' import withPermissions from 'HOCs/withPermissions'
import { KEYS } from 'Types/filter/customFilter'; import { KEYS } from 'Types/filter/customFilter';
import { applyFilter, addAttribute } from 'Duck/filters'; import { applyFilter, addAttribute } from 'Duck/filters';
import { FilterCategory, FilterKey } from 'App/types/filter/filterType'; import { FilterCategory, FilterKey } from 'App/types/filter/filterType';
import { addFilterByKeyAndValue } from 'Duck/liveSearch'; import { addFilterByKeyAndValue, updateCurrentPage } from 'Duck/liveSearch';
const AUTOREFRESH_INTERVAL = .5 * 60 * 1000 const AUTOREFRESH_INTERVAL = .5 * 60 * 1000
const PER_PAGE = 20;
interface Props { interface Props {
loading: Boolean, loading: Boolean,
@ -20,14 +21,20 @@ interface Props {
filters: any, filters: any,
addAttribute: (obj) => void, addAttribute: (obj) => void,
addFilterByKeyAndValue: (key: FilterKey, value: string) => void, addFilterByKeyAndValue: (key: FilterKey, value: string) => void,
updateCurrentPage: (page: number) => void,
currentPage: number,
} }
function LiveSessionList(props: Props) { function LiveSessionList(props: Props) {
const { loading, filters, list } = props; const { loading, filters, list, currentPage } = props;
var timeoutId; var timeoutId;
const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID); const hasUserFilter = filters.map(i => i.key).includes(KEYS.USERID);
const [sessions, setSessions] = React.useState(list); const [sessions, setSessions] = React.useState(list);
const displayedCount = Math.min(currentPage * PER_PAGE, sessions.size);
const addPage = () => props.updateCurrentPage(props.currentPage + 1)
useEffect(() => { useEffect(() => {
if (filters.size === 0) { if (filters.size === 0) {
props.addFilterByKeyAndValue(FilterKey.USERID, ''); props.addFilterByKeyAndValue(FilterKey.USERID, '');
@ -92,7 +99,7 @@ function LiveSessionList(props: Props) {
show={ !loading && sessions && sessions.size === 0} show={ !loading && sessions && sessions.size === 0}
> >
<Loader loading={ loading }> <Loader loading={ loading }>
{sessions && sessions.map(session => ( {sessions && sessions.take(displayedCount).map(session => (
<SessionItem <SessionItem
key={ session.sessionId } key={ session.sessionId }
session={ session } session={ session }
@ -101,6 +108,13 @@ function LiveSessionList(props: Props) {
onUserClick={onUserClick} onUserClick={onUserClick}
/> />
))} ))}
<LoadMoreButton
className="mt-3"
displayedCount={displayedCount}
totalCount={sessions.size}
onClick={addPage}
/>
</Loader> </Loader>
</NoContent> </NoContent>
</div> </div>
@ -112,6 +126,7 @@ export default withPermissions(['ASSIST_LIVE', 'SESSION_REPLAY'])(connect(
list: state.getIn(['sessions', 'liveSessions']), list: state.getIn(['sessions', 'liveSessions']),
loading: state.getIn([ 'sessions', 'loading' ]), loading: state.getIn([ 'sessions', 'loading' ]),
filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]), filters: state.getIn([ 'liveSearch', 'instance', 'filters' ]),
currentPage: state.getIn(["liveSearch", "currentPage"]),
}), }),
{ fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue } { fetchLiveList, applyFilter, addAttribute, addFilterByKeyAndValue, updateCurrentPage }
)(LiveSessionList)); )(LiveSessionList));

View file

@ -7,9 +7,11 @@ import {
connectPlayer, connectPlayer,
init as initPlayer, init as initPlayer,
clean as cleanPlayer, clean as cleanPlayer,
Controls,
} from 'Player'; } from 'Player';
import cn from 'classnames' import cn from 'classnames'
import RightBlock from './RightBlock' import RightBlock from './RightBlock'
import withLocationHandlers from "HOCs/withLocationHandlers";
import PlayerBlockHeader from '../Session_/PlayerBlockHeader'; import PlayerBlockHeader from '../Session_/PlayerBlockHeader';
@ -35,9 +37,17 @@ function PlayerContent({ live, fullscreen, showEvents }) {
) )
} }
function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, config }) { function WebPlayer (props) {
const { session, toggleFullscreen, closeBottomBlock, live, fullscreen, jwt, config } = props;
useEffect(() => { useEffect(() => {
initPlayer(session, jwt, config); initPlayer(session, jwt, config);
const jumptTime = props.query.get('jumpto');
if (jumptTime) {
Controls.jump(parseInt(jumptTime));
}
return () => cleanPlayer() return () => cleanPlayer()
}, [ session.sessionId ]); }, [ session.sessionId ]);
@ -56,7 +66,6 @@ function WebPlayer ({ session, toggleFullscreen, closeBottomBlock, live, fullscr
); );
} }
export default connect(state => ({ export default connect(state => ({
session: state.getIn([ 'sessions', 'current' ]), session: state.getIn([ 'sessions', 'current' ]),
jwt: state.get('jwt'), jwt: state.get('jwt'),
@ -65,5 +74,4 @@ export default connect(state => ({
}), { }), {
toggleFullscreen, toggleFullscreen,
closeBottomBlock, closeBottomBlock,
})(WebPlayer) })(withLocationHandlers()(WebPlayer));

View file

@ -143,6 +143,7 @@ export default class PlayerBlockHeader extends React.PureComponent {
<SharePopup <SharePopup
entity="sessions" entity="sessions"
id={ sessionId } id={ sessionId }
showCopyLink={true}
trigger={ trigger={
<IconButton <IconButton
className="mr-2" className="mr-2"

View file

@ -1,13 +1,31 @@
import React from 'react' import React from 'react';
import { IconButton } from 'UI' import { IconButton } from 'UI';
import copy from 'copy-to-clipboard';
import { connectPlayer } from 'Player';
interface Props {
content: string;
time: any;
}
function SessionCopyLink({ content = '', time }: Props) {
const [copied, setCopied] = React.useState(false)
const copyHandler = () => {
setCopied(true);
copy(window.location.origin + window.location.pathname + '?jumpto=' + Math.round(time));
setTimeout(() => {
setCopied(false);
}, 1000);
};
function SessionCopyLink() {
return ( return (
<div className="flex justify-between items-center w-full border-t -mx-4 px-4"> <div className="flex justify-between items-center w-full mt-2">
<IconButton label="Copy Link" icon="link-45deg" /> <IconButton label="Copy Link" primaryText icon="link-45deg" onClick={copyHandler} />
<div>Copied to Clipboard</div> { copied && <div className="color-teal">Copied to Clipboard</div> }
</div> </div>
) )
} }
export default SessionCopyLink export default connectPlayer(state => ({
time: state.time,
}))(SessionCopyLink);

View file

@ -47,7 +47,7 @@ export default class SharePopup extends React.PureComponent {
changeChannel = (e, { value }) => this.setState({ channelId: value }) changeChannel = (e, { value }) => this.setState({ channelId: value })
render() { render() {
const { trigger, loading, channels } = this.props; const { trigger, loading, channels, showCopyLink = false } = this.props;
const { comment, isOpen, channelId } = this.state; const { comment, isOpen, channelId } = this.state;
const options = channels.map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS(); const options = channels.map(({ webhookId, name }) => ({ value: webhookId, text: name })).toJS();
@ -67,9 +67,11 @@ export default class SharePopup extends React.PureComponent {
<div className={ styles.body }> <div className={ styles.body }>
<IntegrateSlackButton /> <IntegrateSlackButton />
</div> </div>
<div className={styles.footer}> { showCopyLink && (
<SessionCopyLink /> <div className={styles.footer}>
</div> <SessionCopyLink />
</div>
)}
</> </>
: :
<div> <div>
@ -78,32 +80,34 @@ export default class SharePopup extends React.PureComponent {
name="message" name="message"
id="message" id="message"
cols="30" cols="30"
rows="6" rows="4"
resize="none" resize="none"
onChange={ this.editMessage } onChange={ this.editMessage }
value={ comment } value={ comment }
placeholder="Type here..." placeholder="Type here..."
className="p-4" className="p-4"
/> />
<div className="flex items-center justify-between">
<Dropdown
selection
options={ options }
value={ channelId }
onChange={ this.changeChannel }
className="mr-4"
/>
<div>
<button
className={ styles.shareButton }
onClick={ this.share }
>
<Icon name="integrations/slack" size="18" marginRight="10" />
{ loading ? 'Sharing...' : 'Share' }
</button>
</div>
</div>
</div> </div>
<div className={ styles.footer }> <div className={ styles.footer }>
<Dropdown
selection
options={ options }
value={ channelId }
onChange={ this.changeChannel }
className="mr-4"
/>
<div>
<button
className={ styles.shareButton }
onClick={ this.share }
>
<Icon name="integrations/slack" size="18" marginRight="10" />
{ loading ? 'Sharing...' : 'Share' }
</button>
</div>
<SessionCopyLink /> <SessionCopyLink />
</div> </div>

View file

@ -35,13 +35,18 @@
border-radius: 3px; border-radius: 3px;
resize: none; resize: none;
} }
margin-bottom: 14px;
} }
.footer { .footer {
display: flex; /* display: flex; */
align-items: center; /* align-items: center; */
justify-content: space-between; /* justify-content: space-between; */
padding: 10px 0; /* padding: 10px 0; */
border-top: solid thin $gray-light;
margin: 0 -14px;
padding: 0 14px;
/* border-bottom: solid thin $gray-light; */
} }
textarea { textarea {

View file

@ -17,7 +17,7 @@ function CopyButton({ content, className }) {
className={ className } className={ className }
onClick={ copyHandler } onClick={ copyHandler }
> >
{ copied ? 'copied' : 'copy' } { copied ? 'Copied' : 'Copy' }
</button> </button>
) )
} }

View file

@ -16,11 +16,13 @@ const FETCH = fetchType(name);
const EDIT = editType(name); const EDIT = editType(name);
const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`; const CLEAR_SEARCH = `${name}/CLEAR_SEARCH`;
const APPLY = `${name}/APPLY`; const APPLY = `${name}/APPLY`;
const UPDATE_CURRENT_PAGE = `${name}/UPDATE_CURRENT_PAGE`;
const initialState = Map({ const initialState = Map({
list: List(), list: List(),
instance: new Filter({ filters: [] }), instance: new Filter({ filters: [] }),
filterSearchList: {}, filterSearchList: {},
currentPage: 1,
}); });
@ -28,6 +30,8 @@ function reducer(state = initialState, action = {}) {
switch (action.type) { switch (action.type) {
case EDIT: case EDIT:
return state.mergeIn(['instance'], action.instance); return state.mergeIn(['instance'], action.instance);
case UPDATE_CURRENT_PAGE:
return state.set('currentPage', action.page);
} }
return state; return state;
} }
@ -90,3 +94,10 @@ export const addFilterByKeyAndValue = (key, value) => (dispatch, getState) => {
defaultFilter.value = value; defaultFilter.value = value;
dispatch(addFilter(defaultFilter)); dispatch(addFilter(defaultFilter));
} }
export function updateCurrentPage(page) {
return {
type: UPDATE_CURRENT_PAGE,
page,
};
}