mirror of
https://github.com/ajnart/homarr.git
synced 2025-10-26 08:06:12 +01:00
feat: add download functionality for board configuration files (#2111)
This commit is contained in:
@@ -72,6 +72,7 @@
|
||||
"@trpc/server": "^10.37.1",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"adm-zip": "^0.5.15",
|
||||
"axios": "^1.0.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"better-sqlite3": "^8.6.0",
|
||||
@@ -120,6 +121,7 @@
|
||||
"@next/eslint-plugin-next": "^13.4.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||
"@types/adm-zip": "^0.5.5",
|
||||
"@types/better-sqlite3": "^7.6.5",
|
||||
"@types/cookies": "^0.7.7",
|
||||
"@types/dockerode": "^3.3.9",
|
||||
|
||||
34
src/pages/api/download.ts
Normal file
34
src/pages/api/download.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import AdmZip from 'adm-zip';
|
||||
import fs from 'fs';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
if (!session.user.isAdmin) {
|
||||
return res.status(403).end();
|
||||
}
|
||||
|
||||
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
for (const file of files) {
|
||||
const data = await getFrontendConfig(file.replace('.json', ''));
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
zip.addFile(file, Buffer.from(content, 'utf-8'));
|
||||
}
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=board-configs.zip');
|
||||
res.setHeader('Content-Length', zipBuffer.length.toString());
|
||||
res.status(200).end(zipBuffer);
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure, useListState } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconBox,
|
||||
IconCategory,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
IconCursorText,
|
||||
IconDeviceFloppy,
|
||||
IconDotsVertical,
|
||||
IconDownload,
|
||||
IconFolderFilled,
|
||||
IconLock,
|
||||
IconLockOff,
|
||||
@@ -32,6 +34,8 @@ import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { RenameBoardModal } from '~/components/Dashboard/Modals/RenameBoard/RenameBoardModal';
|
||||
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
|
||||
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
@@ -42,16 +46,14 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { RenameBoardModal } from '~/components/Dashboard/Modals/RenameBoard/RenameBoardModal';
|
||||
import { useState } from 'react';
|
||||
|
||||
// Infer return type from the `getServerSideProps` function
|
||||
export default function BoardsPage({
|
||||
boards,
|
||||
session,
|
||||
boards,
|
||||
session,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const [openedRenameBoardModal, { open: openRenameBoardModal, close: closeRenameBoardModal }] = useDisclosure(false);
|
||||
const [openedRenameBoardModal, { open: openRenameBoardModal, close: closeRenameBoardModal }] =
|
||||
useDisclosure(false);
|
||||
const [renameBoardName, setRenameBoardName] = useState<{ boardName: string }>();
|
||||
|
||||
const { data, refetch } = api.boards.all.useQuery(undefined, {
|
||||
@@ -79,6 +81,11 @@ export default function BoardsPage({
|
||||
});
|
||||
|
||||
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
||||
const downloadAllBoards = async () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = `/api/download`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
const { t } = useTranslation('manage/boards');
|
||||
|
||||
@@ -90,22 +97,37 @@ export default function BoardsPage({
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<Modal opened={openedRenameBoardModal} onClose={closeRenameBoardModal}
|
||||
title={t('cards.menu.rename.modal.title', { name: renameBoardName?.boardName })}>
|
||||
<RenameBoardModal boardName={renameBoardName?.boardName as string} configNames={data.map(board => board.name)}
|
||||
onClose={closeRenameBoardModal} />
|
||||
<Modal
|
||||
opened={openedRenameBoardModal}
|
||||
onClose={closeRenameBoardModal}
|
||||
title={t('cards.menu.rename.modal.title', { name: renameBoardName?.boardName })}
|
||||
>
|
||||
<RenameBoardModal
|
||||
boardName={renameBoardName?.boardName as string}
|
||||
configNames={data.map((board) => board.name)}
|
||||
onClose={closeRenameBoardModal}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Group position="apart">
|
||||
<Title mb="xl">{t('pageTitle')}</Title>
|
||||
{session?.user.isAdmin && (
|
||||
<Button
|
||||
onClick={openCreateBoardModal}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
{t('buttons.create')}
|
||||
</Button>
|
||||
<Group>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={downloadAllBoards}
|
||||
leftIcon={<IconDownload size="1rem" />}
|
||||
>
|
||||
Download all boards
|
||||
</Button>
|
||||
<Button
|
||||
onClick={openCreateBoardModal}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
{t('buttons.create')}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -200,18 +222,20 @@ export default function BoardsPage({
|
||||
boardName: board.name,
|
||||
});
|
||||
}}
|
||||
icon={<IconCopy size={'1rem'} />}>
|
||||
icon={<IconCopy size={'1rem'} />}
|
||||
>
|
||||
{t('cards.menu.duplicate')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
setRenameBoardName({
|
||||
boardName: board.name as string
|
||||
boardName: board.name as string,
|
||||
});
|
||||
openRenameBoardModal();
|
||||
}}
|
||||
icon={<IconCursorText size={'1rem'} />}
|
||||
disabled={board.name === 'default'}>
|
||||
disabled={board.name === 'default'}
|
||||
>
|
||||
{t('cards.menu.rename.label')}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
@@ -264,7 +288,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const result = checkForSessionOrAskForLogin(
|
||||
context,
|
||||
session,
|
||||
() => session?.user.isAdmin == true,
|
||||
() => session?.user.isAdmin == true
|
||||
);
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
@@ -281,7 +305,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
manageNamespaces,
|
||||
context.locale,
|
||||
context.req,
|
||||
context.res,
|
||||
context.res
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -3022,6 +3022,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/adm-zip@npm:^0.5.5":
|
||||
version: 0.5.5
|
||||
resolution: "@types/adm-zip@npm:0.5.5"
|
||||
dependencies:
|
||||
"@types/node": "*"
|
||||
checksum: 808c25b8a1c2e1c594cf9b1514e7953105cf96e19e38aa7dc109ff2537bda7345b950ef1f4e54a6e824e5503e29d24b0ff6d0aa1ff9bd4afb79ef0ef2df9ebab
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/aria-query@npm:^5.0.1":
|
||||
version: 5.0.4
|
||||
resolution: "@types/aria-query@npm:5.0.4"
|
||||
@@ -4031,6 +4040,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"adm-zip@npm:^0.5.15":
|
||||
version: 0.5.15
|
||||
resolution: "adm-zip@npm:0.5.15"
|
||||
checksum: 23fc108ba0ead637cf8f89431bd152017d3d2eccbbac5e77bcfa3d0209029a53921d9735c5110c06b51cf223184f4cf2fdade975f20266a64183e94717a535f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aes-decrypter@npm:4.0.1, aes-decrypter@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "aes-decrypter@npm:4.0.1"
|
||||
@@ -7429,6 +7445,7 @@ __metadata:
|
||||
"@trpc/next": ^10.37.1
|
||||
"@trpc/react-query": ^10.37.1
|
||||
"@trpc/server": ^10.37.1
|
||||
"@types/adm-zip": ^0.5.5
|
||||
"@types/bcryptjs": ^2.4.2
|
||||
"@types/better-sqlite3": ^7.6.5
|
||||
"@types/cookies": ^0.7.7
|
||||
@@ -7447,6 +7464,7 @@ __metadata:
|
||||
"@vitest/coverage-c8": ^0.33.0
|
||||
"@vitest/coverage-v8": ^0.34.5
|
||||
"@vitest/ui": ^0.34.4
|
||||
adm-zip: ^0.5.15
|
||||
axios: ^1.0.0
|
||||
bcryptjs: ^2.4.3
|
||||
better-sqlite3: ^8.6.0
|
||||
|
||||
Reference in New Issue
Block a user