mirror of
				https://github.com/ajnart/homarr.git
				synced 2025-10-31 18:46:23 +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