change(ui): add recordings list in assist section
This commit is contained in:
parent
5127be6cfc
commit
4a20b287a7
17 changed files with 446 additions and 22 deletions
|
|
@ -67,6 +67,7 @@ const DASHBOARD_METRIC_DETAILS_PATH = routes.dashboardMetricDetails();
|
|||
// const WIDGET_PATAH = routes.dashboardMetric();
|
||||
const SESSIONS_PATH = routes.sessions();
|
||||
const ASSIST_PATH = routes.assist();
|
||||
const RECORDINGS_PATH = routes.recordings();
|
||||
const ERRORS_PATH = routes.errors();
|
||||
const ERROR_PATH = routes.error();
|
||||
const FUNNEL_PATH = routes.funnels();
|
||||
|
|
@ -213,6 +214,7 @@ class Router extends React.Component {
|
|||
<Route exact strict path={withSiteId(DASHBOARD_METRIC_DETAILS_PATH, siteIdList)} component={Dashboard} />
|
||||
|
||||
<Route exact strict path={withSiteId(ASSIST_PATH, siteIdList)} component={Assist} />
|
||||
<Route exact strict path={withSiteId(RECORDINGS_PATH, siteIdList)} component={Assist} />
|
||||
<Route exact strict path={withSiteId(ERRORS_PATH, siteIdList)} component={Errors} />
|
||||
<Route exact strict path={withSiteId(ERROR_PATH, siteIdList)} component={Errors} />
|
||||
<Route exact strict path={withSiteId(FUNNEL_PATH, siteIdList)} component={FunnelPage} />
|
||||
|
|
|
|||
|
|
@ -1,26 +1,51 @@
|
|||
import React from 'react';
|
||||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import cn from 'classnames'
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import withPageTitle from 'HOCs/withPageTitle';
|
||||
import withPermissions from 'HOCs/withPermissions'
|
||||
// import SessionSearch from '../shared/SessionSearch';
|
||||
// import MainSearchBar from '../shared/MainSearchBar';
|
||||
import AssistSearchField from './AssistSearchField';
|
||||
import AssistRouter from './AssistRouter';
|
||||
import { SideMenuitem } from 'UI';
|
||||
import { withSiteId, assist, recordings } from 'App/routes';
|
||||
|
||||
function Assist() {
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
siteId: string;
|
||||
history: any;
|
||||
setShowAlerts: (show: boolean) => void;
|
||||
}
|
||||
|
||||
function Assist(props: Props) {
|
||||
const { history, siteId, setShowAlerts } = props;
|
||||
const isAssist = history.location.pathname.includes('assist');
|
||||
const isRecords = history.location.pathname.includes('recordings');
|
||||
|
||||
const redirect = (path: string) => {
|
||||
history.push(path);
|
||||
};
|
||||
return (
|
||||
<div className="page-margin container-90 flex relative">
|
||||
<div className="flex-1 flex">
|
||||
<div className={cn("w-full mx-auto")} style={{ maxWidth: '1300px'}}>
|
||||
<AssistSearchField />
|
||||
<LiveSessionSearch />
|
||||
<div className="my-4" />
|
||||
<LiveSessionList />
|
||||
<div className="side-menu">
|
||||
<SideMenuitem
|
||||
active={isAssist}
|
||||
id="menu-assist"
|
||||
title="Live Sessions"
|
||||
iconName="play-circle-light"
|
||||
onClick={() => redirect(withSiteId(assist(), siteId))}
|
||||
/>
|
||||
<SideMenuitem
|
||||
active={isRecords}
|
||||
id="menu-rec"
|
||||
title="Recordings"
|
||||
iconName="record-circle"
|
||||
onClick={() => redirect(withSiteId(recordings(), siteId))}
|
||||
/>
|
||||
</div>
|
||||
<div className="side-menu-margined w-full">
|
||||
<AssistRouter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE'])(Assist));
|
||||
export default withPageTitle("Assist - OpenReplay")(withPermissions(['ASSIST_LIVE'])(withRouter(Assist)));
|
||||
|
|
|
|||
39
frontend/app/components/Assist/AssistRouter.tsx
Normal file
39
frontend/app/components/Assist/AssistRouter.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
assist,
|
||||
recordings,
|
||||
withSiteId,
|
||||
} from 'App/routes';
|
||||
import AssistView from './AssistView'
|
||||
import Recordings from './RecordingsList/Recordings'
|
||||
|
||||
interface Props extends RouteComponentProps {
|
||||
match: any;
|
||||
}
|
||||
|
||||
function AssistRouter(props: Props) {
|
||||
const {
|
||||
match: {
|
||||
params: { siteId },
|
||||
},
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact strict path={withSiteId(assist(), siteId)}>
|
||||
<AssistView />
|
||||
</Route>
|
||||
|
||||
<Route exact strict path={withSiteId(recordings(), siteId)}>
|
||||
<Recordings />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(AssistRouter);
|
||||
18
frontend/app/components/Assist/AssistView.tsx
Normal file
18
frontend/app/components/Assist/AssistView.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import LiveSessionList from 'Shared/LiveSessionList';
|
||||
import LiveSessionSearch from 'Shared/LiveSessionSearch';
|
||||
import cn from 'classnames'
|
||||
import AssistSearchField from './AssistSearchField';
|
||||
|
||||
function AssistView() {
|
||||
return (
|
||||
<div className={cn("w-full mx-auto")} style={{ maxWidth: '1300px'}}>
|
||||
<AssistSearchField />
|
||||
<LiveSessionSearch />
|
||||
<div className="my-4" />
|
||||
<LiveSessionList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssistView;
|
||||
25
frontend/app/components/Assist/RecordingsList/Recordings.tsx
Normal file
25
frontend/app/components/Assist/RecordingsList/Recordings.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import React from 'react'
|
||||
import { PageTitle } from 'UI';
|
||||
import RecordingsSearch from './RecordingsSearch';
|
||||
import RecordingsList from './RecordingsList';
|
||||
|
||||
function Recordings() {
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: '1300px', margin: 'auto'}} className="bg-white rounded py-4 border">
|
||||
<div className="flex items-center mb-4 justify-between px-6">
|
||||
<div className="flex items-baseline mr-3">
|
||||
<PageTitle title="Recordings" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<div className="ml-4 w-1/4" style={{ minWidth: 300 }}>
|
||||
<RecordingsSearch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RecordingsList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Recordings
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import { NoContent, Pagination, Icon } from 'UI';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { filterList } from 'App/utils';
|
||||
import { sliceListPerPage } from 'App/utils';
|
||||
import RecordsListItem from './RecordsListItem';
|
||||
|
||||
function RecordingsList() {
|
||||
const { recordingsStore } = useStore();
|
||||
const [shownRecordings, setRecordings] = React.useState([]);
|
||||
const recordings = recordingsStore.recordings;
|
||||
const recordsSearch = recordingsStore.search;
|
||||
|
||||
React.useEffect(() => {
|
||||
recordingsStore.fetchRecordings()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
setRecordings(filterList(recordings, recordsSearch, ['createdBy', 'name']));
|
||||
}, [recordsSearch]);
|
||||
|
||||
const list = recordsSearch !== '' ? shownRecordings : recordings;
|
||||
const lenth = list.length;
|
||||
|
||||
return (
|
||||
<NoContent
|
||||
show={lenth === 0}
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<Icon name="no-dashboard" size={80} color="figmaColors-accent-secondary" />
|
||||
<div className="text-center text-gray-600 my-4">
|
||||
{recordsSearch !== ''
|
||||
? 'No matching results'
|
||||
: "You haven't created any recordings yet"}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="mt-3 border-b">
|
||||
<div className="grid grid-cols-12 py-2 font-medium px-6">
|
||||
<div className="col-span-8">Title</div>
|
||||
<div className="col-span-4">By</div>
|
||||
</div>
|
||||
|
||||
{sliceListPerPage(list, recordingsStore.page - 1, recordingsStore.pageSize).map(
|
||||
(record: any) => (
|
||||
<React.Fragment key={record.recordId}>
|
||||
<RecordsListItem record={record} />
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full flex items-center justify-between pt-4 px-6">
|
||||
<div className="text-disabled-text">
|
||||
Showing{' '}
|
||||
<span className="font-semibold">{Math.min(list.length, recordingsStore.pageSize)}</span>{' '}
|
||||
out of <span className="font-semibold">{list.length}</span> Recording
|
||||
</div>
|
||||
<Pagination
|
||||
page={recordingsStore.page}
|
||||
totalPages={Math.ceil(lenth / recordingsStore.pageSize)}
|
||||
onPageChange={(page) => recordingsStore.updatePage(page)}
|
||||
limit={recordingsStore.pageSize}
|
||||
debounceRequest={100}
|
||||
/>
|
||||
</div>
|
||||
</NoContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(RecordingsList);
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Icon } from 'UI';
|
||||
import { debounce } from 'App/utils';
|
||||
|
||||
let debounceUpdate: any = () => {}
|
||||
|
||||
function RecordingsSearch() {
|
||||
const { recordingsStore } = useStore();
|
||||
const [query, setQuery] = useState(recordingsStore.search);
|
||||
useEffect(() => {
|
||||
debounceUpdate = debounce((value: any) => recordingsStore.updateSearch(value), 500);
|
||||
}, [])
|
||||
|
||||
// @ts-ignore
|
||||
const write = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceUpdate(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute top-0 bottom-0 ml-2 m-auto" size="16" />
|
||||
<input
|
||||
value={query}
|
||||
name="recordsSearch"
|
||||
className="bg-white p-2 border border-borderColor-gray-light-shade rounded w-full pl-10"
|
||||
placeholder="Filter by title or description"
|
||||
onChange={write}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(RecordingsSearch);
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import React from 'react';
|
||||
import { Icon, ItemMenu } from 'UI';
|
||||
import { durationFromMs, checkForRecent, getDateFromMill } from 'App/date'
|
||||
import { IRecord } from 'App/services/RecordingsService'
|
||||
import { useStore } from 'App/mstore'
|
||||
import { toast } from 'react-toastify'
|
||||
|
||||
interface Props {
|
||||
record: IRecord;
|
||||
}
|
||||
|
||||
function RecordsListItem(props: Props) {
|
||||
const { record } = props;
|
||||
const { recordingsStore } = useStore()
|
||||
|
||||
const onRecordClick = () => {
|
||||
recordingsStore.fetchRecordingUrl(record.recordId).then(url => {
|
||||
window.open(url, "_blank");
|
||||
})
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
recordingsStore.deleteRecording(record.recordId).then(() => {
|
||||
toast.success("Recording deleted")
|
||||
})
|
||||
}
|
||||
|
||||
const menuItems = [
|
||||
{ icon: 'trash', text: 'Delete', onClick: onDelete },
|
||||
]
|
||||
return (
|
||||
<div className="hover:bg-active-blue border-t px-6">
|
||||
<div className="grid grid-cols-12 py-4 select-none items-center">
|
||||
<div className="col-span-8 flex items-start">
|
||||
<div className="flex items-center capitalize-first">
|
||||
<div className="w-9 h-9 rounded-full bg-tealx-lightest flex items-center justify-center mr-2">
|
||||
<Icon name="columns-gap" size="16" color="tealx" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="border-b border-dashed">{record.name}</div>
|
||||
<div className="text-gray-medium text-sm">
|
||||
{durationFromMs(record.duration)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{record.createdBy}
|
||||
</div>
|
||||
<div className="text-gray-medium text-sm">{checkForRecent(getDateFromMill(record.createdAt), 'LLL dd, yyyy, hh:mm a')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 w-full justify-end flex items-center gap-2">
|
||||
<div className="group flex items-center gap-1 cursor-pointer link" onClick={onRecordClick}>
|
||||
<Icon name="play" size={18} color="teal" className="!block group-hover:!hidden"/>
|
||||
<Icon name="play-fill-new" size={18} color="teal" className="!hidden group-hover:!block" />
|
||||
<div>Play Video</div>
|
||||
</div>
|
||||
<ItemMenu
|
||||
bold
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecordsListItem;
|
||||
|
|
@ -44,6 +44,7 @@ function ScreenRecorder({
|
|||
React.useEffect(() => {
|
||||
return () => stopRecorderCb?.();
|
||||
}, []);
|
||||
|
||||
const onSave = async (saveObj: { name: string; duration: number }, blob: Blob) => {
|
||||
try {
|
||||
const url = await recordingsService.reserveUrl(siteId, saveObj);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
|
||||
import { DateTime, Duration } from 'luxon'; // TODO
|
||||
import { toJS } from 'mobx';
|
||||
import { Timezone } from 'MOBX/types/sessionSettings';
|
||||
|
||||
export const durationFormatted = (duration: Duration):string => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
funnelService,
|
||||
errorService,
|
||||
notesService,
|
||||
recordingsService,
|
||||
} from 'App/services';
|
||||
import SettingsStore from './settingsStore';
|
||||
import AuditStore from './auditStore';
|
||||
|
|
@ -22,6 +23,7 @@ import ErrorStore from './errorStore';
|
|||
import SessionStore from './sessionStore';
|
||||
import NotesStore from './notesStore';
|
||||
import BugReportStore from './bugReportStore'
|
||||
import RecordingsStore from './recordingsStore'
|
||||
|
||||
export class RootStore {
|
||||
dashboardStore: DashboardStore;
|
||||
|
|
@ -35,7 +37,8 @@ export class RootStore {
|
|||
notificationStore: NotificationStore;
|
||||
sessionStore: SessionStore;
|
||||
notesStore: NotesStore;
|
||||
bugReportStore: BugReportStore
|
||||
bugReportStore: BugReportStore;
|
||||
recordingsStore: RecordingsStore;
|
||||
|
||||
constructor() {
|
||||
this.dashboardStore = new DashboardStore();
|
||||
|
|
@ -50,6 +53,7 @@ export class RootStore {
|
|||
this.sessionStore = new SessionStore();
|
||||
this.notesStore = new NotesStore();
|
||||
this.bugReportStore = new BugReportStore();
|
||||
this.recordingsStore = new RecordingsStore();
|
||||
}
|
||||
|
||||
initClient() {
|
||||
|
|
@ -62,6 +66,7 @@ export class RootStore {
|
|||
auditService.initClient(client);
|
||||
errorService.initClient(client);
|
||||
notesService.initClient(client)
|
||||
recordingsService.initClient(client);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
74
frontend/app/mstore/recordingsStore.ts
Normal file
74
frontend/app/mstore/recordingsStore.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { makeAutoObservable } from "mobx"
|
||||
import { recordingsService } from "App/services"
|
||||
import { IRecord } from 'App/services/RecordingsService'
|
||||
|
||||
export default class RecordingsStore {
|
||||
recordings: IRecord[] = []
|
||||
loading: boolean
|
||||
|
||||
page = 1
|
||||
pageSize = 15
|
||||
order: 'desc' | 'asc' = 'desc'
|
||||
search = ''
|
||||
// not later we will add search by user id
|
||||
userId: number
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
updateSearch(val: string) {
|
||||
this.search = val
|
||||
}
|
||||
updatePage(page: number) {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
async fetchRecordings() {
|
||||
const filter = {
|
||||
page: this.page,
|
||||
limit: this.pageSize,
|
||||
order: this.order,
|
||||
search: this.search,
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
try {
|
||||
const recordings = await recordingsService.fetchRecordings(filter)
|
||||
this.recordings = recordings;
|
||||
this.fetchRecordingUrl(recordings[0].recordId)
|
||||
return recordings;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async fetchRecordingUrl(id: number): Promise<string> {
|
||||
this.loading = true
|
||||
try {
|
||||
const recording = await recordingsService.fetchRecording(id)
|
||||
return recording.URL;
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRecording(id: number) {
|
||||
this.loading = true
|
||||
try {
|
||||
const recording = await recordingsService.deleteRecording(id)
|
||||
console.log(recording)
|
||||
return recording
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -283,7 +283,6 @@ export default class AssistManager {
|
|||
const recordingState = getState().recordingState
|
||||
if (!this.socket || recordingState === SessionRecordingStatus.Off) return;
|
||||
|
||||
console.log('stop rec')
|
||||
this.socket.emit("stop_recording")
|
||||
this.toggleRecording(false)
|
||||
}
|
||||
|
|
@ -292,7 +291,6 @@ export default class AssistManager {
|
|||
this.md.toggleRecordingStatus(isAccepted)
|
||||
|
||||
update({ recordingState: isAccepted ? SessionRecordingStatus.Recording : SessionRecordingStatus.Off })
|
||||
console.log('Im here', isAccepted, getState().recordingState)
|
||||
}
|
||||
|
||||
/* ==== Remote Control ==== */
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ export const loadFiles = (
|
|||
return processAPIStreamResponse(r, true)
|
||||
})
|
||||
.then(onData)
|
||||
.then(() =>
|
||||
urls.reduce((p, url) =>
|
||||
.then(() =>
|
||||
urls.reduce((p, url) =>
|
||||
p.then(() =>
|
||||
window.fetch(url)
|
||||
.then(r => {
|
||||
|
|
@ -62,7 +62,7 @@ const processAPIStreamResponse = (response: Response, isMainFile: boolean) => {
|
|||
}
|
||||
if (response.status >= 400) {
|
||||
return rej(
|
||||
isMainFile ? `no start file. status code ${ response.status }`
|
||||
isMainFile ? `no start file. status code ${ response.status }`
|
||||
: `Bad endfile status code ${response.status}`
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export const onboarding = (tab = routerOBTabString) => `/onboarding/${ tab }`;
|
|||
|
||||
export const sessions = params => queried('/sessions', params);
|
||||
export const assist = params => queried('/assist', params);
|
||||
export const recordings = params => queried("/recordings", params);
|
||||
|
||||
export const session = (sessionId = ':sessionId', hash) => hashed(`/session/${ sessionId }`, hash);
|
||||
export const liveSession = (sessionId = ':sessionId', params, hash) => hashed(queried(`/assist/${ sessionId }`, params), hash);
|
||||
|
|
@ -123,7 +124,8 @@ const REQUIRED_SITE_ID_ROUTES = [
|
|||
session(''),
|
||||
sessions(),
|
||||
assist(),
|
||||
|
||||
recordings(),
|
||||
|
||||
metrics(),
|
||||
metricDetails(''),
|
||||
metricDetailsSub(''),
|
||||
|
|
@ -172,6 +174,7 @@ const SITE_CHANGE_AVALIABLE_ROUTES = [
|
|||
sessions(),
|
||||
funnels(),
|
||||
assist(),
|
||||
recordings(),
|
||||
dashboard(),
|
||||
dashboardSelected(),
|
||||
metrics(),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@ interface RecordingData {
|
|||
duration: number;
|
||||
}
|
||||
|
||||
interface FetchFilter {
|
||||
page: number
|
||||
limit: number
|
||||
order: 'asc' | 'desc'
|
||||
search: string
|
||||
}
|
||||
|
||||
export interface IRecord {
|
||||
createdAt: number
|
||||
createdBy: string
|
||||
duration: number
|
||||
name: string
|
||||
recordId: number
|
||||
sessionId: number
|
||||
userId: number
|
||||
URL?: string
|
||||
}
|
||||
|
||||
export default class RecordingsService {
|
||||
private client: APIClient;
|
||||
|
||||
|
|
@ -38,4 +56,37 @@ export default class RecordingsService {
|
|||
})
|
||||
}
|
||||
|
||||
fetchRecordings(filters: FetchFilter): Promise<IRecord[]> {
|
||||
return this.client.post(`/assist/records`, filters)
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(j => j.data)
|
||||
} else {
|
||||
throw new Error("Can't get recordings: " + r.status);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fetchRecording(id: number): Promise<IRecord> {
|
||||
return this.client.get(`/assist/records/${id}`)
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(j => j.data)
|
||||
} else {
|
||||
throw new Error("Can't get recordings: " + r.status);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
deleteRecording(id: number): Promise<any> {
|
||||
return this.client.delete(`/assist/records/${id}`)
|
||||
.then(r => {
|
||||
if (r.ok) {
|
||||
return r.json().then(j => j.data)
|
||||
} else {
|
||||
throw new Error("Can't get recordings: " + r.status);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,11 @@ export async function screenRecorder(recName: string, sessionId: string, saveCb:
|
|||
const stream = await recordScreen();
|
||||
const mediaRecorder = createFileRecorder(stream, FILE_TYPE, recName, sessionId, saveCb);
|
||||
|
||||
return () => mediaRecorder.stop();
|
||||
return () => {
|
||||
if (mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue