Files
rrweb/packages/web-extension/src/pages/SessionList.tsx
Yun Feng 282c8fa415 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>
2023-02-14 07:15:34 +08:00

319 lines
9.2 KiB
TypeScript

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>
</>
);
}