mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-08-03 15:22:36 +00:00
new ui updates, primarily streams table, new api endpoints to efficiently get ids and stream groups
This commit is contained in:
parent
7aa30f2678
commit
c05e769f09
6 changed files with 100 additions and 36 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<TextField
|
||||
{...params}
|
||||
|
|
@ -351,7 +352,7 @@ const ChannelsTable = ({}) => {
|
|||
},
|
||||
},
|
||||
],
|
||||
[channelGroupOptions]
|
||||
[channelGroupOptions, filterValues]
|
||||
);
|
||||
|
||||
// Access the row virtualizer instance (optional)
|
||||
|
|
@ -590,7 +591,7 @@ const ChannelsTable = ({}) => {
|
|||
}}
|
||||
sx={{ py: 0, px: 0.5 }}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
<Pencil size="18" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -601,7 +602,7 @@ const ChannelsTable = ({}) => {
|
|||
onClick={() => deleteChannel(row.original.id)}
|
||||
sx={{ py: 0, px: 0.5 }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
<SquareMinus size="18" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<TextField
|
||||
{...params}
|
||||
|
|
@ -162,13 +163,17 @@ const StreamsTable = ({}) => {
|
|||
Header: ({ column }) => (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
options={m3uOptions}
|
||||
options={playlists.map((playlist) => ({
|
||||
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) => (
|
||||
<TextField
|
||||
{...params}
|
||||
|
|
@ -188,7 +193,7 @@ const StreamsTable = ({}) => {
|
|||
),
|
||||
},
|
||||
],
|
||||
[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))
|
||||
}
|
||||
>
|
||||
<PlaylistAddIcon fontSize="small" />
|
||||
<ListPlus size="18" fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -422,7 +462,7 @@ const StreamsTable = ({}) => {
|
|||
onClick={() => createChannelFromStream(row.original)}
|
||||
sx={{ py: 0, px: 0.5 }}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
<SquarePlus size="18" fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
|
@ -482,7 +522,7 @@ const StreamsTable = ({}) => {
|
|||
variant="contained"
|
||||
onClick={() => editStream()}
|
||||
>
|
||||
<AddIcon fontSize="small" />
|
||||
<SquarePlus size="18" fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete Streams">
|
||||
|
|
@ -526,7 +566,7 @@ const StreamsTable = ({}) => {
|
|||
*/
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [pagination, debouncedFilters]);
|
||||
}, [fetchData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue