mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
attempt at table rewrite for efficient virtualized table
This commit is contained in:
parent
eb48083cce
commit
af638326e1
10 changed files with 814 additions and 638 deletions
59
frontend/package-lock.json
generated
59
frontend/package-lock.json
generated
|
|
@ -16,6 +16,7 @@
|
|||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/notifications": "^7.17.2",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"allotment": "^1.20.3",
|
||||
"axios": "^1.8.2",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -31,6 +32,7 @@
|
|||
"react-draggable": "^4.4.6",
|
||||
"react-pro-sidebar": "^1.1.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.11",
|
||||
"recharts": "^2.15.1",
|
||||
"video.js": "^8.21.0",
|
||||
|
|
@ -1741,12 +1743,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-table": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
|
||||
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
|
||||
"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.20.5"
|
||||
"@tanstack/table-core": "8.21.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
|
@ -1778,9 +1780,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tanstack/table-core": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
|
@ -3452,6 +3454,39 @@
|
|||
"react-dom": ">=18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mantine-react-table/node_modules/@tanstack/react-table": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz",
|
||||
"integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/table-core": "8.20.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/mantine-react-table/node_modules/@tanstack/table-core": {
|
||||
"version": "8.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz",
|
||||
"integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
|
@ -4081,6 +4116,16 @@
|
|||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"@mantine/hooks": "^7.17.2",
|
||||
"@mantine/notifications": "^7.17.2",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"allotment": "^1.20.3",
|
||||
"axios": "^1.8.2",
|
||||
"clsx": "^2.1.1",
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
"react-draggable": "^4.4.6",
|
||||
"react-pro-sidebar": "^1.1.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "^1.8.11",
|
||||
"recharts": "^2.15.1",
|
||||
"video.js": "^8.21.0",
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export const WebsocketProvider = ({ children }) => {
|
|||
message: 'EPG match is complete!',
|
||||
color: 'green.5',
|
||||
});
|
||||
fetchChannels();
|
||||
// fetchChannels();
|
||||
fetchEPGData();
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ export default class API {
|
|||
});
|
||||
|
||||
// Optionally refesh the channel list in Zustand
|
||||
await useChannelsStore.getState().fetchChannels();
|
||||
// await useChannelsStore.getState().fetchChannels();
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -604,7 +604,7 @@ export default class API {
|
|||
usePlaylistsStore.getState().removePlaylists([id]);
|
||||
// @TODO: MIGHT need to optimize this later if someone has thousands of channels
|
||||
// but I'm feeling laze right now
|
||||
useChannelsStore.getState().fetchChannels();
|
||||
// useChannelsStore.getState().fetchChannels();
|
||||
} catch (e) {
|
||||
errorNotification(`Failed to delete playlist ${id}`, e);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,86 @@
|
|||
// HeadlessChannelsTable.jsx
|
||||
import React, { useMemo, useState, useCallback, useRef } from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
flexRender,
|
||||
getExpandedRowModel,
|
||||
} from '@tanstack/react-table';
|
||||
import {
|
||||
Table,
|
||||
Box,
|
||||
Checkbox,
|
||||
ActionIcon,
|
||||
ScrollArea,
|
||||
} from '@mantine/core';
|
||||
import { ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import ChannelsTableRow from './ChannelsTableRow';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
|
||||
const ChannelsTableBody = ({ rows, height, onEdit, onDelete, onPreview, onRecord, virtualizedItems }) => {
|
||||
const rowHeight = 48;
|
||||
|
||||
// return (
|
||||
// <tbody>
|
||||
// <AutoSizer disableWidth>
|
||||
// {({ height }) => (
|
||||
// <List
|
||||
// height={height}
|
||||
// itemCount={rows.length}
|
||||
// itemSize={rowHeight}
|
||||
// width="100%"
|
||||
// >
|
||||
// {({ index, style }) => {
|
||||
// const row = rows[index];
|
||||
// return (
|
||||
// <React.Fragment key={row.id}>
|
||||
// <ChannelsTableRow
|
||||
// row={row}
|
||||
// onEdit={onEdit}
|
||||
// onDelete={onDelete}
|
||||
// onPreview={onPreview}
|
||||
// onRecord={onRecord}
|
||||
// />
|
||||
// {row.getIsExpanded() && <ChannelsDetailPanel row={row} />}
|
||||
// </React.Fragment>
|
||||
// );
|
||||
// }}
|
||||
// </List>
|
||||
// )}
|
||||
// </AutoSizer>
|
||||
// </tbody>
|
||||
// );
|
||||
|
||||
return (
|
||||
<Table.Tbody style={{
|
||||
position: 'relative',
|
||||
// display: 'block',
|
||||
height: `${height}px`,
|
||||
// overflowY: 'auto',
|
||||
}}>
|
||||
{virtualizedItems.map((virtualRow, index) => {
|
||||
const row = rows[virtualRow.index]
|
||||
return (
|
||||
<ChannelsTableRow
|
||||
row={row}
|
||||
virtualRow={virtualRow}
|
||||
index={index}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
onPreview={onPreview}
|
||||
onRecord={onRecord}
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelsTableBody;
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// HeadlessChannelsTable.jsx
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
flexRender,
|
||||
getExpandedRowModel,
|
||||
} from '@tanstack/react-table';
|
||||
import {
|
||||
Table,
|
||||
Box,
|
||||
Checkbox,
|
||||
ActionIcon,
|
||||
ScrollArea,
|
||||
Center,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import useSettingsStore from '../../../store/settings';
|
||||
import useChannelsStore from '../../../store/channels';
|
||||
|
||||
const ExpandIcon = ({ row, toggle }) => (
|
||||
<ActionIcon size="xs" onClick={toggle}>
|
||||
{row.getIsExpanded() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</ActionIcon>
|
||||
);
|
||||
|
||||
const ChannelsTableRow = ({ row, virtualRow, index, style, onEdit, onDelete, onPreview, onRecord }) => {
|
||||
return (
|
||||
<Table.Tr style={{
|
||||
...style,
|
||||
position: 'absolute',
|
||||
// top: 0,
|
||||
display: 'table',
|
||||
tableLayout: 'fixed',
|
||||
width: '100%',
|
||||
}}>
|
||||
{row.getVisibleCells().map(cell => {
|
||||
return (
|
||||
<Table.Td key={cell.id} align={cell.column.columnDef.meta?.align} style={{
|
||||
padding: 0,
|
||||
width: cell.column.getSize(),
|
||||
minWidth: cell.column.columnDef.meta?.minWidth,
|
||||
maxWidth: cell.column.columnDef.meta?.maxWidth,
|
||||
// maxWidth: cell.column.getSize(),
|
||||
}}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</Table.Td>
|
||||
)
|
||||
})}
|
||||
</Table.Tr>
|
||||
)
|
||||
};
|
||||
|
||||
export default ChannelsTableRow
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
export default () => {
|
||||
return (
|
||||
<Box
|
||||
style={{
|
||||
paddingTop: 20,
|
||||
bgcolor: theme.palette.background.paper,
|
||||
}}
|
||||
>
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
width: '55%',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 400,
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
letterSpacing: '-0.3px',
|
||||
color: theme.palette.text.secondary,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
It’s recommended to create channels after adding your M3U or
|
||||
streams.
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: 'Inter, sans-serif',
|
||||
fontWeight: 400,
|
||||
fontSize: '16px',
|
||||
lineHeight: '24px',
|
||||
letterSpacing: '-0.2px',
|
||||
color: theme.palette.text.secondary,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
You can still create channels without streams if you’d like,
|
||||
and map them later.
|
||||
</Text>
|
||||
<Button
|
||||
leftSection={<SquarePlus size={18} />}
|
||||
variant="light"
|
||||
size="xs"
|
||||
onClick={() => editChannel()}
|
||||
color="gray"
|
||||
style={{
|
||||
marginTop: 20,
|
||||
borderWidth: '1px',
|
||||
borderColor: 'gray',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
Create Channel
|
||||
</Button>
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
<Center>
|
||||
<Box
|
||||
component="img"
|
||||
src={ghostImage}
|
||||
alt="Ghost"
|
||||
style={{
|
||||
paddingTop: 30,
|
||||
width: '120px',
|
||||
height: 'auto',
|
||||
opacity: 0.2,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ const useAuthStore = create((set, get) => ({
|
|||
|
||||
initData: async () => {
|
||||
await Promise.all([
|
||||
useChannelsStore.getState().fetchChannels(),
|
||||
// useChannelsStore.getState().fetchChannels(),
|
||||
useChannelsStore.getState().fetchChannelGroups(),
|
||||
useChannelsStore.getState().fetchLogos(),
|
||||
useChannelsStore.getState().fetchChannelProfiles(),
|
||||
|
|
|
|||
|
|
@ -310,75 +310,75 @@ const useChannelsStore = create((set, get) => ({
|
|||
})),
|
||||
|
||||
setChannelStats: (stats) => {
|
||||
return set((state) => {
|
||||
const {
|
||||
channels,
|
||||
stats: currentStats,
|
||||
activeChannels: oldChannels,
|
||||
activeClients: oldClients,
|
||||
channelsByUUID,
|
||||
} = state;
|
||||
// return set((state) => {
|
||||
// const {
|
||||
// channels,
|
||||
// stats: currentStats,
|
||||
// activeChannels: oldChannels,
|
||||
// activeClients: oldClients,
|
||||
// channelsByUUID,
|
||||
// } = state;
|
||||
|
||||
const newClients = {};
|
||||
const newChannels = stats.channels.reduce((acc, ch) => {
|
||||
acc[ch.channel_id] = ch;
|
||||
// const newClients = {};
|
||||
// const newChannels = stats.channels.reduce((acc, ch) => {
|
||||
// acc[ch.channel_id] = ch;
|
||||
|
||||
if (currentStats.channels) {
|
||||
if (oldChannels[ch.channel_id] === undefined) {
|
||||
notifications.show({
|
||||
title: 'New channel streaming',
|
||||
message: channels[channelsByUUID[ch.channel_id]].name,
|
||||
color: 'blue.5',
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (currentStats.channels) {
|
||||
// if (oldChannels[ch.channel_id] === undefined) {
|
||||
// notifications.show({
|
||||
// title: 'New channel streaming',
|
||||
// message: channels[channelsByUUID[ch.channel_id]].name,
|
||||
// color: 'blue.5',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
ch.clients.map((client) => {
|
||||
newClients[client.client_id] = client;
|
||||
// This check prevents the notifications if streams are active on page load
|
||||
if (currentStats.channels) {
|
||||
if (oldClients[client.client_id] === undefined) {
|
||||
notifications.show({
|
||||
title: 'New client started streaming',
|
||||
message: `Client streaming from ${client.ip_address}`,
|
||||
color: 'blue.5',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
// ch.clients.map((client) => {
|
||||
// newClients[client.client_id] = client;
|
||||
// // This check prevents the notifications if streams are active on page load
|
||||
// if (currentStats.channels) {
|
||||
// if (oldClients[client.client_id] === undefined) {
|
||||
// notifications.show({
|
||||
// title: 'New client started streaming',
|
||||
// message: `Client streaming from ${client.ip_address}`,
|
||||
// color: 'blue.5',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
// return acc;
|
||||
// }, {});
|
||||
|
||||
// This check prevents the notifications if streams are active on page load
|
||||
if (currentStats.channels) {
|
||||
for (const uuid in oldChannels) {
|
||||
if (newChannels[uuid] === undefined) {
|
||||
notifications.show({
|
||||
title: 'Channel streaming stopped',
|
||||
message: channels[channelsByUUID[uuid]].name,
|
||||
color: 'blue.5',
|
||||
});
|
||||
}
|
||||
}
|
||||
// // This check prevents the notifications if streams are active on page load
|
||||
// if (currentStats.channels) {
|
||||
// for (const uuid in oldChannels) {
|
||||
// if (newChannels[uuid] === undefined) {
|
||||
// notifications.show({
|
||||
// title: 'Channel streaming stopped',
|
||||
// message: channels[channelsByUUID[uuid]].name,
|
||||
// color: 'blue.5',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
for (const clientId in oldClients) {
|
||||
if (newClients[clientId] === undefined) {
|
||||
notifications.show({
|
||||
title: 'Client stopped streaming',
|
||||
message: `Client stopped streaming from ${oldClients[clientId].ip_address}`,
|
||||
color: 'blue.5',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (const clientId in oldClients) {
|
||||
// if (newClients[clientId] === undefined) {
|
||||
// notifications.show({
|
||||
// title: 'Client stopped streaming',
|
||||
// message: `Client stopped streaming from ${oldClients[clientId].ip_address}`,
|
||||
// color: 'blue.5',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return {
|
||||
stats,
|
||||
activeChannels: newChannels,
|
||||
activeClients: newClients,
|
||||
};
|
||||
});
|
||||
// return {
|
||||
// stats,
|
||||
// activeChannels: newChannels,
|
||||
// activeClients: newClients,
|
||||
// };
|
||||
// });
|
||||
},
|
||||
|
||||
fetchRecordings: async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue