From c05e769f096c674bcc386a26a491d06da907f2a1 Mon Sep 17 00:00:00 2001 From: dekzter Date: Sun, 9 Mar 2025 17:35:59 -0400 Subject: [PATCH] new ui updates, primarily streams table, new api endpoints to efficiently get ids and stream groups --- apps/channels/api_urls.py | 2 - apps/channels/api_views.py | 27 +++++-- frontend/src/api.js | 12 +++ .../src/components/tables/ChannelsTable.js | 15 ++-- .../src/components/tables/StreamsTable.js | 76 ++++++++++++++----- frontend/src/helpers/table.js | 4 +- 6 files changed, 100 insertions(+), 36 deletions(-) diff --git a/apps/channels/api_urls.py b/apps/channels/api_urls.py index 719f153e..b2cf387e 100644 --- a/apps/channels/api_urls.py +++ b/apps/channels/api_urls.py @@ -6,7 +6,6 @@ from .api_views import ( ChannelGroupViewSet, BulkDeleteStreamsAPIView, BulkDeleteChannelsAPIView, - StreamIDsAPIView, ) app_name = 'channels' # for DRF routing @@ -20,7 +19,6 @@ urlpatterns = [ # Bulk delete is a single APIView, not a ViewSet path('streams/bulk-delete/', BulkDeleteStreamsAPIView.as_view(), name='bulk_delete_streams'), path('channels/bulk-delete/', BulkDeleteChannelsAPIView.as_view(), name='bulk_delete_channels'), - path('streams/ids/', StreamIDsAPIView.as_view(), name='stream_ids'), ] urlpatterns += router.urls diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index a306c4a5..7cf841af 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -32,13 +32,6 @@ class StreamFilter(django_filters.FilterSet): model = Stream fields = ['name', 'group_name', 'm3u_account', 'm3u_account_name', 'm3u_account_is_active'] -class StreamIDsAPIView(APIView): - permission_classes = [IsAuthenticated] # Enforce authentication if needed - - def get(self, request, *args, **kwargs): - stream_ids = Stream.objects.values_list('id', flat=True) - return Response(list(stream_ids)) - # ───────────────────────────────────────────────────────── # 1) Stream API (CRUD) # ───────────────────────────────────────────────────────── @@ -66,6 +59,26 @@ class StreamViewSet(viewsets.ModelViewSet): qs = qs.filter(channels__isnull=True) return qs + @action(detail=False, methods=['get'], url_path='ids') + def get_ids(self, request, *args, **kwargs): + # Get the filtered queryset + queryset = self.get_queryset() + + # Apply filtering, search, and ordering + queryset = self.filter_queryset(queryset) + + # Return only the IDs from the queryset + stream_ids = queryset.values_list('id', flat=True) + + # Return the response with the list of IDs + return Response(list(stream_ids)) + + @action(detail=False, methods=['get'], url_path='groups') + def get_groups(self, request, *args, **kwargs): + group_names = Stream.objects.exclude(group_name__isnull=True).exclude(group_name="").order_by().values_list('group_name', flat=True).distinct() + + # Return the response with the list of unique group names + return Response(list(group_names)) # ───────────────────────────────────────────────────────── # 2) Channel Group Management (CRUD) diff --git a/frontend/src/api.js b/frontend/src/api.js index 2604bdc1..be45dd56 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -296,6 +296,18 @@ export default class API { return retval; } + static async getStreamGroups() { + const response = await fetch(`${host}/api/channels/streams/groups/`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${await API.getAuthToken()}`, + }, + }); + + const retval = await response.json(); + return retval; + } + static async addStream(values) { const response = await fetch(`${host}/api/channels/streams/`, { method: 'POST', diff --git a/frontend/src/components/tables/ChannelsTable.js b/frontend/src/components/tables/ChannelsTable.js index 235177ca..255c97f7 100644 --- a/frontend/src/components/tables/ChannelsTable.js +++ b/frontend/src/components/tables/ChannelsTable.js @@ -43,7 +43,7 @@ import logo from '../../images/logo.png'; import useVideoStore from '../../store/useVideoStore'; import useSettingsStore from '../../store/settings'; import usePlaylistsStore from '../../store/playlists'; -import { Tv2, ScreenShare, Scroll } from 'lucide-react'; +import { Tv2, ScreenShare, Scroll, SquareMinus, Pencil } from 'lucide-react'; import { styled, useTheme } from '@mui/material/styles'; import ghostImage from '../../images/ghost.svg'; @@ -308,9 +308,10 @@ const ChannelsTable = ({}) => { size="small" sx={{ width: 300 }} clearOnEscape - onChange={(event, newValue) => - handleFilterChange(column.id, newValue) - } + onChange={(event, newValue) => { + event.stopPropagation(); + handleFilterChange(column.id, newValue); + }} renderInput={(params) => ( { }, }, ], - [channelGroupOptions] + [channelGroupOptions, filterValues] ); // Access the row virtualizer instance (optional) @@ -590,7 +591,7 @@ const ChannelsTable = ({}) => { }} sx={{ py: 0, px: 0.5 }} > - + @@ -601,7 +602,7 @@ const ChannelsTable = ({}) => { onClick={() => deleteChannel(row.original.id)} sx={{ py: 0, px: 0.5 }} > - + diff --git a/frontend/src/components/tables/StreamsTable.js b/frontend/src/components/tables/StreamsTable.js index 693dcb19..df9f09cb 100644 --- a/frontend/src/components/tables/StreamsTable.js +++ b/frontend/src/components/tables/StreamsTable.js @@ -29,6 +29,7 @@ import StreamForm from '../forms/Stream'; import usePlaylistsStore from '../../store/playlists'; import useChannelsStore from '../../store/channels'; import { useDebounce } from '../../utils'; +import { SquarePlus, ListPlus } from 'lucide-react'; const StreamsTable = ({}) => { /** @@ -38,7 +39,6 @@ const StreamsTable = ({}) => { const [stream, setStream] = useState(null); const [modalOpen, setModalOpen] = useState(false); const [moreActionsAnchorEl, setMoreActionsAnchorEl] = useState(null); - const [filterValues, setFilterValues] = useState({}); const [groupOptions, setGroupOptions] = useState([]); const [m3uOptions, setM3uOptions] = useState([]); const [actionsOpenRow, setActionsOpenRow] = useState(null); @@ -91,7 +91,7 @@ const StreamsTable = ({}) => { variant="standard" name="name" label="Name" - value={filters[column.id]} + value={filters.name || ''} onClick={(e) => e.stopPropagation()} onChange={handleFilterChange} size="small" @@ -131,11 +131,12 @@ const StreamsTable = ({}) => { disablePortal options={groupOptions} size="small" - sx={{ width: 300 }} + // sx={{ width: 300 }} clearOnEscape - onChange={(event, newValue) => - handleFilterChange(column.id, newValue) - } + onChange={(e, value) => { + e.stopPropagation(); + handleGroupChange(value); + }} renderInput={(params) => ( { Header: ({ column }) => ( ({ + label: playlist.name, + value: playlist.id, + }))} size="small" - sx={{ width: 300 }} + // sx={{ width: 300 }} clearOnEscape - onChange={(event, newValue) => - handleFilterChange(column.id, newValue) - } + onChange={(e, value) => { + e.stopPropagation(); + handleM3UChange(value); + }} renderInput={(params) => ( { ), }, ], - [playlists, groupOptions, m3uOptions, filters] + [playlists, groupOptions, filters] ); /** @@ -202,6 +207,22 @@ const StreamsTable = ({}) => { })); }; + const handleGroupChange = (value) => { + console.log(value); + setFilters((prev) => ({ + ...prev, + group_name: value ? value.value : '', + })); + }; + + const handleM3UChange = (value) => { + console.log(value); + setFilters((prev) => ({ + ...prev, + m3u_account: value ? value.value : '', + })); + }; + const fetchData = useCallback(async () => { setIsLoading(true); @@ -241,9 +262,16 @@ const StreamsTable = ({}) => { console.error('Error fetching data:', error); } + const groups = await API.getStreamGroups(); + setGroupOptions(groups); + setIsLoading(false); }, [pagination, sorting, debouncedFilters]); + useEffect(() => { + console.log(pagination); + }, [pagination]); + // Fallback: Individual creation (optional) const createChannelFromStream = async (stream) => { await API.createChannelFromStream({ @@ -363,6 +391,16 @@ const StreamsTable = ({}) => { setRowSelection(newSelection); }; + const onPaginationChange = (updater) => { + const newPagination = updater(pagination); + if (JSON.stringify(newPagination) === JSON.stringify(pagination)) { + // Prevent infinite re-render when there are no results + return; + } + + setPagination(updater); + }; + const table = useMaterialReactTable({ ...TableHelper.defaultProperties, columns, @@ -375,7 +413,7 @@ const StreamsTable = ({}) => { manualSorting: true, enableBottomToolbar: true, enableStickyHeader: true, - onPaginationChange: setPagination, + onPaginationChange: onPaginationChange, onSortingChange: setSorting, rowCount: rowCount, enableRowSelection: true, @@ -386,7 +424,9 @@ const StreamsTable = ({}) => { onChange: onSelectAllChange, }, onRowSelectionChange: onRowSelectionChange, - onSortingChange: setSorting, + initialState: { + density: 'compact', + }, state: { isLoading: isLoading, sorting, @@ -411,7 +451,7 @@ const StreamsTable = ({}) => { .includes(row.original.id)) } > - + @@ -422,7 +462,7 @@ const StreamsTable = ({}) => { onClick={() => createChannelFromStream(row.original)} sx={{ py: 0, px: 0.5 }} > - + @@ -482,7 +522,7 @@ const StreamsTable = ({}) => { variant="contained" onClick={() => editStream()} > - + @@ -526,7 +566,7 @@ const StreamsTable = ({}) => { */ useEffect(() => { fetchData(); - }, [pagination, debouncedFilters]); + }, [fetchData]); useEffect(() => { if (typeof window !== 'undefined') { diff --git a/frontend/src/helpers/table.js b/frontend/src/helpers/table.js index 33786af2..72d9ecbc 100644 --- a/frontend/src/helpers/table.js +++ b/frontend/src/helpers/table.js @@ -18,7 +18,7 @@ export default { }, muiTableBodyCellProps: { sx: { - padding: '6px', + py: 0, borderColor: '#444', color: '#E0E0E0', fontSize: '0.85rem', @@ -26,7 +26,7 @@ export default { }, muiTableHeadCellProps: { sx: { - padding: '6px', + py: 0, color: '#CFCFCF', backgroundColor: '#383A3F', borderColor: '#444',