diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4f1856f1..9f680d3e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@mantine/hooks": "^7.17.2", "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", + "@tanstack/react-table": "^8.21.2", "allotment": "^1.20.3", "axios": "^1.8.2", "clsx": "^2.1.1", @@ -31,6 +32,7 @@ "react-draggable": "^4.4.6", "react-pro-sidebar": "^1.1.0", "react-router-dom": "^7.3.0", + "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", "recharts": "^2.15.1", "video.js": "^8.21.0", @@ -1741,12 +1743,12 @@ } }, "node_modules/@tanstack/react-table": { - "version": "8.20.5", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", - "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz", + "integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.20.5" + "@tanstack/table-core": "8.21.2" }, "engines": { "node": ">=12" @@ -1778,9 +1780,9 @@ } }, "node_modules/@tanstack/table-core": { - "version": "8.20.5", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", - "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz", + "integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==", "license": "MIT", "engines": { "node": ">=12" @@ -3452,6 +3454,39 @@ "react-dom": ">=18.0" } }, + "node_modules/mantine-react-table/node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/mantine-react-table/node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4081,6 +4116,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz", + "integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==", + "license": "MIT", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-window": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5677558b..c0d6ced3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@mantine/hooks": "^7.17.2", "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", + "@tanstack/react-table": "^8.21.2", "allotment": "^1.20.3", "axios": "^1.8.2", "clsx": "^2.1.1", @@ -33,6 +34,7 @@ "react-draggable": "^4.4.6", "react-pro-sidebar": "^1.1.0", "react-router-dom": "^7.3.0", + "react-virtualized-auto-sizer": "^1.0.26", "react-window": "^1.8.11", "recharts": "^2.15.1", "video.js": "^8.21.0", diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 1d33565f..f9ab1cc3 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -122,7 +122,7 @@ export const WebsocketProvider = ({ children }) => { message: 'EPG match is complete!', color: 'green.5', }); - fetchChannels(); + // fetchChannels(); fetchEPGData(); break; diff --git a/frontend/src/api.js b/frontend/src/api.js index 92dc84e8..411a7f26 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -320,7 +320,7 @@ export default class API { }); // Optionally refesh the channel list in Zustand - await useChannelsStore.getState().fetchChannels(); + // await useChannelsStore.getState().fetchChannels(); return response; } catch (e) { @@ -604,7 +604,7 @@ export default class API { usePlaylistsStore.getState().removePlaylists([id]); // @TODO: MIGHT need to optimize this later if someone has thousands of channels // but I'm feeling laze right now - useChannelsStore.getState().fetchChannels(); + // useChannelsStore.getState().fetchChannels(); } catch (e) { errorNotification(`Failed to delete playlist ${id}`, e); } diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 4a6bc014..d3d4b868 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -5,7 +5,6 @@ import React, { useState, useCallback, } from 'react'; -import { MantineReactTable, useMantineReactTable } from 'mantine-react-table'; import useChannelsStore from '../../store/channels'; import { notifications } from '@mantine/notifications'; import API from '../../api'; @@ -55,7 +54,21 @@ import { MultiSelect, Pagination, NativeSelect, + Checkbox, + Table, } from '@mantine/core'; +import AutoSizer from 'react-virtualized-auto-sizer' +import { FixedSizeList as List } from 'react-window' +import ChannelsTableRow from './ChannelsTable/ChannelsTableRow'; +import ChannelsTableBody from './ChannelsTable/ChannelsTableBody'; +import { + flexRender, + useReactTable, + getCoreRowModel, + getSortedRowModel, + getPaginationRowModel, +} from '@tanstack/react-table' +import { notUndefined, useVirtualizer } from '@tanstack/react-virtual' const ChannelStreams = React.memo(({ channel, isExpanded }) => { const channelStreams = useChannelsStore( @@ -71,7 +84,7 @@ const ChannelStreams = React.memo(({ channel, isExpanded }) => { }); }; - const channelStreamsTable = useMantineReactTable({ + const channelStreamsTable = useReactTable({ ...TableHelper.defaultProperties, data: channelStreams, columns: useMemo( @@ -250,6 +263,160 @@ const CreateProfilePopover = React.memo(({ }) => { ); }); +const ChannelEnabledCell = ({ cell, row, toggleChannelEnabled, selectedProfileId }) => { + const handleSwitchChange = useCallback(() => { + toggleChannelEnabled([row.original.id], !cell.getValue()); + }, [cell.getValue(), row.original.id, toggleChannelEnabled]); + + return ( +
+ +
+ ); +} + +const ChannelLogoCell = React.memo(({ cell }) => { + return ( +
+ channel logo +
+ ) +}) + +const ChannelSelectCell = React.memo(({ + checked, + disabled, + indeterminate, + onChange, +}) => { + return ( +
+
+ +
+
+ ) +}) + +const RowActions = React.memo(({ + row, + editChannel, + deleteChannel, + handleWatchStream, + createRecording, +}) => { + const theme = useMantineTheme() + const { channelsPageSelection } = useChannelsStore() + + const onEdit = useCallback(() => { + editChannel(row.original) + }, [row]); + + const onDelete = useCallback(async () => { + deleteChannel(row.original) + }, [row]); + + const onRecord = useCallback(() => { + createRecording(row.original) + }, [row]); + + const onPreview = useCallback(() => { + handleWatchStream(row.original) + }, [row]); + + return ( + +
+ + + + + 0 && + !channelsPageSelection.map((row) => row.id).includes(row.id) + } + > + {channelsPageSelection.length === 0 ? ( + + ) : ( + + )} + + + + + + + {/* {env_mode == 'dev' && ( + + + + + + + + + + } + > + Record + + + + )} */} +
+
+ ); +}); + const ChannelsTable = React.memo(({ }) => { const { channels, @@ -277,17 +444,17 @@ const ChannelsTable = React.memo(({ }) => { ); const [channelsEnabledHeaderSwitch, setChannelsEnabledHeaderSwitch] = useState(false); - const [initialDataCount, setInitialDataCount] = useState(null); - const [data, setData] = useState([]); + const [data, setData] = useState([]); // Holds fetched data + const [selectedRowIds, setSelectedRowIds] = useState([]); const [rowCount, setRowCount] = useState(0); const [pageCount, setPageCount] = useState(0); const [paginationString, setPaginationString] = useState(''); - const [selectedStreamIds, setSelectedStreamIds] = useState([]); - // const [allRowsSelected, setAllRowsSelected] = useState(false); const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 250, }); + const [groupOptions, setGroupOptions] = useState([]); + const [initialDataCount, setInitialDataCount] = useState(null); const [filters, setFilters] = useState({ name: '', channel_group: '', @@ -295,6 +462,7 @@ const ChannelsTable = React.memo(({ }) => { }); const debouncedFilters = useDebounce(filters, 500); const [isLoading, setIsLoading] = useState(true); + const [selectedChannelIds, setSelectedChannelIds] = useState([]); const [sorting, setSorting] = useState([ { id: 'channel_number', desc: false }, { id: 'name', desc: false }, @@ -326,19 +494,23 @@ const ChannelsTable = React.memo(({ }) => { useEffect(() => { setChannelGroupOptions([ ...new Set( - Object.values(channels).map((channel) => channel.channel_group?.name) + Object.values(data).map((channel) => channel.channel_group?.name) ), ]); - }, [channels]); + }, [data]); - const handleFilterChange = (columnId, value) => { - setFilterValues((prev) => ({ + const handleFilterChange = (e) => { + const { name, value } = e.target; + setFilters((prev) => ({ ...prev, - [columnId]: Array.isArray(value) - ? value - : value - ? value.toLowerCase() - : '', + [name]: value, + })); + }; + + const handleGroupChange = (value) => { + setFilters((prev) => ({ + ...prev, + channel_group: value ? value : '', })); }; @@ -387,7 +559,7 @@ const ChannelsTable = React.memo(({ }) => { const newSelection = {}; result.results.forEach((item, index) => { - if (selectedStreamIds.includes(item.id)) { + if (selectedChannelIds.includes(item.id)) { newSelection[index] = true; } }); @@ -412,7 +584,7 @@ const ChannelsTable = React.memo(({ }) => { const newRowSelection = typeof updater === 'function' ? updater(prevRowSelection) : updater; - const updatedSelected = new Set([...selectedStreamIds]); + const updatedSelected = new Set([...selectedChannelIds]); table.getRowModel().rows.forEach((row) => { if (newRowSelection[row.id] === undefined || !newRowSelection[row.id]) { updatedSelected.delete(row.original.id); @@ -420,7 +592,7 @@ const ChannelsTable = React.memo(({ }) => { updatedSelected.add(row.original.id); } }); - setSelectedStreamIds([...updatedSelected]); + setSelectedChannelIds([...updatedSelected]); return newRowSelection; }); @@ -435,9 +607,9 @@ const ChannelsTable = React.memo(({ }) => { if (value) params.append(key, value); }); const ids = await API.getAllStreamIds(params); - setSelectedStreamIds(ids); + setSelectedChannelIds(ids); } else { - setSelectedStreamIds([]); + setSelectedChannelIds([]); } const newSelection = {}; @@ -489,9 +661,13 @@ const ChannelsTable = React.memo(({ }) => { /> )); - const renderEnabledHeader = useCallback(() => { + const renderEnabledHeader = useCallback(({ header }) => { if (Object.values(rowSelection).length === 0) { - return ; + return ( +
+ +
+ ); } const handleToggle = () => { @@ -518,109 +694,82 @@ const ChannelsTable = React.memo(({ }) => { // Configure columns const columns = useMemo( () => [ + { + id: 'select', + size: 20, + meta: { + minWidth: 20, + maxWidth: 20, + }, + header: ({ table }) => ( +
+ +
+ ), + cell: ({ row }) => ( + + ), + }, { id: 'enabled', - Header: renderEnabledHeader, - enableSorting: false, + size: 32, + meta: { + minWidth: 32, + maxWidth: 32, + }, + header: renderEnabledHeader, accessorFn: (row) => { return selectedProfileId == '0' ? true : enabledChannelSet.has(row.id); }, - mantineTableHeadCellProps: { - align: 'right', - style: { - backgroundColor: '#3F3F46', - width: '40px', - minWidth: '40px', - maxWidth: '40px', - // // minWidth: '20px', - // // width: '50px !important', - // // justifyContent: 'center', - padding: 0, - // // paddingLeft: 8, - // // paddingRight: 0, - }, - }, - mantineTableBodyCellProps: { - align: 'right', - style: { - width: '40px', - minWidth: '40px', - maxWidth: '40px', - // // minWidth: '20px', - // // justifyContent: 'center', - // // paddingLeft: 0, - // // paddingRight: 0, - padding: 0, - }, - }, - Cell: ({ row, cell }) => { - const memoizedCellValue = useMemo(() => cell.getValue(), [cell]); - const handleSwitchChange = useCallback(() => { - toggleChannelEnabled([row.original.id], !memoizedCellValue); - }, [memoizedCellValue, row.original.id, toggleChannelEnabled]); - - return ( - - ); - }, + cell: ({ row, cell }) => ( + + ), }, { - header: '#', - size: 50, - maxSize: 50, + size: 26, + maxSize: 26, accessorKey: 'channel_number', - sortingFn: (a, b, columnId) => { - return ( - parseInt(a.original.channel_number) - - parseInt(b.original.channel_number) - ); - }, - mantineTableHeadCellProps: { - align: 'right', - // // style: { - // // backgroundColor: '#3F3F46', - // // // minWidth: '20px', - // // // justifyContent: 'center', - // // // paddingLeft: 15, - // // paddingRight: 0, - // // }, - }, - mantineTableBodyCellProps: { - align: 'right', - // // style: { - // // minWidth: '20px', - // // // justifyContent: 'center', - // // paddingLeft: 0, - // // paddingRight: 0, - // // }, + header: ({ header }) => ( +
#
+ ), + meta: { + align: 'right' }, + // cell: ({ cell }) => ( + // + // {cell.getValue()} + // + // ) }, { - header: 'Name', accessorKey: 'name', - Header: ({ column }) => ( + header: ({ column }) => ( { - e.stopPropagation(); - handleFilterChange(column.id, e.target.value); - }} + onClick={(e) => e.stopPropagation()} + onChange={handleFilterChange} size="xs" variant="unstyled" className="table-input-header" - onClick={(e) => e.stopPropagation()} /> ), - Cell: ({ cell }) => ( + cell: ({ cell }) => (
{ ), }, { - header: 'Group', accessorKey: 'channel_group.name', accessorFn: (row) => row.channel_group?.name || '', - Cell: ({ cell }) => ( + cell: ({ cell }) => (
{ {cell.getValue()}
), - Header: ({ column }) => ( + header: ({ column }) => ( e.stopPropagation()}> { - handleFilterChange(column.id, value); - }} + onChange={handleGroupChange} data={channelGroupOptions} variant="unstyled" className="table-input-header custom-multiselect" @@ -675,27 +821,23 @@ const ChannelsTable = React.memo(({ }) => { maxWidth: '75px', }, }, - Cell: ({ cell }) => ( - - channel logo - + cell: ({ cell }) => ( + ), }, + { + header: 'Actions', + size: 40, + cell: ({ row }) => ( + + ) + } ], [ channelGroupOptions, @@ -703,8 +845,7 @@ const ChannelsTable = React.memo(({ }) => { selectedProfile, selectedProfileChannels, rowSelection, - channelsPageSelection, - channelsEnabledHeaderSwitch, + // channelsEnabledHeaderSwitch, ] ); @@ -716,6 +857,7 @@ const ChannelsTable = React.memo(({ }) => { .rows.filter((row) => row.getIsSelected()); await API.deleteChannels(selected.map((row) => row.original.id)); + fetchData(); setIsLoading(false); }; @@ -739,7 +881,7 @@ const ChannelsTable = React.memo(({ }) => { }); // Refresh the channel list - await fetchChannels(); + // await fetchChannels(); } catch (err) { console.error(err); notifications.show({ @@ -811,41 +953,26 @@ const ChannelsTable = React.memo(({ }) => { handleCopy(hdhrUrl, hdhrUrlRef); }; + // useEffect(() => { + // const selectedRows = table + // .getSelectedRowModel() + // .rows.map((row) => row.original); + // setChannelsPageSelection(selectedRows); + + // if (selectedProfileId != '0') { + // setChannelsEnabledHeaderSwitch( + // selectedRows.filter( + // (row) => + // selectedProfileChannels.find((channel) => row.id == channel.id) + // .enabled + // ).length == selectedRows.length + // ); + // } + // }, [rowSelection]) + useEffect(() => { - const selectedRows = table - .getSelectedRowModel() - .rows.map((row) => row.original); - setChannelsPageSelection(selectedRows); - - if (selectedProfileId != '0') { - setChannelsEnabledHeaderSwitch( - selectedRows.filter( - (row) => - selectedProfileChannels.find((channel) => row.id == channel.id) - .enabled - ).length == selectedRows.length - ); - } - }, [rowSelection]); - - const filteredData = Object.values(channels).filter((row) => - columns.every(({ accessorKey }) => { - if (!accessorKey) { - return true; - } - - const filterValue = filterValues[accessorKey]; - const rowValue = getDescendantProp(row, accessorKey); - - if (Array.isArray(filterValue) && filterValue.length != 0) { - return filterValue.includes(rowValue); - } else if (filterValue) { - return rowValue?.toLowerCase().includes(filterValues[accessorKey]); - } - - return true; - }) - ); + fetchData(); + }, [fetchData]); const deleteProfile = async (id) => { await API.deleteChannelProfile(id); @@ -872,257 +999,71 @@ const ChannelsTable = React.memo(({ }) => { ); }; - const RowActions = React.memo(({ row }) => { - const editChannel = useCallback(() => { - setChannel(row.original); - setChannelModalOpen(true); - }, []); + const editChannel = useCallback((row) => { + setChannel(row.original); + setChannelModalOpen(true); + }, []); - const deleteChannel = useCallback(async () => { - setRowSelection([]); - // if (channelsPageSelection.length > 0) { - // return deleteChannels(); - // } - await API.deleteChannel(row.id); - }, []); + const deleteChannel = useCallback(async (row) => { + console.log(row) + setRowSelection([]); + // if (channelsPageSelection.length > 0) { + // return deleteChannels(); + // } + await API.deleteChannel(row.id); + }, []); - const createRecording = useCallback(() => { - setChannel(row); - setRecordingModalOpen(true); - }, []); + const createRecording = useCallback((row) => { + setChannel(row); + setRecordingModalOpen(true); + }, []); - const handleWatchStream = useCallback(() => { - let vidUrl = `/proxy/ts/stream/${row.uuid}`; - if (env_mode == 'dev') { - vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; - } - showVideo(vidUrl); - }, []); + const handleWatchStream = useCallback((row) => { + let vidUrl = `/proxy/ts/stream/${row.uuid}`; + if (env_mode == 'dev') { + vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; + } + showVideo(vidUrl); + }, []); - return ( - -
- - - - - 0 && - !channelsPageSelection.map((row) => row.id).includes(row.id) - } - > - {channelsPageSelection.length === 0 ? ( - - ) : ( - - )} - - - - - - - {env_mode == 'dev' && ( - - - - - - - - -
- } - > - Record - - - - )} - - - ); - }); - - const table = useMantineReactTable({ - ...TableHelper.defaultProperties, - columns, + const table = useReactTable({ data, - enablePagination: true, - manualPagination: true, - enableColumnActions: false, + columns, + // filterFns: {}, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + // getPaginationRowModel: getPaginationRowModel(), + // manualPagination: true, enableRowSelection: true, - renderTopToolbar: false, - onRowSelectionChange: onRowSelectionChange, - onSortingChange: setSorting, - state: { - isLoading: isLoading || channelsLoading, - sorting, - rowSelection, - }, - enableBottomToolbar: true, - renderBottomToolbar: ({ table }) => ( - - Page Size - - - {paginationString} - - ), - initialState: { - density: 'compact', - sorting: [ - { - id: 'channel_number', - desc: false, - }, - ], - }, - enableRowActions: true, - enableExpandAll: false, - displayColumnDefOptions: { - 'mrt-row-select': { - size: 10, - maxSize: 10, - mantineTableHeadCellProps: { - align: 'right', - style: { - paddding: 0, - // paddingLeft: 7, - width: '20px', - minWidth: '20px', - backgroundColor: '#3F3F46', - }, - }, - mantineTableBodyCellProps: { - align: 'right', - style: { - paddingLeft: 0, - width: '20px', - minWidth: '20px', - }, - }, - }, - 'mrt-row-expand': { - size: 20, - maxSize: 20, - header: '', - mantineTableHeadCellProps: { - style: { - padding: 0, - paddingLeft: 2, - width: '20px', - minWidth: '20px', - maxWidth: '20px', - backgroundColor: '#3F3F46', - }, - }, - mantineTableBodyCellProps: { - style: { - padding: 0, - paddingLeft: 2, - width: '20px', - minWidth: '20px', - maxWidth: '20px', - }, - }, - }, - 'mrt-row-actions': { - size: 85, - maxWidth: 85, - mantineTableHeadCellProps: { - align: 'center', - style: { - minWidth: '85px', - maxWidth: '85px', - // paddingRight: 40, - fontWeight: 'normal', - color: 'rgb(207,207,207)', - backgroundColor: '#3F3F46', - }, - }, - mantineTableBodyCellProps: { - style: { - minWidth: '85px', - maxWidth: '85px', - paddingLeft: 0, - // paddingRight: 10, - }, - }, - }, - }, - mantineExpandButtonProps: ({ row, table }) => ({ - onClick: () => { - setRowSelection({ [row.index]: true }); - table.setExpanded({ [row.id]: !row.getIsExpanded() }); - }, - size: 'xs', - style: { - transform: row.getIsExpanded() ? 'rotate(180deg)' : 'rotate(-90deg)', - transition: 'transform 0.2s', - }, - }), - renderDetailPanel: ({ row }) => ( - - ), - renderRowActions: ({ row }) => , - mantineTableContainerProps: { - style: { - height: 'calc(100vh - 150px)', - overflowY: 'auto', - // margin: 5, - }, - }, - }); + // debugTable: true, + // debugHeaders: true, + // debugColumns: false, + }) + + const { rows } = table.getRowModel() + + const virtualizerRef = useRef(null) + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => virtualizerRef.current, + estimateSize: () => 21, + overscan: 20, + }) + const items = virtualizer.getVirtualItems() + + const [before, after] = + items.length > 0 + ? [ + notUndefined(items[0]).start - virtualizer.options.scrollMargin, + virtualizer.getTotalSize() - notUndefined(items[items.length - 1]).end + ] + : [0, 0]; return ( {/* Header Row: outside the Paper */} { {/* Paper container: contains top toolbar and table (or ghost state) */} - - {/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */} - - - ({ + label: profile.name, + value: `${profile.id}`, + }))} + renderOption={renderProfileOption} + /> - - - - - - - - - - - - - - - + + + - {/* Table or ghost empty state inside Paper */} - - {Object.keys(channels).length === 0 && ( - + + + + + + + + + + + + - - - -
- -
-
- )} + Add + +
- {Object.keys(channels).length > 0 && ( - + + + {/* Table or ghost empty state inside Paper */} + + {initialDataCount === 0 && ( + )} - + + + + {initialDataCount > 0 && ( + + + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => { + return ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ) + })} + + ))} + + +
+
+ )} { isOpen={recordingModalOpen} onClose={closeRecordingForm} /> - + ); }); diff --git a/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx b/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx new file mode 100644 index 00000000..2f50cb23 --- /dev/null +++ b/frontend/src/components/tables/ChannelsTable/ChannelsTableBody.jsx @@ -0,0 +1,86 @@ +// HeadlessChannelsTable.jsx +import React, { useMemo, useState, useCallback, useRef } from 'react'; +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + flexRender, + getExpandedRowModel, +} from '@tanstack/react-table'; +import { + Table, + Box, + Checkbox, + ActionIcon, + ScrollArea, +} from '@mantine/core'; +import { ChevronRight, ChevronDown } from 'lucide-react'; +import ChannelsTableRow from './ChannelsTableRow'; +import { useVirtualizer } from '@tanstack/react-virtual' + +const ChannelsTableBody = ({ rows, height, onEdit, onDelete, onPreview, onRecord, virtualizedItems }) => { + const rowHeight = 48; + + // return ( + // + // + // {({ height }) => ( + // + // {({ index, style }) => { + // const row = rows[index]; + // return ( + // + // + // {row.getIsExpanded() && } + // + // ); + // }} + // + // )} + // + // + // ); + + return ( + + {virtualizedItems.map((virtualRow, index) => { + const row = rows[virtualRow.index] + return ( + + ); + })} + + ); +}; + +export default ChannelsTableBody; diff --git a/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx b/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx new file mode 100644 index 00000000..94466e13 --- /dev/null +++ b/frontend/src/components/tables/ChannelsTable/ChannelsTableRow.jsx @@ -0,0 +1,61 @@ +// HeadlessChannelsTable.jsx +import React, { useMemo, useState, useCallback } from 'react'; +import { FixedSizeList as List } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { + useReactTable, + getCoreRowModel, + getSortedRowModel, + flexRender, + getExpandedRowModel, +} from '@tanstack/react-table'; +import { + Table, + Box, + Checkbox, + ActionIcon, + ScrollArea, + Center, + useMantineTheme, +} from '@mantine/core'; +import { ChevronRight, ChevronDown } from 'lucide-react'; +import useSettingsStore from '../../../store/settings'; +import useChannelsStore from '../../../store/channels'; + +const ExpandIcon = ({ row, toggle }) => ( + + {row.getIsExpanded() ? : } + +); + +const ChannelsTableRow = ({ row, virtualRow, index, style, onEdit, onDelete, onPreview, onRecord }) => { + return ( + + {row.getVisibleCells().map(cell => { + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) +}; + +export default ChannelsTableRow diff --git a/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx b/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx new file mode 100644 index 00000000..9f719cda --- /dev/null +++ b/frontend/src/components/tables/ChannelsTable/EmptyChannelsTableGuide.jsx @@ -0,0 +1,78 @@ +export default () => { + return ( + +
+ + + It’s recommended to create channels after adding your M3U or + streams. + + + You can still create channels without streams if you’d like, + and map them later. + + + +
+ +
+ +
+
+ ) +} diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx index d6eb8053..3e45d7d9 100644 --- a/frontend/src/store/auth.jsx +++ b/frontend/src/store/auth.jsx @@ -32,7 +32,7 @@ const useAuthStore = create((set, get) => ({ initData: async () => { await Promise.all([ - useChannelsStore.getState().fetchChannels(), + // useChannelsStore.getState().fetchChannels(), useChannelsStore.getState().fetchChannelGroups(), useChannelsStore.getState().fetchLogos(), useChannelsStore.getState().fetchChannelProfiles(), diff --git a/frontend/src/store/channels.jsx b/frontend/src/store/channels.jsx index 53cce7af..f0987f44 100644 --- a/frontend/src/store/channels.jsx +++ b/frontend/src/store/channels.jsx @@ -310,75 +310,75 @@ const useChannelsStore = create((set, get) => ({ })), setChannelStats: (stats) => { - return set((state) => { - const { - channels, - stats: currentStats, - activeChannels: oldChannels, - activeClients: oldClients, - channelsByUUID, - } = state; + // return set((state) => { + // const { + // channels, + // stats: currentStats, + // activeChannels: oldChannels, + // activeClients: oldClients, + // channelsByUUID, + // } = state; - const newClients = {}; - const newChannels = stats.channels.reduce((acc, ch) => { - acc[ch.channel_id] = ch; + // const newClients = {}; + // const newChannels = stats.channels.reduce((acc, ch) => { + // acc[ch.channel_id] = ch; - if (currentStats.channels) { - if (oldChannels[ch.channel_id] === undefined) { - notifications.show({ - title: 'New channel streaming', - message: channels[channelsByUUID[ch.channel_id]].name, - color: 'blue.5', - }); - } - } + // if (currentStats.channels) { + // if (oldChannels[ch.channel_id] === undefined) { + // notifications.show({ + // title: 'New channel streaming', + // message: channels[channelsByUUID[ch.channel_id]].name, + // color: 'blue.5', + // }); + // } + // } - ch.clients.map((client) => { - newClients[client.client_id] = client; - // This check prevents the notifications if streams are active on page load - if (currentStats.channels) { - if (oldClients[client.client_id] === undefined) { - notifications.show({ - title: 'New client started streaming', - message: `Client streaming from ${client.ip_address}`, - color: 'blue.5', - }); - } - } - }); + // ch.clients.map((client) => { + // newClients[client.client_id] = client; + // // This check prevents the notifications if streams are active on page load + // if (currentStats.channels) { + // if (oldClients[client.client_id] === undefined) { + // notifications.show({ + // title: 'New client started streaming', + // message: `Client streaming from ${client.ip_address}`, + // color: 'blue.5', + // }); + // } + // } + // }); - return acc; - }, {}); + // return acc; + // }, {}); - // This check prevents the notifications if streams are active on page load - if (currentStats.channels) { - for (const uuid in oldChannels) { - if (newChannels[uuid] === undefined) { - notifications.show({ - title: 'Channel streaming stopped', - message: channels[channelsByUUID[uuid]].name, - color: 'blue.5', - }); - } - } + // // This check prevents the notifications if streams are active on page load + // if (currentStats.channels) { + // for (const uuid in oldChannels) { + // if (newChannels[uuid] === undefined) { + // notifications.show({ + // title: 'Channel streaming stopped', + // message: channels[channelsByUUID[uuid]].name, + // color: 'blue.5', + // }); + // } + // } - for (const clientId in oldClients) { - if (newClients[clientId] === undefined) { - notifications.show({ - title: 'Client stopped streaming', - message: `Client stopped streaming from ${oldClients[clientId].ip_address}`, - color: 'blue.5', - }); - } - } - } + // for (const clientId in oldClients) { + // if (newClients[clientId] === undefined) { + // notifications.show({ + // title: 'Client stopped streaming', + // message: `Client stopped streaming from ${oldClients[clientId].ip_address}`, + // color: 'blue.5', + // }); + // } + // } + // } - return { - stats, - activeChannels: newChannels, - activeClients: newClients, - }; - }); + // return { + // stats, + // activeChannels: newChannels, + // activeClients: newClients, + // }; + // }); }, fetchRecordings: async () => {