feat: add download functionality for board configuration files (#2111)

This commit is contained in:
Meier Lukas
2024-08-24 14:39:13 +02:00
committed by GitHub
parent cc240f4f87
commit a87935875d
4 changed files with 100 additions and 22 deletions

View File

@@ -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
View 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;

View File

@@ -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 {

View File

@@ -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