diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 313d2b4b..110c332f 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -306,6 +306,12 @@ const ChannelsTable = ({}) => { const [isBulkDelete, setIsBulkDelete] = useState(false); const [channelToDelete, setChannelToDelete] = useState(null); + // Column sizing state for resizable columns + const [columnSizing, setColumnSizing] = useLocalStorage( + 'channels-table-column-sizing', + {} + ); + // M3U and EPG URL configuration state const [m3uParams, setM3uParams] = useState({ cachedlogos: true, @@ -697,14 +703,17 @@ const ChannelsTable = ({}) => { { id: 'expand', size: 20, + enableResizing: false, }, { id: 'select', size: 30, + enableResizing: false, }, { id: 'enabled', size: 45, + enableResizing: false, cell: ({ row, table }) => { return ( { { id: 'channel_number', accessorKey: 'channel_number', - size: 40, + size: columnSizing.channel_number || 40, + minSize: 30, + maxSize: 100, cell: ({ getValue }) => { const value = getValue(); // Format as integer if no decimal component @@ -739,6 +750,8 @@ const ChannelsTable = ({}) => { { id: 'name', accessorKey: 'name', + size: columnSizing.name || 200, + minSize: 100, cell: ({ getValue }) => ( { ); }, - size: 120, + size: columnSizing.epg || 120, + minSize: 80, + maxSize: 200, }, { id: 'channel_group', @@ -812,7 +827,9 @@ const ChannelsTable = ({}) => { {getValue()} ), - size: 175, + size: columnSizing.channel_group || 175, + minSize: 100, + maxSize: 300, }, { id: 'logo', @@ -821,6 +838,9 @@ const ChannelsTable = ({}) => { return row.logo_id; }, size: 75, + minSize: 50, + maxSize: 120, + enableResizing: false, header: '', cell: ({ getValue }) => { const logoId = getValue(); @@ -839,6 +859,7 @@ const ChannelsTable = ({}) => { { id: 'actions', size: tableSize == 'compact' ? 75 : 100, + enableResizing: false, header: '', cell: ({ row }) => ( { ), }, ], - [selectedProfileId, channelGroups, logos, theme] + [selectedProfileId, channelGroups, logos, theme, columnSizing] ); const renderHeaderCell = (header) => { @@ -953,6 +974,12 @@ const ChannelsTable = ({}) => { manualFiltering: true, enableRowSelection: true, onRowSelectionChange: onRowSelectionChange, + state: { + columnSizing, + pagination, + sorting, + }, + onColumnSizingChange: setColumnSizing, getExpandedRowHeight: (row) => { return 20 + 28 * row.original.streams.length; }, @@ -1301,7 +1328,7 @@ const ChannelsTable = ({}) => { style={{ flex: 1, overflowY: 'auto', - overflowX: 'hidden', + overflowX: 'auto', border: 'solid 1px rgb(68,68,68)', borderRadius: 'var(--mantine-radius-default)', }} diff --git a/frontend/src/components/tables/CustomTable/CustomTable.jsx b/frontend/src/components/tables/CustomTable/CustomTable.jsx index 7d811b6f..ee665188 100644 --- a/frontend/src/components/tables/CustomTable/CustomTable.jsx +++ b/frontend/src/components/tables/CustomTable/CustomTable.jsx @@ -1,6 +1,6 @@ import { Box, Flex } from '@mantine/core'; import CustomTableHeader from './CustomTableHeader'; -import { useCallback, useState, useRef } from 'react'; +import { useCallback, useState, useRef, useMemo } from 'react'; import { flexRender } from '@tanstack/react-table'; import table from '../../../helpers/table'; import CustomTableBody from './CustomTableBody'; @@ -9,11 +9,28 @@ import useLocalStorage from '../../../hooks/useLocalStorage'; const CustomTable = ({ table }) => { const [tableSize, _] = useLocalStorage('table-size', 'default'); + // Get column sizing state for dependency tracking + const columnSizing = table.getState().columnSizing; + + // Calculate minimum table width reactively based on column sizes + const minTableWidth = useMemo(() => { + const headerGroups = table.getHeaderGroups(); + if (!headerGroups || headerGroups.length === 0) return 0; + + const width = + headerGroups[0]?.headers.reduce((total, header) => { + return total + header.getSize(); + }, 0) || 0; + + return width; + }, [table, columnSizing]); + return ( { + if (rows.length === 0) return 0; + + return rows[0].getVisibleCells().reduce((total, cell) => { + // Only count columns with fixed sizes, flexible columns will expand + const columnSize = cell.column.columnDef.size + ? cell.column.getSize() + : cell.column.columnDef.minSize || 150; // Default min for flexible columns + return total + columnSize; + }, 0); + }, [rows]); + const renderTableBodyContents = () => { const virtualized = false; @@ -94,6 +108,7 @@ const CustomTableBody = ({ style={{ display: 'flex', width: '100%', + minWidth: '100%', // Force full width ...(row.getIsSelected() && { backgroundColor: '#163632', }), @@ -101,16 +116,24 @@ const CustomTableBody = ({ }} > {row.getVisibleCells().map((cell) => { + const hasFixedSize = cell.column.columnDef.size; + const isFlexible = !hasFixedSize; + return ( diff --git a/frontend/src/components/tables/CustomTable/CustomTableHeader.jsx b/frontend/src/components/tables/CustomTable/CustomTableHeader.jsx index 1e32e258..92643fc9 100644 --- a/frontend/src/components/tables/CustomTable/CustomTableHeader.jsx +++ b/frontend/src/components/tables/CustomTable/CustomTableHeader.jsx @@ -1,6 +1,6 @@ import { Box, Center, Checkbox, Flex } from '@mantine/core'; import { flexRender } from '@tanstack/react-table'; -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; const CustomTableHeader = ({ getHeaderGroups, @@ -40,6 +40,25 @@ const CustomTableHeader = ({ } }; + // Get header groups for dependency tracking + const headerGroups = getHeaderGroups(); + + // Calculate minimum width based only on fixed-size columns + const minTableWidth = useMemo(() => { + if (!headerGroups || headerGroups.length === 0) return 0; + + const width = + headerGroups[0]?.headers.reduce((total, header) => { + // Only count columns with fixed sizes, flexible columns will expand + const columnSize = header.column.columnDef.size + ? header.getSize() + : header.column.columnDef.minSize || 150; // Default min for flexible columns + return total + columnSize; + }, 0) || 0; + + return width; + }, [headerGroups]); + return ( {headerGroup.headers.map((header) => { return ( @@ -62,11 +85,17 @@ const CustomTableHeader = ({ className="th" key={header.id} style={{ - flex: header.column.columnDef.size ? '0 0 auto' : '1 1 0', - width: header.column.columnDef.size - ? header.getSize() - : undefined, - minWidth: 0, + ...(header.column.columnDef.grow + ? { + flex: '1 1 0%', + minWidth: 0, + } + : { + flex: `0 0 ${header.getSize ? header.getSize() : 150}px`, + width: `${header.getSize ? header.getSize() : 150}px`, + maxWidth: `${header.getSize ? header.getSize() : 150}px`, + }), + position: 'relative', // ...(tableCellProps && tableCellProps({ cell: header })), }} > @@ -76,10 +105,46 @@ const CustomTableHeader = ({ ...(header.column.columnDef.style && header.column.columnDef.style), height: '100%', + paddingRight: header.column.getCanResize() ? '8px' : '0px', // Add padding for resize handle }} > {renderHeaderCell(header)} + {header.column.getCanResize() && ( +
{ + e.target.style.opacity = '1'; + e.target.style.backgroundColor = '#6b7280'; + }} + onMouseLeave={(e) => { + if (!header.column.getIsResizing()) { + e.target.style.opacity = '0.5'; + e.target.style.backgroundColor = 'transparent'; + } + }} + /> + )} ); })} diff --git a/frontend/src/components/tables/CustomTable/index.jsx b/frontend/src/components/tables/CustomTable/index.jsx index 3be2dc2f..ec5182c9 100644 --- a/frontend/src/components/tables/CustomTable/index.jsx +++ b/frontend/src/components/tables/CustomTable/index.jsx @@ -18,6 +18,8 @@ const useTable = ({ onRowSelectionChange = null, getExpandedRowHeight = null, state = [], + columnSizing, + setColumnSizing, ...options }) => { const [selectedTableIds, setSelectedTableIds] = useState([]); @@ -85,15 +87,21 @@ const useTable = ({ const table = useReactTable({ defaultColumn: { - size: undefined, minSize: 0, + maxSize: Number.MAX_SAFE_INTEGER, + size: 150, }, ...options, state: { - data: options.data, + ...options.state, selectedTableIds, + ...(columnSizing && { columnSizing }), }, + onStateChange: options.onStateChange, + ...(setColumnSizing && { onColumnSizingChange: setColumnSizing }), getCoreRowModel: options.getCoreRowModel ?? getCoreRowModel(), + enableColumnResizing: true, + columnResizeMode: 'onChange', }); const selectedTableIdsSet = useMemo( diff --git a/frontend/src/components/tables/EPGsTable.jsx b/frontend/src/components/tables/EPGsTable.jsx index 9c40a14a..7cac7d7d 100644 --- a/frontend/src/components/tables/EPGsTable.jsx +++ b/frontend/src/components/tables/EPGsTable.jsx @@ -187,14 +187,15 @@ const EPGsTable = () => { size: 200, }, { - header: 'Source Type', + header: 'Type', accessorKey: 'source_type', - size: 150, + size: 100, }, { header: 'URL / API Key / File Path', accessorKey: 'url', enableSorting: false, + minSize: 250, cell: ({ cell, row }) => { const value = cell.getValue() || @@ -220,7 +221,7 @@ const EPGsTable = () => { { header: 'Status', accessorKey: 'status', - size: 150, + size: 100, cell: ({ row }) => { const data = row.original; @@ -236,6 +237,8 @@ const EPGsTable = () => { header: 'Status Message', accessorKey: 'last_message', enableSorting: false, + minSize: 250, + grow: true, cell: ({ row }) => { const data = row.original; diff --git a/frontend/src/components/tables/LogosTable.jsx b/frontend/src/components/tables/LogosTable.jsx index 46c29e4c..0c4f32b4 100644 --- a/frontend/src/components/tables/LogosTable.jsx +++ b/frontend/src/components/tables/LogosTable.jsx @@ -394,7 +394,7 @@ const LogosTable = () => { { header: 'Name', accessorKey: 'name', - size: 200, + size: 250, cell: ({ getValue }) => ( {getValue()} @@ -479,6 +479,7 @@ const LogosTable = () => { { header: 'URL', accessorKey: 'url', + grow: true, cell: ({ getValue }) => ( { sortable: true, }, { - header: 'Account Type', + header: 'Type', accessorKey: 'account_type', sortable: true, - size: 150, + size: 100, cell: ({ cell }) => { const value = cell.getValue(); return value === 'XC' ? 'XC' : 'M3U'; @@ -443,6 +443,7 @@ const M3UTable = () => { { header: 'URL / File', accessorKey: 'server_url', + size: 250, cell: ({ cell, row }) => { const value = cell.getValue() || row.original.file_path || ''; return ( @@ -461,16 +462,10 @@ const M3UTable = () => { ); }, }, - { - header: 'Max Streams', - accessorKey: 'max_streams', - sortable: true, - size: 150, - }, { header: 'Status', accessorKey: 'status', - size: 150, + size: 100, cell: ({ cell }) => { const value = cell.getValue(); if (!value) return null; @@ -486,6 +481,8 @@ const M3UTable = () => { { header: 'Status Message', accessorKey: 'last_message', + grow: true, + minSize: 250, cell: ({ cell, row }) => { const value = cell.getValue(); const data = row.original; @@ -569,6 +566,12 @@ const M3UTable = () => { ); }, }, + { + header: 'Max Streams', + accessorKey: 'max_streams', + sortable: true, + size: 125, + }, { header: 'Updated', accessorKey: 'updated_at', diff --git a/frontend/src/components/tables/StreamProfilesTable.jsx b/frontend/src/components/tables/StreamProfilesTable.jsx index 76be0462..da2acd30 100644 --- a/frontend/src/components/tables/StreamProfilesTable.jsx +++ b/frontend/src/components/tables/StreamProfilesTable.jsx @@ -72,7 +72,7 @@ const StreamProfiles = () => { { header: 'Name', accessorKey: 'name', - size: 150, + size: 175, cell: ({ cell }) => (
{ { header: 'Command', accessorKey: 'command', - size: 150, + size: 100, cell: ({ cell }) => (
{ { header: 'Parameters', accessorKey: 'parameters', - // size: 200, + grow: true, cell: ({ cell }) => (
{ channel_group: '', m3u_account: '', }); + const [columnSizing, setColumnSizing] = useLocalStorage( + 'streams-table-column-sizing', + {} + ); const debouncedFilters = useDebounce(filters, 500, () => { // Reset to first page whenever filters change to avoid "Invalid page" errors setPagination((prev) => ({ @@ -256,15 +260,16 @@ const StreamsTable = ({}) => { () => [ { id: 'actions', - size: tableSize == 'compact' ? 60 : 80, + size: columnSizing.actions || (tableSize == 'compact' ? 60 : 80), }, { id: 'select', - size: 30, + size: columnSizing.select || 30, }, { header: 'Name', accessorKey: 'name', + size: columnSizing.name || 200, cell: ({ getValue }) => ( { channelGroups[row.channel_group] ? channelGroups[row.channel_group].name : '', + size: columnSizing.group || 150, cell: ({ getValue }) => ( { }, { id: 'm3u', - size: 150, + size: columnSizing.m3u || 150, accessorFn: (row) => playlists.find((playlist) => playlist.id === row.m3u_account)?.name, cell: ({ getValue }) => ( @@ -315,7 +321,7 @@ const StreamsTable = ({}) => { ), }, ], - [channelGroups, playlists] + [channelGroups, playlists, columnSizing, tableSize] ); /** @@ -652,6 +658,8 @@ const StreamsTable = ({}) => { filters, pagination, sorting, + columnSizing, + setColumnSizing, onRowSelectionChange: onRowSelectionChange, manualPagination: true, manualSorting: true, @@ -855,7 +863,7 @@ const StreamsTable = ({}) => { style={{ flex: 1, overflowY: 'auto', - overflowX: 'hidden', + overflowX: 'auto', border: 'solid 1px rgb(68,68,68)', borderRadius: 'var(--mantine-radius-default)', }} diff --git a/frontend/src/components/tables/UserAgentsTable.jsx b/frontend/src/components/tables/UserAgentsTable.jsx index 265ed125..48573926 100644 --- a/frontend/src/components/tables/UserAgentsTable.jsx +++ b/frontend/src/components/tables/UserAgentsTable.jsx @@ -61,11 +61,13 @@ const UserAgentsTable = () => { { header: 'Name', accessorKey: 'name', + size: 125, }, { header: 'User-Agent', accessorKey: 'user_agent', enableSorting: false, + grow: true, cell: ({ cell }) => (
{ { header: 'Email', accessorKey: 'email', + grow: true, cell: ({ getValue }) => ( { const authUser = useAuthStore((s) => s.user); + const [allotmentSizes, setAllotmentSizes] = useLocalStorage( + 'channels-splitter-sizes', + [50, 50] + ); + + const handleSplitChange = (sizes) => { + setAllotmentSizes(sizes); + }; + + const handleResize = (sizes) => { + setAllotmentSizes(sizes); + }; if (!authUser.id) { return <>; @@ -30,10 +43,12 @@ const ChannelsPage = () => { }} >