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(),