mirror of
https://github.com/ajnart/homarr.git
synced 2025-10-30 01:56:05 +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",
|
"@trpc/server": "^10.37.1",
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"adm-zip": "^0.5.15",
|
||||||
"axios": "^1.0.0",
|
"axios": "^1.0.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"better-sqlite3": "^8.6.0",
|
"better-sqlite3": "^8.6.0",
|
||||||
@@ -120,6 +121,7 @@
|
|||||||
"@next/eslint-plugin-next": "^13.4.5",
|
"@next/eslint-plugin-next": "^13.4.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
"@trivago/prettier-plugin-sort-imports": "^4.2.0",
|
||||||
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/better-sqlite3": "^7.6.5",
|
"@types/better-sqlite3": "^7.6.5",
|
||||||
"@types/cookies": "^0.7.7",
|
"@types/cookies": "^0.7.7",
|
||||||
"@types/dockerode": "^3.3.9",
|
"@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,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDisclosure, useListState } from '@mantine/hooks';
|
import { useDisclosure, useListState } from '@mantine/hooks';
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
IconBox,
|
IconBox,
|
||||||
IconCategory,
|
IconCategory,
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
IconCursorText,
|
IconCursorText,
|
||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconDotsVertical,
|
IconDotsVertical,
|
||||||
|
IconDownload,
|
||||||
IconFolderFilled,
|
IconFolderFilled,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconLockOff,
|
IconLockOff,
|
||||||
@@ -32,6 +34,8 @@ import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
|
|||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
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 { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
|
||||||
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
|
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
|
||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
@@ -42,16 +46,14 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati
|
|||||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||||
import { api } from '~/utils/api';
|
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
|
// Infer return type from the `getServerSideProps` function
|
||||||
export default function BoardsPage({
|
export default function BoardsPage({
|
||||||
boards,
|
boards,
|
||||||
session,
|
session,
|
||||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
}: 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 [renameBoardName, setRenameBoardName] = useState<{ boardName: string }>();
|
||||||
|
|
||||||
const { data, refetch } = api.boards.all.useQuery(undefined, {
|
const { data, refetch } = api.boards.all.useQuery(undefined, {
|
||||||
@@ -79,6 +81,11 @@ export default function BoardsPage({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
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');
|
const { t } = useTranslation('manage/boards');
|
||||||
|
|
||||||
@@ -90,15 +97,29 @@ export default function BoardsPage({
|
|||||||
<title>{metaTitle}</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Modal opened={openedRenameBoardModal} onClose={closeRenameBoardModal}
|
<Modal
|
||||||
title={t('cards.menu.rename.modal.title', { name: renameBoardName?.boardName })}>
|
opened={openedRenameBoardModal}
|
||||||
<RenameBoardModal boardName={renameBoardName?.boardName as string} configNames={data.map(board => board.name)}
|
onClose={closeRenameBoardModal}
|
||||||
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>
|
</Modal>
|
||||||
|
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Title mb="xl">{t('pageTitle')}</Title>
|
<Title mb="xl">{t('pageTitle')}</Title>
|
||||||
{session?.user.isAdmin && (
|
{session?.user.isAdmin && (
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={downloadAllBoards}
|
||||||
|
leftIcon={<IconDownload size="1rem" />}
|
||||||
|
>
|
||||||
|
Download all boards
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={openCreateBoardModal}
|
onClick={openCreateBoardModal}
|
||||||
leftIcon={<IconPlus size="1rem" />}
|
leftIcon={<IconPlus size="1rem" />}
|
||||||
@@ -106,6 +127,7 @@ export default function BoardsPage({
|
|||||||
>
|
>
|
||||||
{t('buttons.create')}
|
{t('buttons.create')}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -200,18 +222,20 @@ export default function BoardsPage({
|
|||||||
boardName: board.name,
|
boardName: board.name,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
icon={<IconCopy size={'1rem'} />}>
|
icon={<IconCopy size={'1rem'} />}
|
||||||
|
>
|
||||||
{t('cards.menu.duplicate')}
|
{t('cards.menu.duplicate')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRenameBoardName({
|
setRenameBoardName({
|
||||||
boardName: board.name as string
|
boardName: board.name as string,
|
||||||
});
|
});
|
||||||
openRenameBoardModal();
|
openRenameBoardModal();
|
||||||
}}
|
}}
|
||||||
icon={<IconCursorText size={'1rem'} />}
|
icon={<IconCursorText size={'1rem'} />}
|
||||||
disabled={board.name === 'default'}>
|
disabled={board.name === 'default'}
|
||||||
|
>
|
||||||
{t('cards.menu.rename.label')}
|
{t('cards.menu.rename.label')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -264,7 +288,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const result = checkForSessionOrAskForLogin(
|
const result = checkForSessionOrAskForLogin(
|
||||||
context,
|
context,
|
||||||
session,
|
session,
|
||||||
() => session?.user.isAdmin == true,
|
() => session?.user.isAdmin == true
|
||||||
);
|
);
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
return result;
|
return result;
|
||||||
@@ -281,7 +305,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
manageNamespaces,
|
manageNamespaces,
|
||||||
context.locale,
|
context.locale,
|
||||||
context.req,
|
context.req,
|
||||||
context.res,
|
context.res
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@@ -3022,6 +3022,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/aria-query@npm:^5.0.1":
|
||||||
version: 5.0.4
|
version: 5.0.4
|
||||||
resolution: "@types/aria-query@npm:5.0.4"
|
resolution: "@types/aria-query@npm:5.0.4"
|
||||||
@@ -4031,6 +4040,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"aes-decrypter@npm:4.0.1, aes-decrypter@npm:^4.0.1":
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
resolution: "aes-decrypter@npm:4.0.1"
|
resolution: "aes-decrypter@npm:4.0.1"
|
||||||
@@ -7429,6 +7445,7 @@ __metadata:
|
|||||||
"@trpc/next": ^10.37.1
|
"@trpc/next": ^10.37.1
|
||||||
"@trpc/react-query": ^10.37.1
|
"@trpc/react-query": ^10.37.1
|
||||||
"@trpc/server": ^10.37.1
|
"@trpc/server": ^10.37.1
|
||||||
|
"@types/adm-zip": ^0.5.5
|
||||||
"@types/bcryptjs": ^2.4.2
|
"@types/bcryptjs": ^2.4.2
|
||||||
"@types/better-sqlite3": ^7.6.5
|
"@types/better-sqlite3": ^7.6.5
|
||||||
"@types/cookies": ^0.7.7
|
"@types/cookies": ^0.7.7
|
||||||
@@ -7447,6 +7464,7 @@ __metadata:
|
|||||||
"@vitest/coverage-c8": ^0.33.0
|
"@vitest/coverage-c8": ^0.33.0
|
||||||
"@vitest/coverage-v8": ^0.34.5
|
"@vitest/coverage-v8": ^0.34.5
|
||||||
"@vitest/ui": ^0.34.4
|
"@vitest/ui": ^0.34.4
|
||||||
|
adm-zip: ^0.5.15
|
||||||
axios: ^1.0.0
|
axios: ^1.0.0
|
||||||
bcryptjs: ^2.4.3
|
bcryptjs: ^2.4.3
|
||||||
better-sqlite3: ^8.6.0
|
better-sqlite3: ^8.6.0
|
||||||
|
|||||||
Reference in New Issue
Block a user