rrweb extension implementation (#1044)
* feat: add rrweb web-extension package * refactor: make the extension suitable for manifest v3 * update tsconfig.json * use version_name rather than recorder_version in manifest.json * update manifest.json * enable to keep recording after changing tabs * enable to record between tabs and urls * fix CI error * try to fix CI error * feat: add pause and resume buttons * feat: add a link to new session after recording * improve session list * refactor: migrate session storage from chrome local storage to indexedDB * feat: add pagination to session list * fix: multiple recorders are started after pausing and resuming process * fix: can't stop recording on firefox browser * update type import of 'eventWithTime' * fix CI error * doc: add readme * Apply suggestions from Justin's code review Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com> * refactor: make use of webNavigation API to implement recording consistent during page navigation * fix firefox compatibility issue and add title to pages * add mouseleave listener to enhance the recording liability * fix firefox compatibility issue and improve the experience of recording resume after closing tabs * update tsconfig * upgrade vite-plugin-web-extension config to fix some bugs on facebook web page * update import links * refactor: cross tab recording mechanism apply Justin's suggestion * refactor: slipt util/index.ts into multiple files * implement cross-origin iframe recording * fix: regression of issue: ShadowHost can't be a string (issue 941) * refactor shadow dom recording to make tests cover key code * Apply formatting changes * increase the node memory limitation to avoid CI failure * Create lovely-pears-cross.md * Apply formatting changes * Update packages/web-extension/package.json * Update .changeset/lovely-pears-cross.md * update change logs * delete duplicated property --------- Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
This commit is contained in:
318
packages/web-extension/src/pages/SessionList.tsx
Normal file
318
packages/web-extension/src/pages/SessionList.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
chakra,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
Text,
|
||||
TableContainer,
|
||||
Flex,
|
||||
Checkbox,
|
||||
Button,
|
||||
Spacer,
|
||||
IconButton,
|
||||
Select,
|
||||
Input,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
createColumnHelper,
|
||||
useReactTable,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
} from '@tanstack/react-table';
|
||||
import { VscTriangleDown, VscTriangleUp } from 'react-icons/vsc';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Session, EventName } from '~/types';
|
||||
import Channel from '~/utils/channel';
|
||||
import { deleteSessions, getAllSessions } from '~/utils/storage';
|
||||
import {
|
||||
FiChevronLeft,
|
||||
FiChevronRight,
|
||||
FiChevronsLeft,
|
||||
FiChevronsRight,
|
||||
} from 'react-icons/fi';
|
||||
|
||||
const columnHelper = createColumnHelper<Session>();
|
||||
const channel = new Channel();
|
||||
|
||||
export function SessionList() {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: 'createTimestamp',
|
||||
desc: true,
|
||||
},
|
||||
]);
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
|
||||
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const fetchDataOptions = {
|
||||
pageIndex,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
const fetchData = (options: { pageIndex: number; pageSize: number }) => {
|
||||
return {
|
||||
rows: sessions.slice(
|
||||
options.pageIndex * options.pageSize,
|
||||
(options.pageIndex + 1) * options.pageSize,
|
||||
),
|
||||
pageCount: Math.ceil(sessions.length / options.pageSize),
|
||||
};
|
||||
};
|
||||
const pagination = useMemo(
|
||||
() => ({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
}),
|
||||
[pageIndex, pageSize],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.display({
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
isChecked={table.getIsAllRowsSelected()}
|
||||
isIndeterminate={table.getIsSomeRowsSelected()}
|
||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
isChecked={row.getIsSelected()}
|
||||
isIndeterminate={row.getIsSomeSelected()}
|
||||
onChange={row.getToggleSelectedHandler()}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor((row) => row.name, {
|
||||
cell: (info) => info.getValue(),
|
||||
header: 'Name',
|
||||
}),
|
||||
columnHelper.accessor((row) => row.createTimestamp, {
|
||||
id: 'createTimestamp',
|
||||
cell: (info) => new Date(info.getValue()).toLocaleString(),
|
||||
header: 'Created Time',
|
||||
sortDescFirst: true,
|
||||
}),
|
||||
columnHelper.accessor((row) => row.recorderVersion, {
|
||||
cell: (info) => info.getValue(),
|
||||
header: 'RRWEB Version',
|
||||
}),
|
||||
],
|
||||
[],
|
||||
);
|
||||
const table = useReactTable<Session>({
|
||||
columns,
|
||||
data: fetchData(fetchDataOptions).rows,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
state: {
|
||||
pagination,
|
||||
sorting,
|
||||
rowSelection,
|
||||
},
|
||||
manualPagination: true,
|
||||
pageCount: fetchData(fetchDataOptions).pageCount,
|
||||
});
|
||||
|
||||
const updateSessions = async () => {
|
||||
const sessions = await getAllSessions();
|
||||
setSessions(sessions);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void updateSessions();
|
||||
channel.on(EventName.SessionUpdated, () => {
|
||||
void updateSessions();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableContainer fontSize="md">
|
||||
<Table variant="simple">
|
||||
<Thead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<Tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const meta = header.column.columnDef.meta as
|
||||
| {
|
||||
isNumeric: boolean;
|
||||
}
|
||||
| undefined;
|
||||
return (
|
||||
<Th
|
||||
key={header.id}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
isNumeric={meta?.isNumeric}
|
||||
verticalAlign="center"
|
||||
userSelect="none"
|
||||
>
|
||||
<Flex align="center">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
<chakra.span pl={4}>
|
||||
{{
|
||||
asc: (
|
||||
<VscTriangleUp aria-label="sorted ascending" />
|
||||
),
|
||||
desc: (
|
||||
<VscTriangleDown aria-label="sorted descending" />
|
||||
),
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</chakra.span>
|
||||
</Flex>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
))}
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<Tr key={row.id} _hover={{ cursor: 'pointer' }}>
|
||||
{row.getVisibleCells().map((cell, index) => {
|
||||
const meta = cell.column.columnDef.meta as
|
||||
| {
|
||||
isNumeric: boolean;
|
||||
}
|
||||
| undefined;
|
||||
return (
|
||||
<Td
|
||||
key={cell.id}
|
||||
isNumeric={meta?.isNumeric}
|
||||
onClick={() => {
|
||||
if (index !== 0)
|
||||
navigate(`/session/${row.original.id}`);
|
||||
}}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Flex mt={4}>
|
||||
<Flex gap={16} align="center" ml={4}>
|
||||
<Flex gap={1}>
|
||||
<IconButton
|
||||
aria-label={'Goto 1st Page'}
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<FiChevronsLeft />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={'Goto Previous Page'}
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<FiChevronLeft />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
<Flex gap={1} fontSize="md">
|
||||
<Text>Page</Text>
|
||||
<Text as="b" w={12}>
|
||||
{`${
|
||||
table.getState().pagination.pageIndex + 1
|
||||
} of ${table.getPageCount()}`}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Divider orientation="vertical" />
|
||||
<Flex gap={1} justify="center" align="center" fontSize="md">
|
||||
<Text w={28}>Go to page:</Text>
|
||||
<Input
|
||||
w={20}
|
||||
size="md"
|
||||
type="number"
|
||||
defaultValue={table.getState().pagination.pageIndex + 1}
|
||||
onChange={(e) => {
|
||||
const page = e.target.value ? Number(e.target.value) - 1 : 0;
|
||||
table.setPageIndex(page);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex gap={1}>
|
||||
<IconButton
|
||||
aria-label={'Goto Next Page'}
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<FiChevronRight />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label={'Goto last Page'}
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<FiChevronsRight />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Spacer />
|
||||
<Flex gap={8} align="center" mr={4}>
|
||||
<Select
|
||||
variant="outline"
|
||||
size="md"
|
||||
value={table.getState().pagination.pageSize}
|
||||
onChange={(e) => {
|
||||
table.setPageSize(Number(e.target.value));
|
||||
}}
|
||||
>
|
||||
{[10, 20, 30, 40, 50].map((pageSize) => (
|
||||
<option key={pageSize} value={pageSize}>
|
||||
Show {pageSize} items
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{Object.keys(rowSelection).length > 0 && (
|
||||
<Button
|
||||
mr={4}
|
||||
size="md"
|
||||
colorScheme="red"
|
||||
onClick={() => {
|
||||
if (table.getSelectedRowModel().flatRows.length === 0) return;
|
||||
const ids = table
|
||||
.getSelectedRowModel()
|
||||
.flatRows.map((row) => row.original.id);
|
||||
void deleteSessions(ids).then(() => {
|
||||
setRowSelection({});
|
||||
void updateSessions();
|
||||
channel.emit(EventName.SessionUpdated, {});
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user