From 669d311b0c7d2098a14b16b34d9a4f7875f7334c Mon Sep 17 00:00:00 2001 From: ajnart Date: Fri, 17 Nov 2023 15:34:14 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=9A=A7=20WIP=20on=20Docker=20import?= =?UTF-8?q?=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../en/layout/element-selector/selector.json | 45 ++--- .../Components/DockerImportModal.tsx | 167 ++++++++++++++++++ .../Overview/AvailableElementsOverview.tsx | 16 +- .../AvailableStaticElementsTab.tsx | 34 ---- .../WidgetsTab/AvailableWidgetsTab.tsx | 6 +- .../SelectElement/SelectElementModal.tsx | 10 +- .../Tools/Docker/ContainerActionBar.tsx | 2 +- .../Manage/Tools/Docker/ContainerTable.tsx | 27 ++- src/pages/onboard.tsx | 14 +- src/server/api/routers/board.ts | 5 + src/server/api/routers/icon.ts | 49 ++--- src/server/api/routers/overseerr.ts | 4 +- 12 files changed, 275 insertions(+), 104 deletions(-) create mode 100644 src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx delete mode 100644 src/components/Dashboard/Modals/SelectElement/Components/StaticElementsTab/AvailableStaticElementsTab.tsx diff --git a/public/locales/en/layout/element-selector/selector.json b/public/locales/en/layout/element-selector/selector.json index 3d2b934b0..491d44ca8 100644 --- a/public/locales/en/layout/element-selector/selector.json +++ b/public/locales/en/layout/element-selector/selector.json @@ -1,25 +1,26 @@ { - "modal": { - "title": "Add a new tile", - "text": "Tiles are the main element of Homarr. They are used to display your apps and other information. You can add as many tiles as you want." - }, - "widgetDescription": "Widgets interact with your apps, to provide you with more control over your applications. They usually require additional configuration before use.", - "goBack": "Go back to the previous step", - "actionIcon": { - "tooltip": "Add a tile" - }, - "apps": "Apps", - "app": { - "defaultName": "Your App" - }, - "widgets": "Widgets", - "categories": "Categories", - "category": { - "newName": "Name of new category", - "defaultName": "New Category", - "created": { - "title": "Category created", - "message": "The category \"{{name}}\" has been created" - } + "modal": { + "title": "Add a new tile", + "text": "Tiles are the main element of Homarr. They are used to display your apps and other information. You can add as many tiles as you want." + }, + "widgetDescription": "Widgets interact with your apps, to provide you with more control over your applications. They usually require additional configuration before use.", + "goBack": "Go back to the previous step", + "actionIcon": { + "tooltip": "Add a tile" + }, + "apps": "Apps", + "app": { + "defaultName": "Your App" + }, + "widgets": "Widgets", + "categories": "Categories", + "category": { + "newName": "Name of new category", + "defaultName": "New Category", + "created": { + "title": "Category created", + "message": "The category \"{{name}}\" has been created" } + }, + "importFromDocker": "Import from docker" } diff --git a/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx b/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx new file mode 100644 index 000000000..81f1a8be6 --- /dev/null +++ b/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx @@ -0,0 +1,167 @@ +import { Button, Card, Center, Checkbox, Grid, Group, Image, Stack, Text } from '@mantine/core'; +import { closeAllModals, closeModal } from '@mantine/modals'; +import { notifications } from '@mantine/notifications'; +import Dockerode from 'dockerode'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import ContainerState from '~/components/Manage/Tools/Docker/ContainerState'; +import { useConfigContext } from '~/config/provider'; +import { NormalizedIconRepositoryResult } from '~/tools/server/images/abstract-icons-repository'; +import { api } from '~/utils/api'; +import { ConditionalWrapper } from '~/utils/security'; +import { WidgetLoading } from '~/widgets/loading'; + +import { SelectorBackArrow } from './Shared/SelectorBackArrow'; + +function DockerDispaly({ + container, + selected, + setSelected, + iconsData, +}: { + container: Dockerode.ContainerInfo; + selected: Dockerode.ContainerInfo[]; + setSelected: (containers: Dockerode.ContainerInfo[]) => void; + iconsData: NormalizedIconRepositoryResult[]; +}) { + const containerName = container.Names[0].replace('/', ''); + const isSelected = selected.includes(container); + // Example image : linuxserver.io/sonarr:latest + // Remove the slashes + const imageParsed = container.Image.split('/'); + // Remove the version + const image = imageParsed[imageParsed.length - 1].split(':')[0]; + const foundIcon = iconsData + .flatMap((repository) => + repository.entries.map((entry) => ({ + ...entry, + repository: repository.name, + })) + ) + .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); + return ( + + setSelected(isSelected ? selected.filter((c) => c !== container) : [...selected, container]) + } + > + + + + + + + +
+ +
+ + + {containerName} + + {container.Image && ( + + {container.Image} + + )} + +
+
+
+ ); +} + +function findIconForContainer( + container: Dockerode.ContainerInfo, + iconsData: NormalizedIconRepositoryResult[] +) { + const imageParsed = container.Image.split('/'); + // Remove the version + const image = imageParsed[imageParsed.length - 1].split(':')[0]; + const foundIcon = iconsData + .flatMap((repository) => + repository.entries.map((entry) => ({ + ...entry, + repository: repository.name, + })) + ) + .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); + return foundIcon; +} + +export default function ImportFromDockerModal({ onClickBack }: { onClickBack: () => void }) { + const { data, isLoading } = api.docker.containers.useQuery(undefined, {}); + const { data: iconsData } = api.icon.all.useQuery(); + const { config } = useConfigContext(); + const { mutateAsync, isLoading: mutationIsLoading } = + api.boards.addAppsForContainers.useMutation(); + + const [selected, setSelected] = useState([]); + + if (isLoading || !data || !iconsData) return ; + return ( + + + + {data?.map((container) => ( + + + + ))} + + + + ); +} diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index c1b2045db..72ec3cce8 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -1,8 +1,9 @@ import { Group, Space, Stack, Text, UnstyledButton } from '@mantine/core'; import { closeModal } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; -import { IconBox, IconBoxAlignTop, IconStack } from '@tabler/icons-react'; +import { IconBox, IconBoxAlignTop, IconBrandDocker, IconStack } from '@tabler/icons-react'; import { motion } from 'framer-motion'; +import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import { ReactNode } from 'react'; import { v4 as uuidv4 } from 'uuid'; @@ -18,17 +19,19 @@ import { useStyles } from '../Shared/styles'; interface AvailableElementTypesProps { modalId: string; onOpenIntegrations: () => void; - onOpenStaticElements: () => void; + onOpenDocker: () => void; } export const AvailableElementTypes = ({ modalId, onOpenIntegrations: onOpenWidgets, - onOpenStaticElements, + onOpenDocker, }: AvailableElementTypesProps) => { const { t } = useTranslation('layout/element-selector/selector'); const { config, name: configName } = useConfigContext(); const { updateConfig } = useConfigStore(); + const { data } = useSession(); + const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0]; const onClickCreateCategory = async () => { @@ -96,6 +99,13 @@ export const AvailableElementTypes = ({ }); }} /> + {data && data.user.isAdmin && ( + } + onClick={onOpenDocker} + /> + )} } diff --git a/src/components/Dashboard/Modals/SelectElement/Components/StaticElementsTab/AvailableStaticElementsTab.tsx b/src/components/Dashboard/Modals/SelectElement/Components/StaticElementsTab/AvailableStaticElementsTab.tsx deleted file mode 100644 index 3c4a66821..000000000 --- a/src/components/Dashboard/Modals/SelectElement/Components/StaticElementsTab/AvailableStaticElementsTab.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Container, Grid, Text } from '@mantine/core'; -import { IconCursorText } from '@tabler/icons-react'; -import { useTranslation } from 'next-i18next'; - -import { GenericAvailableElementType } from '../Shared/GenericElementType'; -import { SelectorBackArrow } from '../Shared/SelectorBackArrow'; - -interface AvailableStaticTypesProps { - onClickBack: () => void; -} - -export const AvailableStaticTypes = ({ onClickBack }: AvailableStaticTypesProps) => { - const { t } = useTranslation('layout/element-selector/selector'); - return ( - <> - - - - Static elements provide you additional control over your dashboard. They are static, because - they don't integrate with any apps and their content never changes. - - - - {/* - {}} - /> */} - - - ); -}; diff --git a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/AvailableWidgetsTab.tsx b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/AvailableWidgetsTab.tsx index d013567a3..83831ac5f 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/AvailableWidgetsTab.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/WidgetsTab/AvailableWidgetsTab.tsx @@ -1,4 +1,4 @@ -import { Grid, Text } from '@mantine/core'; +import { Grid, Stack, Text } from '@mantine/core'; import { useTranslation } from 'next-i18next'; import widgets from '../../../../../../widgets'; @@ -14,7 +14,7 @@ export const AvailableIntegrationElements = ({ }: AvailableIntegrationElementsProps) => { const { t } = useTranslation('layout/element-selector/selector'); return ( - <> + @@ -26,6 +26,6 @@ export const AvailableIntegrationElements = ({ ))} - + ); }; diff --git a/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx b/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx index fca0a4eed..3727b00f8 100644 --- a/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx +++ b/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx @@ -2,11 +2,11 @@ import { ContextModalProps } from '@mantine/modals'; import { useState } from 'react'; import { AvailableElementTypes } from './Components/Overview/AvailableElementsOverview'; -import { AvailableStaticTypes } from './Components/StaticElementsTab/AvailableStaticElementsTab'; import { AvailableIntegrationElements } from './Components/WidgetsTab/AvailableWidgetsTab'; +import ImportFromDockerModal from './Components/DockerImportModal'; export const SelectElementModal = ({ context, id }: ContextModalProps) => { - const [activeTab, setActiveTab] = useState(); + const [activeTab, setActiveTab] = useState(); switch (activeTab) { case undefined: @@ -14,13 +14,13 @@ export const SelectElementModal = ({ context, id }: ContextModalProps) => { setActiveTab('integrations')} - onOpenStaticElements={() => setActiveTab('static_elements')} + onOpenDocker={() => setActiveTab('dockerImport')} /> ); case 'integrations': return setActiveTab(undefined)} />; - case 'static_elements': - return setActiveTab(undefined)} />; + case 'dockerImport': + return setActiveTab(undefined)} />; default: /* default to the main selection tab */ setActiveTab(undefined); diff --git a/src/components/Manage/Tools/Docker/ContainerActionBar.tsx b/src/components/Manage/Tools/Docker/ContainerActionBar.tsx index cf18de50c..2fa0f591b 100644 --- a/src/components/Manage/Tools/Docker/ContainerActionBar.tsx +++ b/src/components/Manage/Tools/Docker/ContainerActionBar.tsx @@ -94,7 +94,7 @@ export default function ContainerActionBar({ color="indigo" variant="light" radius="md" - disabled={selected.length !== 1} + disabled={selected.length < 1} onClick={() => openDockerSelectBoardModal({ containers: selected })} > {t('actionBar.addToHomarr.title')} diff --git a/src/components/Manage/Tools/Docker/ContainerTable.tsx b/src/components/Manage/Tools/Docker/ContainerTable.tsx index bd0f76c84..23ecc711a 100644 --- a/src/components/Manage/Tools/Docker/ContainerTable.tsx +++ b/src/components/Manage/Tools/Docker/ContainerTable.tsx @@ -2,6 +2,7 @@ import { Badge, Checkbox, Group, + Image, ScrollArea, Table, Text, @@ -13,6 +14,7 @@ import { IconSearch } from '@tabler/icons-react'; import Dockerode, { ContainerInfo } from 'dockerode'; import { useTranslation } from 'next-i18next'; import { Dispatch, SetStateAction, useMemo, useState } from 'react'; +import { api } from '~/utils/api'; import { MIN_WIDTH_MOBILE } from '../../../../constants/constants'; import ContainerState from './ContainerState'; @@ -117,6 +119,22 @@ type RowProps = { const Row = ({ container, selected, toggleRow, width }: RowProps) => { const { t } = useTranslation('modules/docker'); const { classes, cx } = useStyles(); + const { data: iconsData } = api.icon.all.useQuery(); + if (!iconsData) return null; + const containerName = container.Names[0].replace('/', ''); + // Example image : linuxserver.io/sonarr:latest + // Remove the slashes + const imageParsed = container.Image.split('/'); + // Remove the version + const image = imageParsed[imageParsed.length - 1].split(':')[0]; + const foundIcon = iconsData + .flatMap((repository) => + repository.entries.map((entry) => ({ + ...entry, + repository: repository.name, + })) + ) + .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); return ( @@ -124,9 +142,12 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => { toggleRow(container)} transitionDuration={0} /> - - {container.Names[0].replace('/', '')} - + + {image} + + {containerName} + + {width > MIN_WIDTH_MOBILE && ( diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx index de85c4eff..7b98b3da1 100644 --- a/src/pages/onboard.tsx +++ b/src/pages/onboard.tsx @@ -26,7 +26,7 @@ export default function OnboardPage({ const [onboardingSteps, { open: showOnboardingSteps }] = useDisclosure(false); - const isUpgradeFromSchemaOne = configSchemaVersions.includes(1); + const isUpgradeFromSchemaOne = false; return ( <> @@ -81,12 +81,12 @@ export default function OnboardPage({ } export const getServerSideProps: GetServerSideProps = async (ctx) => { - const userCount = await getTotalUserCountAsync(); - if (userCount >= 1) { - return { - notFound: true, - }; - } + // const userCount = await getTotalUserCountAsync(); + // if (userCount >= 1) { + // return { + // notFound: true, + // }; + // } const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); const configs = files.map((file) => getConfig(file)); diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 05c74c2e4..5c03d0dcc 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -41,6 +41,7 @@ export const boardRouter = createTRPCRouter({ apps: z.array( z.object({ name: z.string(), + icon: z.string().optional(), port: z.number().optional(), }) ), @@ -71,6 +72,10 @@ export const boardRouter = createTRPCRouter({ ...defaultApp, name: container.name, url: address, + appearance: { + ...defaultApp.appearance, + icon: container.icon, + }, behaviour: { ...defaultApp.behaviour, externalUrl: address, diff --git a/src/server/api/routers/icon.ts b/src/server/api/routers/icon.ts index a5b6c2493..2ad4d2ad7 100644 --- a/src/server/api/routers/icon.ts +++ b/src/server/api/routers/icon.ts @@ -1,35 +1,36 @@ +import { GitHubIconsRepository } from '~/tools/server/images/github-icons-repository'; import { JsdelivrIconsRepository } from '~/tools/server/images/jsdelivr-icons-repository'; import { LocalIconsRepository } from '~/tools/server/images/local-icons-repository'; import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-repository'; -import { GitHubIconsRepository } from '~/tools/server/images/github-icons-repository'; import { createTRPCRouter, publicProcedure } from '../trpc'; +const respositories = [ + new LocalIconsRepository(), + new GitHubIconsRepository( + GitHubIconsRepository.walkxcode, + 'Walkxcode Dashboard Icons', + 'Walkxcode on Github' + ), + new UnpkgIconsRepository( + UnpkgIconsRepository.tablerRepository, + 'Tabler Icons', + 'Tabler Icons - GitHub (MIT)' + ), + new JsdelivrIconsRepository( + JsdelivrIconsRepository.papirusRepository, + 'Papirus Icons', + 'Papirus Development Team on GitHub (Apache 2.0)' + ), + new JsdelivrIconsRepository( + JsdelivrIconsRepository.homelabSvgAssetsRepository, + 'Homelab Svg Assets', + 'loganmarchione on GitHub (MIT)' + ), +]; + export const iconRouter = createTRPCRouter({ all: publicProcedure.query(async () => { - const respositories = [ - new LocalIconsRepository(), - new GitHubIconsRepository( - GitHubIconsRepository.walkxcode, - 'Walkxcode Dashboard Icons', - 'Walkxcode on Github' - ), - new UnpkgIconsRepository( - UnpkgIconsRepository.tablerRepository, - 'Tabler Icons', - 'Tabler Icons - GitHub (MIT)' - ), - new JsdelivrIconsRepository( - JsdelivrIconsRepository.papirusRepository, - 'Papirus Icons', - 'Papirus Development Team on GitHub (Apache 2.0)' - ), - new JsdelivrIconsRepository( - JsdelivrIconsRepository.homelabSvgAssetsRepository, - 'Homelab Svg Assets', - 'loganmarchione on GitHub (MIT)' - ), - ]; const fetches = respositories.map((rep) => rep.fetch()); const data = await Promise.all(fetches); return data; diff --git a/src/server/api/routers/overseerr.ts b/src/server/api/routers/overseerr.ts index 222082603..ff29e5de2 100644 --- a/src/server/api/routers/overseerr.ts +++ b/src/server/api/routers/overseerr.ts @@ -7,10 +7,10 @@ import { OriginalLanguage, Result } from '~/modules/overseerr/SearchResult'; import { TvShowResult } from '~/modules/overseerr/TvShow'; import { getConfig } from '~/tools/config/getConfig'; -import { createTRPCRouter, publicProcedure } from '../trpc'; +import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc'; export const overseerrRouter = createTRPCRouter({ - search: publicProcedure + search: adminProcedure .input( z.object({ configName: z.string(), From c88cd3c05ee455a699bedb96c4926da4b639a818 Mon Sep 17 00:00:00 2001 From: ajnart Date: Mon, 20 Nov 2023 13:37:58 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=9A=A7=20WIP=20on=20docker=20import?= =?UTF-8?q?=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- .../Components/DockerImportModal.tsx | 167 ------------------ .../Overview/AvailableElementsOverview.tsx | 11 +- .../SelectElement/SelectElementModal.tsx | 4 - .../Tools/Docker/ContainerActionBar.tsx | 10 +- .../Manage/Tools/Docker/ContainerTable.tsx | 24 +-- .../Docker/docker-select-board.modal.tsx | 15 +- .../layout/Templates/BoardLayout.tsx | 60 ++++++- src/pages/board/index.tsx | 39 +++- src/server/api/routers/app.ts | 22 ++- src/server/api/routers/board.ts | 4 +- src/server/api/routers/docker/router.ts | 23 ++- src/server/api/routers/icon.ts | 4 +- src/tools/server/translation-namespaces.ts | 1 + 14 files changed, 151 insertions(+), 235 deletions(-) delete mode 100644 src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx diff --git a/package.json b/package.json index 3724f70c2..7fbaf9707 100644 --- a/package.json +++ b/package.json @@ -233,4 +233,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx b/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx deleted file mode 100644 index 81f1a8be6..000000000 --- a/src/components/Dashboard/Modals/SelectElement/Components/DockerImportModal.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { Button, Card, Center, Checkbox, Grid, Group, Image, Stack, Text } from '@mantine/core'; -import { closeAllModals, closeModal } from '@mantine/modals'; -import { notifications } from '@mantine/notifications'; -import Dockerode from 'dockerode'; -import { motion } from 'framer-motion'; -import { useState } from 'react'; -import ContainerState from '~/components/Manage/Tools/Docker/ContainerState'; -import { useConfigContext } from '~/config/provider'; -import { NormalizedIconRepositoryResult } from '~/tools/server/images/abstract-icons-repository'; -import { api } from '~/utils/api'; -import { ConditionalWrapper } from '~/utils/security'; -import { WidgetLoading } from '~/widgets/loading'; - -import { SelectorBackArrow } from './Shared/SelectorBackArrow'; - -function DockerDispaly({ - container, - selected, - setSelected, - iconsData, -}: { - container: Dockerode.ContainerInfo; - selected: Dockerode.ContainerInfo[]; - setSelected: (containers: Dockerode.ContainerInfo[]) => void; - iconsData: NormalizedIconRepositoryResult[]; -}) { - const containerName = container.Names[0].replace('/', ''); - const isSelected = selected.includes(container); - // Example image : linuxserver.io/sonarr:latest - // Remove the slashes - const imageParsed = container.Image.split('/'); - // Remove the version - const image = imageParsed[imageParsed.length - 1].split(':')[0]; - const foundIcon = iconsData - .flatMap((repository) => - repository.entries.map((entry) => ({ - ...entry, - repository: repository.name, - })) - ) - .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); - return ( - - setSelected(isSelected ? selected.filter((c) => c !== container) : [...selected, container]) - } - > - - - - - - - -
- -
- - - {containerName} - - {container.Image && ( - - {container.Image} - - )} - -
-
-
- ); -} - -function findIconForContainer( - container: Dockerode.ContainerInfo, - iconsData: NormalizedIconRepositoryResult[] -) { - const imageParsed = container.Image.split('/'); - // Remove the version - const image = imageParsed[imageParsed.length - 1].split(':')[0]; - const foundIcon = iconsData - .flatMap((repository) => - repository.entries.map((entry) => ({ - ...entry, - repository: repository.name, - })) - ) - .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); - return foundIcon; -} - -export default function ImportFromDockerModal({ onClickBack }: { onClickBack: () => void }) { - const { data, isLoading } = api.docker.containers.useQuery(undefined, {}); - const { data: iconsData } = api.icon.all.useQuery(); - const { config } = useConfigContext(); - const { mutateAsync, isLoading: mutationIsLoading } = - api.boards.addAppsForContainers.useMutation(); - - const [selected, setSelected] = useState([]); - - if (isLoading || !data || !iconsData) return ; - return ( - - - - {data?.map((container) => ( - - - - ))} - - - - ); -} diff --git a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx index 72ec3cce8..3c7ce5262 100644 --- a/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx +++ b/src/components/Dashboard/Modals/SelectElement/Components/Overview/AvailableElementsOverview.tsx @@ -1,7 +1,7 @@ import { Group, Space, Stack, Text, UnstyledButton } from '@mantine/core'; import { closeModal } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; -import { IconBox, IconBoxAlignTop, IconBrandDocker, IconStack } from '@tabler/icons-react'; +import { IconBox, IconBoxAlignTop, IconStack } from '@tabler/icons-react'; import { motion } from 'framer-motion'; import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; @@ -19,13 +19,11 @@ import { useStyles } from '../Shared/styles'; interface AvailableElementTypesProps { modalId: string; onOpenIntegrations: () => void; - onOpenDocker: () => void; } export const AvailableElementTypes = ({ modalId, onOpenIntegrations: onOpenWidgets, - onOpenDocker, }: AvailableElementTypesProps) => { const { t } = useTranslation('layout/element-selector/selector'); const { config, name: configName } = useConfigContext(); @@ -99,13 +97,6 @@ export const AvailableElementTypes = ({ }); }} /> - {data && data.user.isAdmin && ( - } - onClick={onOpenDocker} - /> - )} } diff --git a/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx b/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx index 3727b00f8..7a1cd5648 100644 --- a/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx +++ b/src/components/Dashboard/Modals/SelectElement/SelectElementModal.tsx @@ -3,7 +3,6 @@ import { useState } from 'react'; import { AvailableElementTypes } from './Components/Overview/AvailableElementsOverview'; import { AvailableIntegrationElements } from './Components/WidgetsTab/AvailableWidgetsTab'; -import ImportFromDockerModal from './Components/DockerImportModal'; export const SelectElementModal = ({ context, id }: ContextModalProps) => { const [activeTab, setActiveTab] = useState(); @@ -14,13 +13,10 @@ export const SelectElementModal = ({ context, id }: ContextModalProps) => { setActiveTab('integrations')} - onOpenDocker={() => setActiveTab('dockerImport')} /> ); case 'integrations': return setActiveTab(undefined)} />; - case 'dockerImport': - return setActiveTab(undefined)} />; default: /* default to the main selection tab */ setActiveTab(undefined); diff --git a/src/components/Manage/Tools/Docker/ContainerActionBar.tsx b/src/components/Manage/Tools/Docker/ContainerActionBar.tsx index 2fa0f591b..cd35d9681 100644 --- a/src/components/Manage/Tools/Docker/ContainerActionBar.tsx +++ b/src/components/Manage/Tools/Docker/ContainerActionBar.tsx @@ -16,7 +16,7 @@ import { RouterInputs, api } from '~/utils/api'; import { openDockerSelectBoardModal } from './docker-select-board.modal'; export interface ContainerActionBarProps { - selected: Dockerode.ContainerInfo[]; + selected: (Dockerode.ContainerInfo & { icon?: string })[]; reload: () => void; isLoading: boolean; } @@ -127,13 +127,7 @@ const useDockerActionMutation = () => { { action, id: container.Id }, { onSuccess: () => { - notifications.update({ - id: container.Id, - title: containerName, - message: `${t(`actions.${action}.end`)} ${containerName}`, - icon: , - autoClose: 2000, - }); + notifications.cleanQueue(); }, onError: (err) => { notifications.update({ diff --git a/src/components/Manage/Tools/Docker/ContainerTable.tsx b/src/components/Manage/Tools/Docker/ContainerTable.tsx index 23ecc711a..aec474ea8 100644 --- a/src/components/Manage/Tools/Docker/ContainerTable.tsx +++ b/src/components/Manage/Tools/Docker/ContainerTable.tsx @@ -14,7 +14,6 @@ import { IconSearch } from '@tabler/icons-react'; import Dockerode, { ContainerInfo } from 'dockerode'; import { useTranslation } from 'next-i18next'; import { Dispatch, SetStateAction, useMemo, useState } from 'react'; -import { api } from '~/utils/api'; import { MIN_WIDTH_MOBILE } from '../../../../constants/constants'; import ContainerState from './ContainerState'; @@ -98,6 +97,7 @@ export default function ContainerTable({ void; width: number; + icon?: string; }; -const Row = ({ container, selected, toggleRow, width }: RowProps) => { +const Row = ({ icon, container, selected, toggleRow, width }: RowProps) => { const { t } = useTranslation('modules/docker'); const { classes, cx } = useStyles(); - const { data: iconsData } = api.icon.all.useQuery(); - if (!iconsData) return null; const containerName = container.Names[0].replace('/', ''); - // Example image : linuxserver.io/sonarr:latest - // Remove the slashes - const imageParsed = container.Image.split('/'); - // Remove the version - const image = imageParsed[imageParsed.length - 1].split(':')[0]; - const foundIcon = iconsData - .flatMap((repository) => - repository.entries.map((entry) => ({ - ...entry, - repository: repository.name, - })) - ) - .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); return ( @@ -143,7 +129,7 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => { - {image} + {containerName} @@ -151,7 +137,7 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => { {width > MIN_WIDTH_MOBILE && ( - {container.Image} + {container.Image.slice(0, 25)} )} {width > MIN_WIDTH_MOBILE && ( diff --git a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx index 8eabedb7d..63b4da364 100644 --- a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx +++ b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx @@ -1,4 +1,4 @@ -import { Button, Group, Select, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Button, Group, Select, Stack, Text, Title } from '@mantine/core'; import { useForm } from '@mantine/form'; import { ContextModalProps, modals } from '@mantine/modals'; import { showNotification } from '@mantine/notifications'; @@ -6,6 +6,7 @@ import { IconCheck, IconX } from '@tabler/icons-react'; import { ContainerInfo } from 'dockerode'; import { Trans, useTranslation } from 'next-i18next'; import { z } from 'zod'; +import { useConfigStore } from '~/config/store'; import { api } from '~/utils/api'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; @@ -14,7 +15,7 @@ const dockerSelectBoardSchema = z.object({ }); type InnerProps = { - containers: ContainerInfo[]; + containers: (ContainerInfo & { icon?: string })[]; }; type FormType = z.infer; @@ -22,12 +23,14 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps { await mutateAsync( { apps: innerProps.containers.map((container) => ({ name: (container.Names.at(0) ?? 'App').replace('/', ''), port: container.Ports.at(0)?.PublicPort, + icon: container.icon, })), boardName: values.board, }, @@ -39,7 +42,7 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps, color: 'green', }); - + //TODO: Update config or reload it from server modals.close(id); }, onError: () => { @@ -117,5 +120,9 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => { ), innerProps, }); - umami.track('Add to homarr modal') + umami.track('Add to homarr modal'); }; +function uuidv4(): any { + throw new Error('Function not implemented.'); +} + diff --git a/src/components/layout/Templates/BoardLayout.tsx b/src/components/layout/Templates/BoardLayout.tsx index 3e82ea775..4f165b12c 100644 --- a/src/components/layout/Templates/BoardLayout.tsx +++ b/src/components/layout/Templates/BoardLayout.tsx @@ -1,16 +1,26 @@ -import { Button, Global, Text, Title, Tooltip, clsx } from '@mantine/core'; -import { useHotkeys, useWindowEvent } from '@mantine/hooks'; +import { Button, Global, Modal, Stack, Text, Title, Tooltip, clsx } from '@mantine/core'; +import { useDisclosure, useHotkeys, useWindowEvent } from '@mantine/hooks'; import { openContextModal } from '@mantine/modals'; import { hideNotification, showNotification } from '@mantine/notifications'; -import { IconApps, IconEditCircle, IconEditCircleOff, IconSettings } from '@tabler/icons-react'; +import { + IconApps, + IconBrandDocker, + IconEditCircle, + IconEditCircleOff, + IconSettings, +} from '@tabler/icons-react'; import Consola from 'consola'; +import { ContainerInfo } from 'dockerode'; import { useSession } from 'next-auth/react'; import { Trans, useTranslation } from 'next-i18next'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { env } from 'process'; +import { useEffect, useState } from 'react'; import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store'; +import ContainerActionBar from '~/components/Manage/Tools/Docker/ContainerActionBar'; +import ContainerTable from '~/components/Manage/Tools/Docker/ContainerTable'; import { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride'; import { HeaderActionButton } from '~/components/layout/header/ActionButton'; import { useConfigContext } from '~/config/provider'; @@ -44,11 +54,55 @@ export const HeaderActions = () => { return ( <> + ); }; +const DockerButton = () => { + const [selection, setSelection] = useState<(ContainerInfo & { icon?: string })[]>([]); + const [opened, { open, close, toggle }] = useDisclosure(false); + useHotkeys([['mod+B', toggle]]); + + const { data, refetch, isRefetching } = api.docker.containers.useQuery(undefined, { + cacheTime: 60 * 1000 * 5, + staleTime: 60 * 1000 * 1, + }); + const { t } = useTranslation('tools/docker'); + const reload = () => { + refetch(); + setSelection([]); + }; + + return ( + <> + + + + + + + + + + + + + ); +}; + const CustomizeBoardButton = () => { const { name } = useConfigContext(); const { t } = useTranslation('boards/common'); diff --git a/src/pages/board/index.tsx b/src/pages/board/index.tsx index 9822d125b..627baf176 100644 --- a/src/pages/board/index.tsx +++ b/src/pages/board/index.tsx @@ -1,8 +1,10 @@ -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; +import Consola from 'consola'; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { SSRConfig } from 'next-i18next'; import { Dashboard } from '~/components/Dashboard/Dashboard'; import { BoardLayout } from '~/components/layout/Templates/BoardLayout'; import { useInitConfig } from '~/config/init'; +import { dockerRouter } from '~/server/api/routers/docker/router'; import { getServerAuthSession } from '~/server/auth'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; @@ -10,11 +12,20 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { boardNamespaces } from '~/tools/server/translation-namespaces'; import { ConfigType } from '~/types/config'; +import { api } from '~/utils/api'; export default function BoardPage({ config: initialConfig, + dockerIsConfigured, + initialContainers, }: InferGetServerSidePropsType) { useInitConfig(initialConfig); + const { data } = api.docker.containers.useQuery(undefined, { + initialData: initialContainers, + enabled: dockerIsConfigured, + cacheTime: 60 * 1000 * 5, + staleTime: 60 * 1000 * 1, + }); return ( @@ -28,33 +39,45 @@ type BoardGetServerSideProps = { _nextI18Next?: SSRConfig['_nextI18Next']; }; -export const getServerSideProps: GetServerSideProps = async (ctx) => { - const session = await getServerAuthSession(ctx); +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const session = await getServerAuthSession(context); const boardName = await getDefaultBoardAsync(session?.user?.id, 'default'); const translations = await getServerSideTranslations( boardNamespaces, - ctx.locale, - ctx.req, - ctx.res + context.locale, + context.req, + context.res ); const config = await getFrontendConfig(boardName); const result = checkForSessionOrAskForLogin( - ctx, + context, session, () => config.settings.access.allowGuests || session?.user != undefined ); if (result) { return result; } - + const caller = dockerRouter.createCaller({ + session: session, + cookies: context.req.cookies, + }); + let containers = undefined; + // Fetch containers if user is admin, otherwise we don't need them + try { + if (session?.user.isAdmin == true) containers = await caller.containers(); + } catch (error) { + Consola.error(`The docker integration failed with the following error: ${error}`); + } return { props: { config, primaryColor: config.settings.customization.colors.primary, secondaryColor: config.settings.customization.colors.secondary, primaryShade: config.settings.customization.colors.shade, + dockerIsConfigured: containers != undefined, + initialContainers: containers, ...translations, }, }; diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index e7c3f5e23..ddfd49a8b 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -1,7 +1,6 @@ import { TRPCError } from '@trpc/server'; -import axios, { AxiosError } from 'axios'; +import { AxiosError } from 'axios'; import Consola from 'consola'; -import https from 'https'; import { z } from 'zod'; import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing'; import { getConfig } from '~/tools/config/getConfig'; @@ -9,6 +8,12 @@ import { AppType } from '~/types/app'; import { createTRPCRouter, publicProcedure } from '../trpc'; +// const agent = new https.Agent({ rejectUnauthorized: false }); +// const getCacheResponse = unstable_cache( +// async (app) => await axios.get(app.url, { httpsAgent: agent, timeout: 10000 }), +// ['app-id'] +// ); + export const appRouter = createTRPCRouter({ ping: publicProcedure .input( @@ -18,7 +23,6 @@ export const appRouter = createTRPCRouter({ }) ) .query(async ({ input }) => { - const agent = new https.Agent({ rejectUnauthorized: false }); const config = getConfig(input.configName); const app = config.apps.find((app) => app.id === input.id); @@ -30,8 +34,16 @@ export const appRouter = createTRPCRouter({ message: `App ${input.id} was not found`, }); } - const res = await axios - .get(app.url, { httpsAgent: agent, timeout: 10000 }) + + const res = await fetch(app.url, { + method: 'GET', + cache: 'force-cache', + headers: { + // Cache for 5 minutes + 'Cache-Control': 'max-age=300', + 'Content-Type': 'application/json', + }, + }) .then((response) => ({ status: response.status, statusText: response.statusText, diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index 5c03d0dcc..72bab7eaf 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -55,7 +55,6 @@ export const boardRouter = createTRPCRouter({ }); } const config = await getConfig(input.boardName); - const lowestWrapper = config?.wrappers.sort((a, b) => a.position - b.position)[0]; const newConfig = { @@ -67,14 +66,13 @@ export const boardRouter = createTRPCRouter({ const address = container.port ? `http://localhost:${container.port}` : 'http://localhost'; - return { ...defaultApp, name: container.name, url: address, appearance: { ...defaultApp.appearance, - icon: container.icon, + iconUrl: container.icon, }, behaviour: { ...defaultApp.behaviour, diff --git a/src/server/api/routers/docker/router.ts b/src/server/api/routers/docker/router.ts index 44b2a35da..cccd7362b 100644 --- a/src/server/api/routers/docker/router.ts +++ b/src/server/api/routers/docker/router.ts @@ -3,6 +3,7 @@ import Dockerode from 'dockerode'; import { z } from 'zod'; import { adminProcedure, createTRPCRouter } from '../../trpc'; +import { IconRespositories } from '../icon'; import DockerSingleton from './DockerSingleton'; const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']); @@ -12,7 +13,27 @@ export const dockerRouter = createTRPCRouter({ try { const docker = new Dockerode({}); const containers = await docker.listContainers({ all: true }); - return containers; + const fetches = IconRespositories.map((rep) => rep.fetch()); + const data = await Promise.all(fetches); + const returnedData = containers.map((container) => { + const imageParsed = container.Image.split('/'); + // Remove the version + const image = imageParsed[imageParsed.length - 1].split(':')[0]; + const foundIcon = data + .flatMap((repository) => + repository.entries.map((entry) => ({ + ...entry, + repository: repository.name, + })) + ) + .find((entry) => entry.name.toLowerCase().includes(image.toLowerCase())); + + return { + ...container, + icon: foundIcon?.url ?? '/public/imgs/logo/logo.svg' + }; + }); + return returnedData; } catch (err) { throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', diff --git a/src/server/api/routers/icon.ts b/src/server/api/routers/icon.ts index 2ad4d2ad7..02e8630b6 100644 --- a/src/server/api/routers/icon.ts +++ b/src/server/api/routers/icon.ts @@ -5,7 +5,7 @@ import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-reposito import { createTRPCRouter, publicProcedure } from '../trpc'; -const respositories = [ +export const IconRespositories = [ new LocalIconsRepository(), new GitHubIconsRepository( GitHubIconsRepository.walkxcode, @@ -31,7 +31,7 @@ const respositories = [ export const iconRouter = createTRPCRouter({ all: publicProcedure.query(async () => { - const fetches = respositories.map((rep) => rep.fetch()); + const fetches = IconRespositories.map((rep) => rep.fetch()); const data = await Promise.all(fetches); return data; }), diff --git a/src/tools/server/translation-namespaces.ts b/src/tools/server/translation-namespaces.ts index ff186c616..2141b82ae 100644 --- a/src/tools/server/translation-namespaces.ts +++ b/src/tools/server/translation-namespaces.ts @@ -1,4 +1,5 @@ export const boardNamespaces = [ + 'tools/docker', 'layout/element-selector/selector', 'layout/modals/add-app', 'layout/modals/change-position', From e2da8338e0e70593950e668627c3babdfb3783e5 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sun, 26 Nov 2023 23:24:03 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=9A=A7=20=20WIP=20on=20adding=20docke?= =?UTF-8?q?r=20containers=20to=20boards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/layout/Templates/BoardLayout.tsx | 11 ++++++----- src/pages/board/index.tsx | 15 +++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/layout/Templates/BoardLayout.tsx b/src/components/layout/Templates/BoardLayout.tsx index 4f165b12c..81f4ed712 100644 --- a/src/components/layout/Templates/BoardLayout.tsx +++ b/src/components/layout/Templates/BoardLayout.tsx @@ -16,7 +16,7 @@ import { Trans, useTranslation } from 'next-i18next'; import Link from 'next/link'; import { useRouter } from 'next/router'; import { env } from 'process'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store'; import ContainerActionBar from '~/components/Manage/Tools/Docker/ContainerActionBar'; @@ -30,14 +30,15 @@ import { MainLayout } from './MainLayout'; type BoardLayoutProps = { children: React.ReactNode; + isDockerEnabled?: boolean; }; -export const BoardLayout = ({ children }: BoardLayoutProps) => { +export const BoardLayout = ({ children, isDockerEnabled = false }: BoardLayoutProps) => { const { config } = useConfigContext(); const { data: session } = useSession(); return ( - }> + }> {children} @@ -46,7 +47,7 @@ export const BoardLayout = ({ children }: BoardLayoutProps) => { ); }; -export const HeaderActions = () => { +export const HeaderActions = ({isDockerEnabled = false} : { isDockerEnabled: boolean}) => { const { data: sessionData } = useSession(); if (!sessionData?.user?.isAdmin) return null; @@ -54,7 +55,7 @@ export const HeaderActions = () => { return ( <> - + {isDockerEnabled && } ); diff --git a/src/pages/board/index.tsx b/src/pages/board/index.tsx index 627baf176..661db269c 100644 --- a/src/pages/board/index.tsx +++ b/src/pages/board/index.tsx @@ -1,4 +1,3 @@ -import Consola from 'consola'; import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { SSRConfig } from 'next-i18next'; import { Dashboard } from '~/components/Dashboard/Dashboard'; @@ -16,19 +15,19 @@ import { api } from '~/utils/api'; export default function BoardPage({ config: initialConfig, - dockerIsConfigured, + isDockerEnabled, initialContainers, }: InferGetServerSidePropsType) { useInitConfig(initialConfig); const { data } = api.docker.containers.useQuery(undefined, { - initialData: initialContainers, - enabled: dockerIsConfigured, + initialData: initialContainers ?? undefined, + enabled: isDockerEnabled, cacheTime: 60 * 1000 * 5, staleTime: 60 * 1000 * 1, }); return ( - + ); @@ -68,7 +67,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => try { if (session?.user.isAdmin == true) containers = await caller.containers(); } catch (error) { - Consola.error(`The docker integration failed with the following error: ${error}`); + } return { props: { @@ -76,8 +75,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => primaryColor: config.settings.customization.colors.primary, secondaryColor: config.settings.customization.colors.secondary, primaryShade: config.settings.customization.colors.shade, - dockerIsConfigured: containers != undefined, - initialContainers: containers, + isDockerEnabled: containers != undefined, + initialContainers: containers ?? null, ...translations, }, }; From 48ca5352c94e1863050ea1e3003fe873fc050aaf Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 27 Nov 2023 00:14:41 +0100 Subject: [PATCH 4/8] Add client side reload of the config with the newly added apps --- .../Docker/docker-select-board.modal.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx index 63b4da364..4248f4b0e 100644 --- a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx +++ b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx @@ -6,7 +6,9 @@ import { IconCheck, IconX } from '@tabler/icons-react'; import { ContainerInfo } from 'dockerode'; import { Trans, useTranslation } from 'next-i18next'; import { z } from 'zod'; +import { useConfigContext } from '~/config/provider'; import { useConfigStore } from '~/config/store'; +import { generateDefaultApp } from '~/tools/shared/app'; import { api } from '~/utils/api'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; @@ -23,15 +25,18 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps store.updateConfig); const handleSubmit = async (values: FormType) => { + const newApps = innerProps.containers.map((container) => ({ + name: (container.Names.at(0) ?? 'App').replace('/', ''), + port: container.Ports.at(0)?.PublicPort, + icon: container.icon, + })); await mutateAsync( { - apps: innerProps.containers.map((container) => ({ - name: (container.Names.at(0) ?? 'App').replace('/', ''), - port: container.Ports.at(0)?.PublicPort, - icon: container.icon, - })), + apps: newApps, boardName: values.board, }, { @@ -42,7 +47,21 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps, color: 'green', }); - //TODO: Update config or reload it from server + updateConfig(configName!, (config) => { + const lowestWrapper = config?.wrappers.sort((a, b) => a.position - b.position)[0]; + const defaultApp = generateDefaultApp(lowestWrapper.id); + return { + ...config, + apps: [ + ...config.apps, + ...newApps.map((app) => ({ + ...defaultApp, + ...app, + wrapperId: lowestWrapper.id, + })), + ], + }; + }); modals.close(id); }, onError: () => { @@ -125,4 +144,3 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => { function uuidv4(): any { throw new Error('Function not implemented.'); } - From deee511f8642b5d37e33ff8af358ccbfef80c997 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Sat, 2 Dec 2023 12:41:07 +0100 Subject: [PATCH 5/8] Fix unused function and enable user count check in getServerSideProps --- .../Tools/Docker/docker-select-board.modal.tsx | 3 --- src/pages/onboard.tsx | 12 ++++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx index 4248f4b0e..688c6796c 100644 --- a/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx +++ b/src/components/Manage/Tools/Docker/docker-select-board.modal.tsx @@ -141,6 +141,3 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => { }); umami.track('Add to homarr modal'); }; -function uuidv4(): any { - throw new Error('Function not implemented.'); -} diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx index 7b98b3da1..05b34c07c 100644 --- a/src/pages/onboard.tsx +++ b/src/pages/onboard.tsx @@ -81,12 +81,12 @@ export default function OnboardPage({ } export const getServerSideProps: GetServerSideProps = async (ctx) => { - // const userCount = await getTotalUserCountAsync(); - // if (userCount >= 1) { - // return { - // notFound: true, - // }; - // } + const userCount = await getTotalUserCountAsync(); + if (userCount >= 1) { + return { + notFound: true, + }; + } const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json')); const configs = files.map((file) => getConfig(file)); From f9c9f23b0cfe9a15fa940b11687cac98a34cc5f6 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Fri, 29 Dec 2023 17:57:32 +0100 Subject: [PATCH 6/8] Update overseerr.ts --- src/server/api/routers/overseerr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/api/routers/overseerr.ts b/src/server/api/routers/overseerr.ts index ff29e5de2..fc778c433 100644 --- a/src/server/api/routers/overseerr.ts +++ b/src/server/api/routers/overseerr.ts @@ -7,10 +7,10 @@ import { OriginalLanguage, Result } from '~/modules/overseerr/SearchResult'; import { TvShowResult } from '~/modules/overseerr/TvShow'; import { getConfig } from '~/tools/config/getConfig'; -import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc'; +import { protectedProcedure, createTRPCRouter, publicProcedure } from '../trpc'; export const overseerrRouter = createTRPCRouter({ - search: adminProcedure + search: protectedProcedure .input( z.object({ configName: z.string(), From f20c209c941f05d68df3bc150d195c3c132658c5 Mon Sep 17 00:00:00 2001 From: ajnart Date: Fri, 29 Dec 2023 17:59:19 +0100 Subject: [PATCH 7/8] Address PR comments --- src/pages/onboard.tsx | 2 +- src/server/api/routers/app.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx index 05b34c07c..de85c4eff 100644 --- a/src/pages/onboard.tsx +++ b/src/pages/onboard.tsx @@ -26,7 +26,7 @@ export default function OnboardPage({ const [onboardingSteps, { open: showOnboardingSteps }] = useDisclosure(false); - const isUpgradeFromSchemaOne = false; + const isUpgradeFromSchemaOne = configSchemaVersions.includes(1); return ( <> diff --git a/src/server/api/routers/app.ts b/src/server/api/routers/app.ts index ddfd49a8b..4ec416c71 100644 --- a/src/server/api/routers/app.ts +++ b/src/server/api/routers/app.ts @@ -8,12 +8,6 @@ import { AppType } from '~/types/app'; import { createTRPCRouter, publicProcedure } from '../trpc'; -// const agent = new https.Agent({ rejectUnauthorized: false }); -// const getCacheResponse = unstable_cache( -// async (app) => await axios.get(app.url, { httpsAgent: agent, timeout: 10000 }), -// ['app-id'] -// ); - export const appRouter = createTRPCRouter({ ping: publicProcedure .input( From c83b04dfc026629f5ffcfebaa33b00bbbe9f5230 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sun, 31 Dec 2023 11:25:21 +0100 Subject: [PATCH 8/8] Refactor Docker integration in board page --- src/pages/board/[slug].tsx | 47 ++++++++++++++++++++++++-------------- src/pages/board/index.tsx | 9 +------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/pages/board/[slug].tsx b/src/pages/board/[slug].tsx index c121d93cb..529e2901e 100644 --- a/src/pages/board/[slug].tsx +++ b/src/pages/board/[slug].tsx @@ -1,41 +1,43 @@ -import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; -import { SSRConfig } from 'next-i18next'; +import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; import { z } from 'zod'; import { Dashboard } from '~/components/Dashboard/Dashboard'; import { BoardLayout } from '~/components/layout/Templates/BoardLayout'; import { useInitConfig } from '~/config/init'; -import { env } from '~/env'; +import { dockerRouter } from '~/server/api/routers/docker/router'; import { getServerAuthSession } from '~/server/auth'; import { configExists } from '~/tools/config/configExists'; import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { boardNamespaces } from '~/tools/server/translation-namespaces'; -import { ConfigType } from '~/types/config'; +import { api } from '~/utils/api'; export default function BoardPage({ config: initialConfig, + isDockerEnabled, + initialContainers, }: InferGetServerSidePropsType) { useInitConfig(initialConfig); + const { data } = api.docker.containers.useQuery(undefined, { + initialData: initialContainers ?? undefined, + enabled: isDockerEnabled, + cacheTime: 60 * 1000 * 5, + staleTime: 60 * 1000 * 1, + }); return ( - + ); } -type BoardGetServerSideProps = { - config: ConfigType; - _nextI18Next?: SSRConfig['_nextI18Next']; -}; - const routeParamsSchema = z.object({ slug: z.string(), }); -export const getServerSideProps: GetServerSideProps = async (ctx) => { - const routeParams = routeParamsSchema.safeParse(ctx.params); +export const getServerSideProps = async (context: GetServerSidePropsContext) => { + const routeParams = routeParamsSchema.safeParse(context.params); if (!routeParams.success) { return { notFound: true, @@ -52,21 +54,30 @@ export const getServerSideProps: GetServerSideProps = a const config = await getFrontendConfig(routeParams.data.slug); const translations = await getServerSideTranslations( boardNamespaces, - ctx.locale, - ctx.req, - ctx.res + context.locale, + context.req, + context.res ); - const session = await getServerAuthSession({ req: ctx.req, res: ctx.res }); + const session = await getServerAuthSession({ req: context.req, res: context.res }); const result = checkForSessionOrAskForLogin( - ctx, + context, session, () => config.settings.access.allowGuests || session?.user != undefined ); if (result) { return result; } + const caller = dockerRouter.createCaller({ + session: session, + cookies: context.req.cookies, + }); + let containers = undefined; + // Fetch containers if user is admin, otherwise we don't need them + try { + if (session?.user.isAdmin == true) containers = await caller.containers(); + } catch (error) {} return { props: { @@ -74,6 +85,8 @@ export const getServerSideProps: GetServerSideProps = a primaryColor: config.settings.customization.colors.primary, secondaryColor: config.settings.customization.colors.secondary, primaryShade: config.settings.customization.colors.shade, + isDockerEnabled: containers != undefined, + initialContainers: containers ?? null, ...translations, }, }; diff --git a/src/pages/board/index.tsx b/src/pages/board/index.tsx index 661db269c..6d054627c 100644 --- a/src/pages/board/index.tsx +++ b/src/pages/board/index.tsx @@ -33,11 +33,6 @@ export default function BoardPage({ ); } -type BoardGetServerSideProps = { - config: ConfigType; - _nextI18Next?: SSRConfig['_nextI18Next']; -}; - export const getServerSideProps = async (context: GetServerSidePropsContext) => { const session = await getServerAuthSession(context); const boardName = await getDefaultBoardAsync(session?.user?.id, 'default'); @@ -66,9 +61,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => // Fetch containers if user is admin, otherwise we don't need them try { if (session?.user.isAdmin == true) containers = await caller.containers(); - } catch (error) { - - } + } catch (error) {} return { props: { config,