diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 7ea7e3aa..15da6c14 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -139,13 +139,13 @@ class ChannelViewSet(viewsets.ModelViewSet): queryset = Channel.objects.all() serializer_class = ChannelSerializer permission_classes = [IsAuthenticated] - # pagination_class = ChannelPagination + pagination_class = ChannelPagination - # filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] - # filterset_class = ChannelFilter - # search_fields = ['name', 'channel_group__name'] - # ordering_fields = ['channel_number', 'name', 'channel_group__name'] - # ordering = ['-channel_number'] + filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] + filterset_class = ChannelFilter + search_fields = ['name', 'channel_group__name'] + ordering_fields = ['channel_number', 'name', 'channel_group__name'] + ordering = ['-channel_number'] def get_queryset(self): qs = super().get_queryset() diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py index fcab5905..f157d4dd 100644 --- a/apps/channels/serializers.py +++ b/apps/channels/serializers.py @@ -83,12 +83,9 @@ class ChannelProfileSerializer(serializers.ModelSerializer): fields = ['id', 'name', 'channels'] def get_channels(self, obj): - memberships = ChannelProfileMembership.objects.filter(channel_profile=obj) + memberships = ChannelProfileMembership.objects.filter(channel_profile=obj, enabled=True) return [ - { - 'id': membership.channel.id, - 'enabled': membership.enabled - } + membership.channel.id for membership in memberships ] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9f680d3e..edd99202 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,7 +16,11 @@ "@mantine/hooks": "^7.17.2", "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", +<<<<<<< Updated upstream "@tanstack/react-table": "^8.21.2", +======= + "@tanstack/react-table": "^8.21.3", +>>>>>>> Stashed changes "allotment": "^1.20.3", "axios": "^1.8.2", "clsx": "^2.1.1", @@ -1743,12 +1747,21 @@ } }, "node_modules/@tanstack/react-table": { +<<<<<<< Updated upstream "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.21.2" +======= + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" +>>>>>>> Stashed changes }, "engines": { "node": ">=12" @@ -1780,9 +1793,15 @@ } }, "node_modules/@tanstack/table-core": { +<<<<<<< Updated upstream "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==", +======= + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", +>>>>>>> Stashed changes "license": "MIT", "engines": { "node": ">=12" diff --git a/frontend/package.json b/frontend/package.json index c0d6ced3..76a72114 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,11 @@ "@mantine/hooks": "^7.17.2", "@mantine/notifications": "^7.17.2", "@tabler/icons-react": "^3.31.0", +<<<<<<< Updated upstream "@tanstack/react-table": "^8.21.2", +======= + "@tanstack/react-table": "^8.21.3", +>>>>>>> Stashed changes "allotment": "^1.20.3", "axios": "^1.8.2", "clsx": "^2.1.1", diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index d3d4b868..d75a7274 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -54,6 +54,7 @@ import { MultiSelect, Pagination, NativeSelect, +<<<<<<< Updated upstream Checkbox, Table, } from '@mantine/core'; @@ -69,6 +70,19 @@ import { getPaginationRowModel, } from '@tanstack/react-table' import { notUndefined, useVirtualizer } from '@tanstack/react-virtual' +======= + Table, + Checkbox, +} from '@mantine/core'; +import { + useReactTable, + getCoreRowModel, + getPaginationRowModel, + getSortedRowModel, + getFilteredRowModel, + flexRender, +} from '@tanstack/react-table'; +>>>>>>> Stashed changes const ChannelStreams = React.memo(({ channel, isExpanded }) => { const channelStreams = useChannelsStore( @@ -206,7 +220,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(); @@ -263,6 +277,7 @@ const CreateProfilePopover = React.memo(({ }) => { ); }); +<<<<<<< Updated upstream const ChannelEnabledCell = ({ cell, row, toggleChannelEnabled, selectedProfileId }) => { const handleSwitchChange = useCallback(() => { toggleChannelEnabled([row.original.id], !cell.getValue()); @@ -385,6 +400,88 @@ const RowActions = React.memo(({ {/* {env_mode == 'dev' && ( +======= +const ChannelEnabledSwitch = React.memo( + ({ row, selectedProfileId, toggleChannelEnabled, enabled }) => { + const isEnabled = selectedProfileId === '0' || enabled; + + const handleToggle = useCallback(() => { + toggleChannelEnabled([row.original.id], !isEnabled); + }, []); + + return ( + + ); + } +); + +const ChannelRowActions = React.memo( + ({ + theme, + row, + editChannel, + deleteChannel, + handleWatchStream, + createRecording, + }) => { + const onEdit = useCallback(() => { + editChannel(row.original); + }, []); + + const onDelete = useCallback(() => { + deleteChannel(row.original.id); + }, []); + + const onPreview = useCallback(() => { + handleWatchStream(row.original.uuid); + }, []); + + const onRecord = useCallback(() => { + createRecording(row.original); + }, []); + + return ( + +
+ + + + + + + + + + + + + + + + + + +>>>>>>> Stashed changes @@ -394,7 +491,11 @@ const RowActions = React.memo(({ >>>>>> Stashed changes leftSection={
+<<<<<<< Updated upstream )} */}
@@ -418,6 +520,15 @@ const RowActions = React.memo(({ }); const ChannelsTable = React.memo(({ }) => { +======= + + + ); + } +); + +const ChannelsTable = React.memo(({}) => { +>>>>>>> Stashed changes const { channels, isLoading: channelsLoading, @@ -426,7 +537,7 @@ const ChannelsTable = React.memo(({ }) => { profiles, selectedProfileId, setSelectedProfileId, - selectedProfileChannels, + selectedProfileChannelIds, channelsPageSelection, } = useChannelsStore(); @@ -451,14 +562,13 @@ const ChannelsTable = React.memo(({ }) => { const [paginationString, setPaginationString] = useState(''); const [pagination, setPagination] = useState({ pageIndex: 0, - pageSize: 250, + pageSize: 50, }); const [groupOptions, setGroupOptions] = useState([]); const [initialDataCount, setInitialDataCount] = useState(null); const [filters, setFilters] = useState({ name: '', channel_group: '', - m3u_account: '', }); const debouncedFilters = useDebounce(filters, 500); const [isLoading, setIsLoading] = useState(true); @@ -499,6 +609,13 @@ const ChannelsTable = React.memo(({ }) => { ]); }, [data]); +<<<<<<< Updated upstream +======= + const stopPropagation = (e) => { + e.stopPropagation(); + }; + +>>>>>>> Stashed changes const handleFilterChange = (e) => { const { name, value } = e.target; setFilters((prev) => ({ @@ -518,6 +635,32 @@ const ChannelsTable = React.memo(({ }) => { const m3uUrlRef = useRef(null); const epgUrlRef = useRef(null); + const editChannel = async (ch = null) => { + setChannel(ch); + setChannelModalOpen(true); + }; + + const deleteChannel = async (id) => { + setRowSelection([]); + if (channelsPageSelection.length > 0) { + return deleteChannels(); + } + await API.deleteChannel(id); + }; + + const createRecording = (channel) => { + setChannel(channel); + setRecordingModalOpen(true); + }; + + function handleWatchStream(channelNumber) { + let vidUrl = `/proxy/ts/stream/${channelNumber}`; + if (env_mode == 'dev') { + vidUrl = `${window.location.protocol}//${window.location.hostname}:5656${vidUrl}`; + } + showVideo(vidUrl); + } + const fetchData = useCallback(async () => { setIsLoading(true); @@ -576,8 +719,8 @@ const ChannelsTable = React.memo(({ }) => { }, [pagination, sorting, debouncedFilters]); useEffect(() => { - fetchData() - }, [fetchData]) + fetchData(); + }, [fetchData]); const onRowSelectionChange = (updater) => { setRowSelection((prevRowSelection) => { @@ -637,20 +780,21 @@ const ChannelsTable = React.memo(({ }) => { }); }; - const toggleChannelEnabled = async (channelIds, enabled) => { - if (channelIds.length == 1) { - await API.updateProfileChannel(channelIds[0], selectedProfileId, enabled); - } else { - await API.updateProfileChannels(channelIds, selectedProfileId, enabled); - setChannelsEnabledHeaderSwitch(enabled); - } - }; - - const enabledChannelSet = useMemo(() => { - return new Set( - selectedProfileChannels.filter((c) => c.enabled).map((c) => c.id) - ); - }, [selectedProfileChannels]); + const toggleChannelEnabled = useCallback( + async (channelIds, enabled) => { + if (channelIds.length == 1) { + await API.updateProfileChannel( + channelIds[0], + selectedProfileId, + enabled + ); + } else { + await API.updateProfileChannels(channelIds, selectedProfileId, enabled); + setChannelsEnabledHeaderSwitch(enabled); + } + }, + [selectedProfileId] + ); const EnabledHeaderSwitch = React.memo(({ isActive, toggle, disabled }) => ( { selectedProfileId, ]); +<<<<<<< Updated upstream // Configure columns const columns = useMemo( () => [ @@ -848,6 +993,201 @@ const ChannelsTable = React.memo(({ }) => { // channelsEnabledHeaderSwitch, ] ); +======= + // // Configure columns + // const columns = useMemo( + // () => [ + // { + // id: 'enabled', + // // Header: renderEnabledHeader, + // enableSorting: false, + // 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 ( + // + // ); + // }, + // }, + // { + // header: '#', + // size: 50, + // maxSize: 50, + // 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, + // // // }, + // }, + // }, + // { + // id: 'name', + // header: 'Name', + // accessorKey: 'name', + // Header: ({ column }) => ( + // { + // e.stopPropagation(); + // handleFilterChange(column.id, e.target.value); + // }} + // size="xs" + // variant="unstyled" + // className="table-input-header" + // onClick={(e) => e.stopPropagation()} + // /> + // ), + // Cell: ({ cell }) => ( + //
+ // {cell.getValue()} + //
+ // ), + // }, + // { + // header: 'Group', + // accessorKey: 'channel_group.name', + // accessorFn: (row) => row.channel_group?.name || '', + // Cell: ({ cell }) => ( + //
+ // {cell.getValue()} + //
+ // ), + // Header: ({ column }) => ( + // e.stopPropagation()}> + // { + // handleFilterChange(column.id, value); + // }} + // data={channelGroupOptions} + // variant="unstyled" + // className="table-input-header custom-multiselect" + // /> + // + // ), + // }, + // { + // header: '', + // accessorKey: 'logo', + // enableSorting: false, + // size: 75, + // mantineTableBodyCellProps: { + // align: 'center', + // style: { + // maxWidth: '75px', + // }, + // }, + // Cell: ({ cell }) => ( + // + // channel logo + // + // ), + // }, + // ], + // [ + // channelGroupOptions, + // filterValues, + // selectedProfile, + // selectedProfileChannels, + // rowSelection, + // channelsPageSelection, + // channelsEnabledHeaderSwitch, + // ] + // ); +>>>>>>> Stashed changes // (Optional) bulk delete, but your endpoint is @TODO const deleteChannels = async () => { @@ -971,8 +1311,42 @@ const ChannelsTable = React.memo(({ }) => { // }, [rowSelection]) useEffect(() => { +<<<<<<< Updated upstream fetchData(); }, [fetchData]); +======= + const selectedRows = table + .getSelectedRowModel() + .rows.map((row) => row.original); + setChannelsPageSelection(selectedRows); + + if (selectedProfileId != '0') { + setChannelsEnabledHeaderSwitch( + selectedRows.filter((row) => selectedProfileChannelIds.has(row.id)) + .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; + }) + ); +>>>>>>> Stashed changes const deleteProfile = async (id) => { await API.deleteChannelProfile(id); @@ -999,6 +1373,7 @@ const ChannelsTable = React.memo(({ }) => { ); }; +<<<<<<< Updated upstream const editChannel = useCallback((row) => { setChannel(row.original); setChannelModalOpen(true); @@ -1058,6 +1433,334 @@ const ChannelsTable = React.memo(({ }) => { virtualizer.getTotalSize() - notUndefined(items[items.length - 1]).end ] : [0, 0]; +======= + const newColumns = useMemo( + () => [ + { + id: 'select', + size: 30, + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + enableColumnFilter: false, + }, + { + id: 'enabled', + size: 45, + header: () => ( +
+ +
+ ), + cell: ({ row }) => ( + + ), + enableSorting: false, + }, + { + accessorKey: 'channel_number', + size: 30, + header: () => #, + cell: ({ getValue }) => ( + + {getValue()} + + ), + }, + { + id: 'name', + accessorKey: 'name', + header: ({ column }) => ( + + ), + cell: ({ getValue }) => ( + + {getValue()} + + ), + }, + { + accessorFn: (row) => row.channel_group?.name || '', + id: 'channel_group', + header: () => ( + d.channel_group?.name || '')) + )} + size="xs" + searchable + onClick={stopPropagation} + onChange={handleGroupChange} + /> + ), + cell: ({ getValue }) => ( + + {getValue()} + + ), + }, + { + accessorKey: 'logo', + size: 75, + header: '', + cell: ({ getValue }) => { + const value = getValue(); + const src = value?.cache_url || logo; + return ( + + logo + + ); + }, + enableSorting: false, + }, + { + id: 'actions', + size: 75, + header: '', + cell: ({ row }) => ( + + ), + enableSorting: false, + }, + ], + [selectedProfileId, selectedProfileChannelIds, data] + ); + + const table = useReactTable({ + data, + columns: newColumns, + pageCount, + state: { + sorting, + filters, + pagination, + rowSelection, + }, + manualPagination: true, + manualSorting: true, + manualFiltering: true, + enableRowSelection: true, + onPaginationChange: setPagination, + onSortingChange: setSorting, + onColumnFiltersChange: setFilters, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + debugTable: true, + }); +>>>>>>> Stashed changes + + // const oldtable = useMantineReactTable({ + // ...TableHelper.defaultProperties, + // columns, + // data, + // enablePagination: true, + // manualPagination: true, + // enableColumnActions: false, + // 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, + // }, + // }, + // }); return ( @@ -1200,6 +1903,30 @@ const ChannelsTable = React.memo(({ }) => { {/* Paper container: contains top toolbar and table (or ghost state) */} +<<<<<<< Updated upstream +======= + + {/* Top toolbar with Remove, Assign, Auto-match, and Add buttons */} + + +