From eb48083ccec9fcd748e87f0532d0697c310f252d Mon Sep 17 00:00:00 2001 From: dekzter Date: Tue, 15 Apr 2025 09:03:24 -0400 Subject: [PATCH] channels pagination --- .../src/components/tables/ChannelsTable.jsx | 205 +++++++++++++++--- 1 file changed, 177 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 6ba5ea9a..4a6bc014 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -12,7 +12,7 @@ import API from '../../api'; import ChannelForm from '../forms/Channel'; import RecordingForm from '../forms/Recording'; import { TableHelper } from '../../helpers'; -import { getDescendantProp } from '../../utils'; +import { getDescendantProp, useDebounce } from '../../utils'; import logo from '../../images/logo.png'; import useVideoStore from '../../store/useVideoStore'; import useSettingsStore from '../../store/settings'; @@ -53,6 +53,8 @@ import { Switch, Menu, MultiSelect, + Pagination, + NativeSelect, } from '@mantine/core'; const ChannelStreams = React.memo(({ channel, isExpanded }) => { @@ -191,7 +193,7 @@ const m3uUrlBase = `${window.location.protocol}//${window.location.host}/output/ const epgUrlBase = `${window.location.protocol}//${window.location.host}/output/epg`; const hdhrUrlBase = `${window.location.protocol}//${window.location.host}/hdhr`; -const CreateProfilePopover = React.memo(({}) => { +const CreateProfilePopover = React.memo(({ }) => { const [opened, setOpened] = useState(false); const [name, setName] = useState(''); const theme = useMantineTheme(); @@ -248,7 +250,7 @@ const CreateProfilePopover = React.memo(({}) => { ); }); -const ChannelsTable = React.memo(({}) => { +const ChannelsTable = React.memo(({ }) => { const { channels, isLoading: channelsLoading, @@ -275,6 +277,28 @@ const ChannelsTable = React.memo(({}) => { ); const [channelsEnabledHeaderSwitch, setChannelsEnabledHeaderSwitch] = useState(false); + const [initialDataCount, setInitialDataCount] = useState(null); + const [data, setData] = 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 [filters, setFilters] = useState({ + name: '', + channel_group: '', + m3u_account: '', + }); + const debouncedFilters = useDebounce(filters, 500); + const [isLoading, setIsLoading] = useState(true); + const [sorting, setSorting] = useState([ + { id: 'channel_number', desc: false }, + { id: 'name', desc: false }, + ]); const [hdhrUrl, setHDHRUrl] = useState(hdhrUrlBase); const [epgUrl, setEPGUrl] = useState(epgUrlBase); @@ -322,6 +346,125 @@ const ChannelsTable = React.memo(({}) => { const m3uUrlRef = useRef(null); const epgUrlRef = useRef(null); + const fetchData = useCallback(async () => { + setIsLoading(true); + + const params = new URLSearchParams(); + params.append('page', pagination.pageIndex + 1); + params.append('page_size', pagination.pageSize); + + // Apply sorting + if (sorting.length > 0) { + const sortField = sorting[0].id; + const sortDirection = sorting[0].desc ? '-' : ''; + params.append('ordering', `${sortDirection}${sortField}`); + } + + // Apply debounced filters + Object.entries(debouncedFilters).forEach(([key, value]) => { + if (value) params.append(key, value); + }); + + try { + const result = await API.queryChannels(params); + setData(result.results); + setRowCount(result.count); + setPageCount(Math.ceil(result.count / pagination.pageSize)); + + // Calculate the starting and ending item indexes + const startItem = pagination.pageIndex * pagination.pageSize + 1; // +1 to start from 1, not 0 + const endItem = Math.min( + (pagination.pageIndex + 1) * pagination.pageSize, + result.count + ); + + if (initialDataCount === null) { + setInitialDataCount(result.count); + } + + // Generate the string + setPaginationString(`${startItem} to ${endItem} of ${result.count}`); + + const newSelection = {}; + result.results.forEach((item, index) => { + if (selectedStreamIds.includes(item.id)) { + newSelection[index] = true; + } + }); + + // ✅ Only update rowSelection if it's different + if (JSON.stringify(newSelection) !== JSON.stringify(rowSelection)) { + setRowSelection(newSelection); + } + } catch (error) { + console.error('Error fetching data:', error); + } + + setIsLoading(false); + }, [pagination, sorting, debouncedFilters]); + + useEffect(() => { + fetchData() + }, [fetchData]) + + const onRowSelectionChange = (updater) => { + setRowSelection((prevRowSelection) => { + const newRowSelection = + typeof updater === 'function' ? updater(prevRowSelection) : updater; + + const updatedSelected = new Set([...selectedStreamIds]); + table.getRowModel().rows.forEach((row) => { + if (newRowSelection[row.id] === undefined || !newRowSelection[row.id]) { + updatedSelected.delete(row.original.id); + } else { + updatedSelected.add(row.original.id); + } + }); + setSelectedStreamIds([...updatedSelected]); + + return newRowSelection; + }); + }; + + const onSelectAllChange = async (e) => { + const selectAll = e.target.checked; + if (selectAll) { + // Get all stream IDs for current view + const params = new URLSearchParams(); + Object.entries(debouncedFilters).forEach(([key, value]) => { + if (value) params.append(key, value); + }); + const ids = await API.getAllStreamIds(params); + setSelectedStreamIds(ids); + } else { + setSelectedStreamIds([]); + } + + const newSelection = {}; + table.getRowModel().rows.forEach((item, index) => { + newSelection[index] = selectAll; + }); + setRowSelection(newSelection); + }; + + const onPageSizeChange = (e) => { + setPagination({ + ...pagination, + pageSize: e.target.value, + }); + }; + + const onPageIndexChange = (pageIndex) => { + if (!pageIndex || pageIndex > pageCount) { + return; + } + + setPagination({ + ...pagination, + pageIndex: pageIndex - 1, + }); + }; + const toggleChannelEnabled = async (channelIds, enabled) => { if (channelIds.length == 1) { await API.updateProfileChannel(channelIds[0], selectedProfileId, enabled); @@ -565,15 +708,6 @@ const ChannelsTable = React.memo(({}) => { ] ); - // Access the row virtualizer instance (optional) - const rowVirtualizerInstanceRef = useRef(null); - - const [isLoading, setIsLoading] = useState(true); - const [sorting, setSorting] = useState([ - { id: 'channel_number', desc: false }, - { id: 'name', desc: false }, - ]); - // (Optional) bulk delete, but your endpoint is @TODO const deleteChannels = async () => { setIsLoading(true); @@ -645,15 +779,6 @@ const ChannelsTable = React.memo(({}) => { } }, []); - useEffect(() => { - // Scroll to the top of the table when sorting changes - try { - rowVirtualizerInstanceRef.current?.scrollToIndex?.(0); - } catch (error) { - console.error(error); - } - }, [sorting]); - const handleCopy = async (textToCopy, ref) => { try { await navigator.clipboard.writeText(textToCopy); @@ -848,21 +973,45 @@ const ChannelsTable = React.memo(({}) => { const table = useMantineReactTable({ ...TableHelper.defaultProperties, columns, - data: filteredData, - enablePagination: false, + data, + enablePagination: true, + manualPagination: true, enableColumnActions: false, - enableRowVirtualization: true, enableRowSelection: true, renderTopToolbar: false, - onRowSelectionChange: setRowSelection, + onRowSelectionChange: onRowSelectionChange, onSortingChange: setSorting, state: { isLoading: isLoading || channelsLoading, sorting, rowSelection, }, - rowVirtualizerInstanceRef, - rowVirtualizerOptions: { overscan: 25 }, + enableBottomToolbar: true, + renderBottomToolbar: ({ table }) => ( + + Page Size + + + {paginationString} + + ), initialState: { density: 'compact', sorting: [ @@ -962,7 +1111,7 @@ const ChannelsTable = React.memo(({}) => { renderRowActions: ({ row }) => , mantineTableContainerProps: { style: { - height: 'calc(100vh - 110px)', + height: 'calc(100vh - 150px)', overflowY: 'auto', // margin: 5, },