Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Sorting and grouping #276

Closed
wants to merge 13 commits into from
2 changes: 1 addition & 1 deletion bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
\Prologue\Alerts\AlertsServiceProvider::class,
])
->withRouting(
web: __DIR__.'/../routes/web.php',
web: __DIR__.'/../routes/base.php',
Poseidon281 marked this conversation as resolved.
Show resolved Hide resolved
// api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
// channels: __DIR__.'/../routes/channels.php',
Expand Down
7 changes: 7 additions & 0 deletions lang/en/dashboard/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
'no-other-servers' => 'There are no other servers to display.',
'no-servers-associated' => 'There are no servers associated with your account.',

'sorting_disabled' => 'Sorting mode disabled',
'sorting_enabled' => 'Sorting mode enabled',
'new_group_name' => 'New Group Name',
'add_group' => 'Add Group',
'delete_group' => 'Delete Current Group',
'move_server' => 'Move Server',

'content_tabs' => 'Content tabs',
'overview' => 'Overview',
'heading' => 'Welcome to Pelican!',
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"i18next-multiload-backend-adapter": "^1.0.0",
"qrcode.react": "^1.0.1",
"react": "^16.14.0",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^4.2.0",
"react-dom": "npm:@hot-loader/react-dom",
"react-fast-compare": "^3.2.0",
Expand Down Expand Up @@ -79,6 +80,7 @@
"@types/node": "^14.11.10",
"@types/qrcode.react": "^1.0.1",
"@types/react": "^16.14.0",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.16",
"@types/react-redux": "^7.1.1",
Expand Down
224 changes: 194 additions & 30 deletions resources/scripts/components/dashboard/DashboardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import Switch from '@/components/elements/Switch';
import tw from 'twin.macro';
import useSWR from 'swr';
import { PaginatedResult } from '@/api/http';
import Pagination from '@/components/elements/Pagination';
import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';

export default () => {
const [allowDragDrop, setAllowDragDrop] = useState(true);
const { t } = useTranslation('dashboard/index');

const { search } = useLocation();
const defaultPage = Number(new URLSearchParams(search).get('page') || '1');

Expand All @@ -32,17 +32,30 @@ export default () => {
() => getServers({ page, type: showOnlyAdmin && rootAdmin ? 'admin' : undefined })
);

const [groups, setGroups] = useState<{ [key: string]: string[] }>({ default: [] });
const [currentGroup, setCurrentGroup] = useState('default');
const [newGroupName, setNewGroupName] = useState('');
const [showMoveOptions, setShowMoveOptions] = useState<{ [key: string]: boolean }>({});

useEffect(() => {
if (!servers) return;
if (servers.pagination.currentPage > 1 && !servers.items.length) {
if (servers) {
const allServers = servers.items.map((server) => server.uuid);
setGroups((prevGroups) => {
if (!prevGroups['default'].length) {
return { ...prevGroups, default: allServers };
}
return prevGroups;
});
}
}, [servers]);

useEffect(() => {
if (servers?.pagination?.currentPage && servers.pagination.currentPage > 1 && servers.items?.length === 0) {
setPage(1);
}
}, [servers?.pagination.currentPage]);
}, [servers?.pagination?.currentPage]);

useEffect(() => {
// Don't use react-router to handle changing this part of the URL, otherwise it
// triggers a needless re-render. We just want to track this in the URL incase the
// user refreshes the page.
window.history.replaceState(null, document.title, `/${page <= 1 ? '' : `?page=${page}`}`);
}, [page]);

Expand All @@ -51,36 +64,187 @@ export default () => {
if (!error) clearFlashes('dashboard');
}, [error]);

useEffect(() => {
const savedGroups = localStorage.getItem(`groups:${uuid}`);
if (savedGroups) {
setGroups(JSON.parse(savedGroups));
}
}, [uuid]);
Poseidon281 marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
localStorage.setItem(`groups:${uuid}`, JSON.stringify(groups));
}, [groups]);

const addGroup = () => {
if (newGroupName && !groups[newGroupName]) {
setGroups({ ...groups, [newGroupName]: [] });
setNewGroupName('');
}
};

const deleteGroup = (group: string) => {
const updatedGroups = { ...groups };
delete updatedGroups[group];
setGroups(updatedGroups);
if (currentGroup === group) {
setCurrentGroup('default');
}
};

const moveServerToGroup = (serverUuid: string, targetGroup: string) => {
if (!groups[targetGroup]) {
console.warn(`Group ${targetGroup} does not exist`);
return;
}

const updatedGroups = { ...groups };
const currentGroup = Object.keys(updatedGroups).find((group) => updatedGroups[group].includes(serverUuid));
if (currentGroup) {
updatedGroups[currentGroup] = updatedGroups[currentGroup].filter((uuid) => uuid !== serverUuid);
}
updatedGroups[targetGroup].push(serverUuid);
setGroups(updatedGroups);
setShowMoveOptions((prevState) => ({ ...prevState, [serverUuid]: false }));
};

const onDragEnd = (result: DropResult) => {
const { source, destination } = result;

if (!destination) {
return;
}

const newOrder = Array.from(groups[currentGroup]);
newOrder.splice(source.index, 1);
newOrder.splice(destination.index, 0, groups[currentGroup][source.index]);

setGroups({ ...groups, [currentGroup]: newOrder });
};

const serversOrder = groups[currentGroup];

return (
<PageContentBlock title={t('title')} showFlashKey={'dashboard'}>
{rootAdmin && (
<div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{showOnlyAdmin ? t('showing-others-servers') : t('showing-your-servers')}
</p>
<Switch
name={'show_all_servers'}
defaultChecked={showOnlyAdmin}
onChange={() => setShowOnlyAdmin((s) => !s)}
/>
<div css={tw`mb-4 flex justify-between items-center`}>
<div css={tw`flex items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{allowDragDrop ? t('sorting_disabled') : t('sorting_enabled')}
</p>
<Switch
name={'allow_drag_drop'}
defaultChecked={!allowDragDrop}
onChange={() => setAllowDragDrop(!allowDragDrop)}
/>
</div>
<div css={tw`flex items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{showOnlyAdmin ? t('showing-others-servers') : t('showing-your-servers')}
</p>
<Switch
name={'show_all_servers'}
defaultChecked={showOnlyAdmin}
onChange={() => setShowOnlyAdmin((s) => !s)}
/>
</div>
</div>
)}
<div css={tw`mb-4 flex justify-between items-center`}>
<div css={tw`flex items-center`}>
<select
css={tw`p-2 border border-neutral-600 rounded bg-neutral-700 text-white`}
value={currentGroup}
onChange={(e) => setCurrentGroup(e.target.value)}
>
{Object.keys(groups).map((group) => (
<option key={group} value={group}>
{group}
</option>
))}
</select>
<input
css={tw`ml-2 p-2 border border-neutral-600 rounded bg-neutral-700 text-white`}
type='text'
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
placeholder={t('new_group_name')}
/>
<button css={tw`ml-2 p-2 bg-blue-600 text-white rounded`} onClick={addGroup}>
{t('add_group')}
</button>
</div>
{currentGroup !== 'default' && (
<button css={tw`p-2 bg-red-600 text-white rounded`} onClick={() => deleteGroup(currentGroup)}>
{t('delete_group')}
</button>
)}
</div>
{!servers ? (
<Spinner centered size={'large'} />
) : (
<Pagination data={servers} onPageSelect={setPage}>
{({ items }) =>
items.length > 0 ? (
items.map((server, index) => (
<ServerRow key={server.uuid} server={server} css={index > 0 ? tw`mt-2` : undefined} />
))
) : (
<p css={tw`text-center text-sm text-neutral-400`}>
{showOnlyAdmin ? t('no-other-servers') : t('no-servers-associated')}
</p>
)
}
</Pagination>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='servers'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{serversOrder.map((serverUuid, index) => {
const server = servers.items.find((s) => s.uuid === serverUuid);
if (!server) {
console.warn(`Server with uuid ${serverUuid} not found`);
return null;
}
return (
<Draggable
key={server.uuid}
draggableId={server.uuid}
index={index}
isDragDisabled={allowDragDrop}
>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
css={tw`mb-2 p-4 text-white`}
>
<ServerRow server={server} />
<div css={tw`mt-2 flex justify-end`}>
<button
css={tw`p-2 bg-blue-600 text-white rounded`}
onClick={() =>
setShowMoveOptions((prevState) => ({
...prevState,
[server.uuid]: !prevState[server.uuid],
}))
}
>
{t('move_server')}
</button>
{showMoveOptions[server.uuid] && (
<select
css={tw`ml-2 p-2 border border-neutral-600 rounded bg-neutral-700 text-white`}
value={currentGroup}
onChange={(e) =>
moveServerToGroup(server.uuid, e.target.value)
}
>
{Object.keys(groups).map((group) => (
<option key={group} value={group}>
{group}
</option>
))}
</select>
)}
</div>
</div>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
</PageContentBlock>
);
Expand Down
4 changes: 2 additions & 2 deletions resources/scripts/components/elements/PageContentBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ const PageContentBlock: React.FC<PageContentBlockProps> = ({ title, showFlashKey
target={'_blank'}
css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
>
Panel
Pelican Panel
</a>
&nbsp;Pelican&copy; 2024 - {new Date().getFullYear()}
&nbsp;&copy; 2024 - {new Date().getFullYear()}
</p>
</ContentContainer>
</>
Expand Down
Loading
Loading