From 29395f05381a07dbbc3102a2e8f23c91796f3f2f Mon Sep 17 00:00:00 2001 From: Jeffrey C <990135+JeffreyBytes@users.noreply.github.com> Date: Wed, 14 Jan 2026 09:48:40 -0600 Subject: [PATCH] unify StreamsTable refresh with channels requery --- frontend/src/WebSocket.jsx | 1 + frontend/src/api.js | 51 ++++++++ .../components/tables/ChannelTableStreams.jsx | 1 + .../src/components/tables/StreamsTable.jsx | 121 ++++++++---------- frontend/src/store/streamsTable.jsx | 49 +++++++ 5 files changed, 152 insertions(+), 71 deletions(-) create mode 100644 frontend/src/store/streamsTable.jsx diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 87d80953..72bbb39a 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -755,6 +755,7 @@ export const WebsocketProvider = ({ children }) => { // Refresh the channels table to show new channels try { await API.requeryChannels(); + await API.requeryStreams(); await useChannelsStore.getState().fetchChannels(); await fetchChannelProfiles(); console.log('Channels refreshed after bulk creation'); diff --git a/frontend/src/api.js b/frontend/src/api.js index c33ff1ee..f8b1f1c8 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -10,6 +10,7 @@ import useStreamProfilesStore from './store/streamProfiles'; import useSettingsStore from './store/settings'; import { notifications } from '@mantine/notifications'; import useChannelsTableStore from './store/channelsTable'; +import useStreamsTableStore from './store/streamsTable'; import useUsersStore from './store/users'; // If needed, you can set a base host or keep it empty if relative requests @@ -380,6 +381,7 @@ export default class API { }); useChannelsStore.getState().removeChannels([id]); + await API.requeryStreams(); } catch (e) { errorNotification('Failed to delete channel', e); } @@ -394,6 +396,7 @@ export default class API { }); useChannelsStore.getState().removeChannels(channel_ids); + await API.requeryStreams(); } catch (e) { errorNotification('Failed to delete channels', e); } @@ -447,6 +450,9 @@ export default class API { ); useChannelsStore.getState().updateChannel(response); + if (Object.prototype.hasOwnProperty.call(payload, 'streams')) { + await API.requeryStreams(); + } return response; } catch (e) { errorNotification('Failed to update channel', e); @@ -630,6 +636,7 @@ export default class API { useChannelsStore.getState().addChannel(response); } + await API.requeryStreams(); return response; } catch (e) { errorNotification('Failed to create channel', e); @@ -705,6 +712,46 @@ export default class API { } } + static async queryStreamsTable(params) { + try { + API.lastStreamQueryParams = params; + + const response = await request( + `${host}/api/channels/streams/?${params.toString()}` + ); + + useStreamsTableStore.getState().queryStreams(response, params); + + return response; + } catch (e) { + errorNotification('Failed to fetch streams', e); + } + } + + static async requeryStreams() { + if (!API.lastStreamQueryParams) { + return null; + } + + try { + const [response, ids] = await Promise.all([ + request( + `${host}/api/channels/streams/?${API.lastStreamQueryParams.toString()}` + ), + API.getAllStreamIds(API.lastStreamQueryParams), + ]); + + useStreamsTableStore + .getState() + .queryStreams(response, API.lastStreamQueryParams); + useStreamsTableStore.getState().setAllQueryIds(ids); + + return response; + } catch (e) { + errorNotification('Failed to fetch streams', e); + } + } + static async getAllStreamIds(params) { try { const response = await request( @@ -738,6 +785,7 @@ export default class API { useStreamsStore.getState().addStream(response); } + await API.requeryStreams(); return response; } catch (e) { errorNotification('Failed to add stream', e); @@ -756,6 +804,7 @@ export default class API { useStreamsStore.getState().updateStream(response); } + await API.requeryStreams(); return response; } catch (e) { errorNotification('Failed to update stream', e); @@ -769,6 +818,7 @@ export default class API { }); useStreamsStore.getState().removeStreams([id]); + await API.requeryStreams(); } catch (e) { errorNotification('Failed to delete stream', e); } @@ -782,6 +832,7 @@ export default class API { }); useStreamsStore.getState().removeStreams(ids); + await API.requeryStreams(); } catch (e) { errorNotification('Failed to delete streams', e); } diff --git a/frontend/src/components/tables/ChannelTableStreams.jsx b/frontend/src/components/tables/ChannelTableStreams.jsx index 4fb62009..0443b3ca 100644 --- a/frontend/src/components/tables/ChannelTableStreams.jsx +++ b/frontend/src/components/tables/ChannelTableStreams.jsx @@ -167,6 +167,7 @@ const ChannelStreams = ({ channel, isExpanded }) => { streams: newStreamList.map((s) => s.id), }); await API.requeryChannels(); + await API.requeryStreams(); }; // Create M3U account map for quick lookup diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index bc10e904..2d7de36e 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -46,7 +46,6 @@ import { MultiSelect, useMantineTheme, UnstyledButton, - LoadingOverlay, Skeleton, Modal, NumberInput, @@ -61,7 +60,7 @@ import { CustomTable, useTable } from './CustomTable'; import useLocalStorage from '../../hooks/useLocalStorage'; import ConfirmationDialog from '../ConfirmationDialog'; import CreateChannelModal from '../modals/CreateChannelModal'; -import { useWebSocket } from '../../WebSocket'; +import useStreamsTableStore from '../../store/streamsTable'; const StreamRowActions = ({ theme, @@ -71,7 +70,6 @@ const StreamRowActions = ({ handleWatchStream, selectedChannelIds, createChannelFromStream, - fetchData, }) => { const [tableSize, _] = useLocalStorage('table-size', 'default'); const channelSelectionStreams = useChannelsTableStore( @@ -89,7 +87,6 @@ const StreamRowActions = ({ ], }); await API.requeryChannels(); - await fetchData({ showLoader: false }); }; const onEdit = useCallback(() => { @@ -182,23 +179,18 @@ const StreamRowActions = ({ const StreamsTable = ({ onReady }) => { const theme = useMantineTheme(); const hasSignaledReady = useRef(false); - const [, , websocketEvent] = useWebSocket(); + const hasFetchedOnce = useRef(false); /** * useState */ - const [allRowIds, setAllRowIds] = useState([]); const [stream, setStream] = useState(null); const [modalOpen, setModalOpen] = useState(false); const [groupOptions, setGroupOptions] = useState([]); const [initialDataCount, setInitialDataCount] = useState(null); - const [data, setData] = useState([]); // Holds fetched data - const [pageCount, setPageCount] = useState(0); const [paginationString, setPaginationString] = useState(''); const [isLoading, setIsLoading] = useState(true); - const [sorting, setSorting] = useState([{ id: 'name', desc: false }]); - const [selectedStreamIds, setSelectedStreamIds] = useState([]); // Channel creation modal state (bulk) const [channelNumberingModalOpen, setChannelNumberingModalOpen] = @@ -230,10 +222,6 @@ const StreamsTable = ({ onReady }) => { 'streams-page-size', 50 ); - const [pagination, setPagination] = useState({ - pageIndex: 0, - pageSize: storedPageSize, - }); const [filters, setFilters] = useState({ name: '', channel_group: '', @@ -246,15 +234,12 @@ const StreamsTable = ({ onReady }) => { ); const debouncedFilters = useDebounce(filters, 500, () => { // Reset to first page whenever filters change to avoid "Invalid page" errors - setPagination((prev) => ({ - ...prev, + setPagination({ + ...pagination, pageIndex: 0, - })); + }); }); - // Add state to track if stream groups are loaded - const [groupsLoaded, setGroupsLoaded] = useState(false); - const navigate = useNavigate(); /** @@ -277,6 +262,20 @@ const StreamsTable = ({ onReady }) => { const showVideo = useVideoStore((s) => s.showVideo); const [tableSize, _] = useLocalStorage('table-size', 'default'); + const data = useStreamsTableStore((s) => s.streams); + const pageCount = useStreamsTableStore((s) => s.pageCount); + const totalCount = useStreamsTableStore((s) => s.totalCount); + const allRowIds = useStreamsTableStore((s) => s.allQueryIds); + const setAllRowIds = useStreamsTableStore((s) => s.setAllQueryIds); + const pagination = useStreamsTableStore((s) => s.pagination); + const setPagination = useStreamsTableStore((s) => s.setPagination); + const sorting = useStreamsTableStore((s) => s.sorting); + const setSorting = useStreamsTableStore((s) => s.setSorting); + const selectedStreamIds = useStreamsTableStore((s) => s.selectedStreamIds); + const setSelectedStreamIds = useStreamsTableStore( + (s) => s.setSelectedStreamIds + ); + // Warnings store for "remember choice" functionality const suppressWarning = useWarningsStore((s) => s.suppressWarning); const isWarningSuppressed = useWarningsStore((s) => s.isWarningSuppressed); @@ -401,16 +400,6 @@ const StreamsTable = ({ onReady }) => { setIsLoading(true); } - // Ensure we have channel groups first (if not already loaded) - if (!groupsLoaded && Object.keys(channelGroups).length === 0) { - try { - await fetchChannelGroups(); - setGroupsLoaded(true); - } catch (error) { - console.error('Error fetching channel groups:', error); - } - } - const params = new URLSearchParams(); params.append('page', pagination.pageIndex + 1); params.append('page_size', pagination.pageSize); @@ -436,30 +425,18 @@ const StreamsTable = ({ onReady }) => { try { const [result, ids, groups] = await Promise.all([ - API.queryStreams(params), + API.queryStreamsTable(params), API.getAllStreamIds(params), API.getStreamGroups(), ]); setAllRowIds(ids); - setData(result.results); - setPageCount(Math.ceil(result.count / pagination.pageSize)); setGroupOptions(groups); - // 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}`); - // Signal that initial data load is complete if (!hasSignaledReady.current && onReady) { hasSignaledReady.current = true; @@ -469,18 +446,11 @@ const StreamsTable = ({ onReady }) => { console.error('Error fetching data:', error); } + hasFetchedOnce.current = true; if (showLoader) { setIsLoading(false); } - }, [ - pagination, - sorting, - debouncedFilters, - groupsLoaded, - channelGroups, - fetchChannelGroups, - onReady, - ]); + }, [pagination, sorting, debouncedFilters, onReady]); // Bulk creation: create channels from selected streams asynchronously const createChannelsFromStreams = async () => { @@ -554,9 +524,7 @@ const StreamsTable = ({ onReady }) => { // Clear selection since the task has started setSelectedStreamIds([]); - // Note: This is a background task, so fetchData may not show updated data immediately - // The actual update will occur when the WebSocket event fires on task completion - await fetchData({ showLoader: false }); + // Note: This is a background task, so the update happens on WebSocket completion } catch (error) { console.error('Error starting bulk channel creation:', error); // Error notifications will be handled by WebSocket @@ -616,7 +584,6 @@ const StreamsTable = ({ onReady }) => { setDeleting(true); try { await API.deleteStream(id); - fetchData({ showLoader: false }); // Clear the selection for the deleted stream setSelectedStreamIds([]); table.setSelectedTableIds([]); @@ -642,7 +609,6 @@ const StreamsTable = ({ onReady }) => { setDeleting(true); try { await API.deleteStreams(selectedStreamIds); - fetchData({ showLoader: false }); setSelectedStreamIds([]); table.setSelectedTableIds([]); } finally { @@ -654,7 +620,7 @@ const StreamsTable = ({ onReady }) => { const closeStreamForm = () => { setStream(null); setModalOpen(false); - fetchData({ showLoader: false }); + API.requeryStreams(); }; // Single channel creation functions @@ -724,7 +690,6 @@ const StreamsTable = ({ onReady }) => { await API.requeryChannels(); // const fetchLogos = useChannelsStore.getState().fetchLogos; // fetchLogos(); - await fetchData({ showLoader: false }); }; // Handle confirming the single channel numbering modal @@ -767,7 +732,6 @@ const StreamsTable = ({ onReady }) => { ], }); await API.requeryChannels(); - await fetchData({ showLoader: false }); }; const onRowSelectionChange = (updatedIds) => { @@ -928,7 +892,6 @@ const StreamsTable = ({ onReady }) => { handleWatchStream={handleWatchStream} selectedChannelIds={selectedChannelIds} createChannelFromStream={createChannelFromStream} - fetchData={fetchData} /> ); } @@ -940,7 +903,6 @@ const StreamsTable = ({ onReady }) => { editStream, deleteStream, handleWatchStream, - fetchData, ] ); @@ -958,6 +920,10 @@ const StreamsTable = ({ onReady }) => { manualSorting: true, manualFiltering: true, enableRowSelection: true, + state: { + pagination, + sorting, + }, headerCellRenderFns: { name: renderHeaderCell, group: renderHeaderCell, @@ -985,15 +951,29 @@ const StreamsTable = ({ onReady }) => { }, [fetchData]); useEffect(() => { - const eventType = websocketEvent?.data?.type; - if ( - eventType === 'channels_created' || - (eventType === 'bulk_channel_creation_progress' && - websocketEvent?.data?.status === 'completed') - ) { - fetchData({ showLoader: false }); + if (Object.keys(channelGroups).length > 0) { + return; } - }, [websocketEvent, fetchData]); + + const loadGroups = async () => { + try { + await fetchChannelGroups(); + } catch (error) { + console.error('Error fetching channel groups:', error); + } + }; + + loadGroups(); + }, [channelGroups, fetchChannelGroups]); + + useEffect(() => { + const startItem = pagination.pageIndex * pagination.pageSize + 1; + const endItem = Math.min( + (pagination.pageIndex + 1) * pagination.pageSize, + totalCount + ); + setPaginationString(`${startItem} to ${endItem} of ${totalCount}`); + }, [pagination.pageIndex, pagination.pageSize, totalCount]); return ( <> @@ -1201,7 +1181,6 @@ const StreamsTable = ({ onReady }) => { borderRadius: 'var(--mantine-radius-default)', }} > - diff --git a/frontend/src/store/streamsTable.jsx b/frontend/src/store/streamsTable.jsx new file mode 100644 index 00000000..f353acf7 --- /dev/null +++ b/frontend/src/store/streamsTable.jsx @@ -0,0 +1,49 @@ +import { create } from 'zustand'; + +const useStreamsTableStore = create((set) => ({ + streams: [], + pageCount: 0, + totalCount: 0, + sorting: [{ id: 'name', desc: false }], + pagination: { + pageIndex: 0, + pageSize: + JSON.parse(localStorage.getItem('streams-page-size')) || 50, + }, + selectedStreamIds: [], + allQueryIds: [], + + queryStreams: ({ results, count }, params) => { + set(() => ({ + streams: results, + totalCount: count, + pageCount: Math.ceil(count / params.get('page_size')), + })); + }, + + setAllQueryIds: (allQueryIds) => { + set(() => ({ + allQueryIds, + })); + }, + + setSelectedStreamIds: (selectedStreamIds) => { + set(() => ({ + selectedStreamIds, + })); + }, + + setPagination: (pagination) => { + set(() => ({ + pagination, + })); + }, + + setSorting: (sorting) => { + set(() => ({ + sorting, + })); + }, +})); + +export default useStreamsTableStore;