🚧 WIP on docker import feature

This commit is contained in:
ajnart
2023-11-20 13:37:58 +01:00
parent 669d311b0c
commit c88cd3c05e
14 changed files with 151 additions and 235 deletions

View File

@@ -233,4 +233,4 @@
] ]
} }
} }
} }

View File

@@ -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 (
<motion.div
whileHover={{ scale: 1.1 }}
style={{
cursor: 'pointer',
}}
onClick={() =>
setSelected(isSelected ? selected.filter((c) => c !== container) : [...selected, container])
}
>
<Card style={{ height: '100%' }} shadow="sm" radius="md" withBorder>
<Stack justify="space-between" style={{ height: '100%' }}>
<Group
style={{
alignSelf: 'flex-end',
}}
>
<ContainerState state={container.State} />
<Checkbox radius="xl" checked={isSelected} size="sm" />
</Group>
<Center>
<Image
withPlaceholder
h={60}
maw={60}
w={60}
src={foundIcon?.url ?? `https://placehold.co/60x60?text=${containerName}`}
/>
</Center>
<Stack spacing={0}>
<Text lineClamp={1} align="center">
{containerName}
</Text>
{container.Image && (
<Text
size="xs"
style={{ overflow: 'hidden', textOverflow: 'elipsis' }}
align="center"
color="dimmed"
>
{container.Image}
</Text>
)}
</Stack>
</Stack>
</Card>
</motion.div>
);
}
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<Dockerode.ContainerInfo[]>([]);
if (isLoading || !data || !iconsData) return <WidgetLoading />;
return (
<Stack m="sm">
<SelectorBackArrow onClickBack={onClickBack} />
<Grid>
{data?.map((container) => (
<Grid.Col xs={12} sm={4} md={3}>
<DockerDispaly
selected={selected}
setSelected={setSelected}
iconsData={iconsData}
container={container}
/>
</Grid.Col>
))}
</Grid>
<Button
loading={mutationIsLoading}
style={{
zIndex: 1000,
}}
pos={'sticky'}
bottom={10}
disabled={selected.length === 0}
onClick={async () => {
mutateAsync({
apps: selected.map((container) => ({
name: (container.Names.at(0) ?? 'App').replace('/', ''),
port: container.Ports.at(0)?.PublicPort,
icon: findIconForContainer(container, iconsData)?.url,
})),
boardName: config?.configProperties.name!,
}).then(() => {
//TODO: Reload config
closeAllModals();
notifications.show({
title: 'Success',
message: 'Containers added to dashboard',
color: 'green',
});
});
}}
>
Add {selected.length} container{selected.length > 1 && 's'} to dashboard
</Button>
</Stack>
);
}

View File

@@ -1,7 +1,7 @@
import { Group, Space, Stack, Text, UnstyledButton } from '@mantine/core'; import { Group, Space, Stack, Text, UnstyledButton } from '@mantine/core';
import { closeModal } from '@mantine/modals'; import { closeModal } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; 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 { motion } from 'framer-motion';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
@@ -19,13 +19,11 @@ import { useStyles } from '../Shared/styles';
interface AvailableElementTypesProps { interface AvailableElementTypesProps {
modalId: string; modalId: string;
onOpenIntegrations: () => void; onOpenIntegrations: () => void;
onOpenDocker: () => void;
} }
export const AvailableElementTypes = ({ export const AvailableElementTypes = ({
modalId, modalId,
onOpenIntegrations: onOpenWidgets, onOpenIntegrations: onOpenWidgets,
onOpenDocker,
}: AvailableElementTypesProps) => { }: AvailableElementTypesProps) => {
const { t } = useTranslation('layout/element-selector/selector'); const { t } = useTranslation('layout/element-selector/selector');
const { config, name: configName } = useConfigContext(); const { config, name: configName } = useConfigContext();
@@ -99,13 +97,6 @@ export const AvailableElementTypes = ({
}); });
}} }}
/> />
{data && data.user.isAdmin && (
<ElementItem
name={t('importFromDocker')}
icon={<IconBrandDocker size={40} strokeWidth={1.3} />}
onClick={onOpenDocker}
/>
)}
<ElementItem <ElementItem
name={t('widgets')} name={t('widgets')}
icon={<IconStack size={40} strokeWidth={1.3} />} icon={<IconStack size={40} strokeWidth={1.3} />}

View File

@@ -3,7 +3,6 @@ import { useState } from 'react';
import { AvailableElementTypes } from './Components/Overview/AvailableElementsOverview'; import { AvailableElementTypes } from './Components/Overview/AvailableElementsOverview';
import { AvailableIntegrationElements } from './Components/WidgetsTab/AvailableWidgetsTab'; import { AvailableIntegrationElements } from './Components/WidgetsTab/AvailableWidgetsTab';
import ImportFromDockerModal from './Components/DockerImportModal';
export const SelectElementModal = ({ context, id }: ContextModalProps) => { export const SelectElementModal = ({ context, id }: ContextModalProps) => {
const [activeTab, setActiveTab] = useState<undefined | 'integrations' | 'dockerImport'>(); const [activeTab, setActiveTab] = useState<undefined | 'integrations' | 'dockerImport'>();
@@ -14,13 +13,10 @@ export const SelectElementModal = ({ context, id }: ContextModalProps) => {
<AvailableElementTypes <AvailableElementTypes
modalId={id} modalId={id}
onOpenIntegrations={() => setActiveTab('integrations')} onOpenIntegrations={() => setActiveTab('integrations')}
onOpenDocker={() => setActiveTab('dockerImport')}
/> />
); );
case 'integrations': case 'integrations':
return <AvailableIntegrationElements onClickBack={() => setActiveTab(undefined)} />; return <AvailableIntegrationElements onClickBack={() => setActiveTab(undefined)} />;
case 'dockerImport':
return <ImportFromDockerModal onClickBack={() => setActiveTab(undefined)} />;
default: default:
/* default to the main selection tab */ /* default to the main selection tab */
setActiveTab(undefined); setActiveTab(undefined);

View File

@@ -16,7 +16,7 @@ import { RouterInputs, api } from '~/utils/api';
import { openDockerSelectBoardModal } from './docker-select-board.modal'; import { openDockerSelectBoardModal } from './docker-select-board.modal';
export interface ContainerActionBarProps { export interface ContainerActionBarProps {
selected: Dockerode.ContainerInfo[]; selected: (Dockerode.ContainerInfo & { icon?: string })[];
reload: () => void; reload: () => void;
isLoading: boolean; isLoading: boolean;
} }
@@ -127,13 +127,7 @@ const useDockerActionMutation = () => {
{ action, id: container.Id }, { action, id: container.Id },
{ {
onSuccess: () => { onSuccess: () => {
notifications.update({ notifications.cleanQueue();
id: container.Id,
title: containerName,
message: `${t(`actions.${action}.end`)} ${containerName}`,
icon: <IconCheck />,
autoClose: 2000,
});
}, },
onError: (err) => { onError: (err) => {
notifications.update({ notifications.update({

View File

@@ -14,7 +14,6 @@ import { IconSearch } from '@tabler/icons-react';
import Dockerode, { ContainerInfo } from 'dockerode'; import Dockerode, { ContainerInfo } from 'dockerode';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Dispatch, SetStateAction, useMemo, useState } from 'react'; import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import { api } from '~/utils/api';
import { MIN_WIDTH_MOBILE } from '../../../../constants/constants'; import { MIN_WIDTH_MOBILE } from '../../../../constants/constants';
import ContainerState from './ContainerState'; import ContainerState from './ContainerState';
@@ -98,6 +97,7 @@ export default function ContainerTable({
<Row <Row
key={container.Id} key={container.Id}
container={container} container={container}
icon={(container as any).icon ?? undefined}
selected={selected} selected={selected}
toggleRow={toggleRow} toggleRow={toggleRow}
width={width} width={width}
@@ -115,26 +115,12 @@ type RowProps = {
selected: boolean; selected: boolean;
toggleRow: (container: ContainerInfo) => void; toggleRow: (container: ContainerInfo) => void;
width: number; 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 { t } = useTranslation('modules/docker');
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
const { data: iconsData } = api.icon.all.useQuery();
if (!iconsData) return null;
const containerName = container.Names[0].replace('/', ''); 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 ( return (
<tr className={cx({ [classes.rowSelected]: selected })}> <tr className={cx({ [classes.rowSelected]: selected })}>
@@ -143,7 +129,7 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => {
</td> </td>
<td> <td>
<Group noWrap> <Group noWrap>
<Image withPlaceholder src={foundIcon?.url} alt={image} width={30} height={30} /> <Image withPlaceholder src={icon} width={30} height={30} />
<Text size="lg" weight={600}> <Text size="lg" weight={600}>
{containerName} {containerName}
</Text> </Text>
@@ -151,7 +137,7 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => {
</td> </td>
{width > MIN_WIDTH_MOBILE && ( {width > MIN_WIDTH_MOBILE && (
<td> <td>
<Text size="lg">{container.Image}</Text> <Text size="lg">{container.Image.slice(0, 25)}</Text>
</td> </td>
)} )}
{width > MIN_WIDTH_MOBILE && ( {width > MIN_WIDTH_MOBILE && (

View File

@@ -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 { useForm } from '@mantine/form';
import { ContextModalProps, modals } from '@mantine/modals'; import { ContextModalProps, modals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
@@ -6,6 +6,7 @@ import { IconCheck, IconX } from '@tabler/icons-react';
import { ContainerInfo } from 'dockerode'; import { ContainerInfo } from 'dockerode';
import { Trans, useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { useConfigStore } from '~/config/store';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
@@ -14,7 +15,7 @@ const dockerSelectBoardSchema = z.object({
}); });
type InnerProps = { type InnerProps = {
containers: ContainerInfo[]; containers: (ContainerInfo & { icon?: string })[];
}; };
type FormType = z.infer<typeof dockerSelectBoardSchema>; type FormType = z.infer<typeof dockerSelectBoardSchema>;
@@ -22,12 +23,14 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps<Inn
const { t } = useTranslation('tools/docker'); const { t } = useTranslation('tools/docker');
const { mutateAsync, isLoading } = api.boards.addAppsForContainers.useMutation(); const { mutateAsync, isLoading } = api.boards.addAppsForContainers.useMutation();
const { i18nZodResolver } = useI18nZodResolver(); const { i18nZodResolver } = useI18nZodResolver();
const { updateConfig } = useConfigStore();
const handleSubmit = async (values: FormType) => { const handleSubmit = async (values: FormType) => {
await mutateAsync( await mutateAsync(
{ {
apps: innerProps.containers.map((container) => ({ apps: innerProps.containers.map((container) => ({
name: (container.Names.at(0) ?? 'App').replace('/', ''), name: (container.Names.at(0) ?? 'App').replace('/', ''),
port: container.Ports.at(0)?.PublicPort, port: container.Ports.at(0)?.PublicPort,
icon: container.icon,
})), })),
boardName: values.board, boardName: values.board,
}, },
@@ -39,7 +42,7 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps<Inn
icon: <IconCheck />, icon: <IconCheck />,
color: 'green', color: 'green',
}); });
//TODO: Update config or reload it from server
modals.close(id); modals.close(id);
}, },
onError: () => { onError: () => {
@@ -117,5 +120,9 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => {
), ),
innerProps, innerProps,
}); });
umami.track('Add to homarr modal') umami.track('Add to homarr modal');
}; };
function uuidv4(): any {
throw new Error('Function not implemented.');
}

View File

@@ -1,16 +1,26 @@
import { Button, Global, Text, Title, Tooltip, clsx } from '@mantine/core'; import { Button, Global, Modal, Stack, Text, Title, Tooltip, clsx } from '@mantine/core';
import { useHotkeys, useWindowEvent } from '@mantine/hooks'; import { useDisclosure, useHotkeys, useWindowEvent } from '@mantine/hooks';
import { openContextModal } from '@mantine/modals'; import { openContextModal } from '@mantine/modals';
import { hideNotification, showNotification } from '@mantine/notifications'; 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 Consola from 'consola';
import { ContainerInfo } from 'dockerode';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { Trans, useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { env } from 'process'; import { env } from 'process';
import { useEffect, useState } from 'react';
import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore'; import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore';
import { useNamedWrapperColumnCount } from '~/components/Dashboard/Wrappers/gridstack/store'; 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 { BoardHeadOverride } from '~/components/layout/Meta/BoardHeadOverride';
import { HeaderActionButton } from '~/components/layout/header/ActionButton'; import { HeaderActionButton } from '~/components/layout/header/ActionButton';
import { useConfigContext } from '~/config/provider'; import { useConfigContext } from '~/config/provider';
@@ -44,11 +54,55 @@ export const HeaderActions = () => {
return ( return (
<> <>
<ToggleEditModeButton /> <ToggleEditModeButton />
<DockerButton />
<CustomizeBoardButton /> <CustomizeBoardButton />
</> </>
); );
}; };
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 (
<>
<Tooltip label={t('title')}>
<HeaderActionButton onClick={open}>
<IconBrandDocker size={20} stroke={1.5} />
</HeaderActionButton>
</Tooltip>
<Modal
title={t('title')}
withCloseButton={true}
closeOnClickOutside={true}
size="full"
opened={opened}
onClose={close}
>
<Stack>
<ContainerActionBar selected={selection} reload={reload} isLoading={isRefetching} />
<ContainerTable
containers={data ?? []}
selection={selection}
setSelection={setSelection}
/>
</Stack>
</Modal>
</>
);
};
const CustomizeBoardButton = () => { const CustomizeBoardButton = () => {
const { name } = useConfigContext(); const { name } = useConfigContext();
const { t } = useTranslation('boards/common'); const { t } = useTranslation('boards/common');

View File

@@ -1,8 +1,10 @@
import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; import Consola from 'consola';
import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import { SSRConfig } from 'next-i18next'; import { SSRConfig } from 'next-i18next';
import { Dashboard } from '~/components/Dashboard/Dashboard'; import { Dashboard } from '~/components/Dashboard/Dashboard';
import { BoardLayout } from '~/components/layout/Templates/BoardLayout'; import { BoardLayout } from '~/components/layout/Templates/BoardLayout';
import { useInitConfig } from '~/config/init'; import { useInitConfig } from '~/config/init';
import { dockerRouter } from '~/server/api/routers/docker/router';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { getDefaultBoardAsync } from '~/server/db/queries/userSettings'; import { getDefaultBoardAsync } from '~/server/db/queries/userSettings';
import { getFrontendConfig } from '~/tools/config/getFrontendConfig'; import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
@@ -10,11 +12,20 @@ import { getServerSideTranslations } from '~/tools/server/getServerSideTranslati
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder'; import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { boardNamespaces } from '~/tools/server/translation-namespaces'; import { boardNamespaces } from '~/tools/server/translation-namespaces';
import { ConfigType } from '~/types/config'; import { ConfigType } from '~/types/config';
import { api } from '~/utils/api';
export default function BoardPage({ export default function BoardPage({
config: initialConfig, config: initialConfig,
dockerIsConfigured,
initialContainers,
}: InferGetServerSidePropsType<typeof getServerSideProps>) { }: InferGetServerSidePropsType<typeof getServerSideProps>) {
useInitConfig(initialConfig); useInitConfig(initialConfig);
const { data } = api.docker.containers.useQuery(undefined, {
initialData: initialContainers,
enabled: dockerIsConfigured,
cacheTime: 60 * 1000 * 5,
staleTime: 60 * 1000 * 1,
});
return ( return (
<BoardLayout> <BoardLayout>
@@ -28,33 +39,45 @@ type BoardGetServerSideProps = {
_nextI18Next?: SSRConfig['_nextI18Next']; _nextI18Next?: SSRConfig['_nextI18Next'];
}; };
export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = async (ctx) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerAuthSession(ctx); const session = await getServerAuthSession(context);
const boardName = await getDefaultBoardAsync(session?.user?.id, 'default'); const boardName = await getDefaultBoardAsync(session?.user?.id, 'default');
const translations = await getServerSideTranslations( const translations = await getServerSideTranslations(
boardNamespaces, boardNamespaces,
ctx.locale, context.locale,
ctx.req, context.req,
ctx.res context.res
); );
const config = await getFrontendConfig(boardName); const config = await getFrontendConfig(boardName);
const result = checkForSessionOrAskForLogin( const result = checkForSessionOrAskForLogin(
ctx, context,
session, session,
() => config.settings.access.allowGuests || session?.user != undefined () => config.settings.access.allowGuests || session?.user != undefined
); );
if (result) { if (result) {
return 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 { return {
props: { props: {
config, config,
primaryColor: config.settings.customization.colors.primary, primaryColor: config.settings.customization.colors.primary,
secondaryColor: config.settings.customization.colors.secondary, secondaryColor: config.settings.customization.colors.secondary,
primaryShade: config.settings.customization.colors.shade, primaryShade: config.settings.customization.colors.shade,
dockerIsConfigured: containers != undefined,
initialContainers: containers,
...translations, ...translations,
}, },
}; };

View File

@@ -1,7 +1,6 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import axios, { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import Consola from 'consola'; import Consola from 'consola';
import https from 'https';
import { z } from 'zod'; import { z } from 'zod';
import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing'; import { isStatusOk } from '~/components/Dashboard/Tiles/Apps/AppPing';
import { getConfig } from '~/tools/config/getConfig'; import { getConfig } from '~/tools/config/getConfig';
@@ -9,6 +8,12 @@ import { AppType } from '~/types/app';
import { createTRPCRouter, publicProcedure } from '../trpc'; 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({ export const appRouter = createTRPCRouter({
ping: publicProcedure ping: publicProcedure
.input( .input(
@@ -18,7 +23,6 @@ export const appRouter = createTRPCRouter({
}) })
) )
.query(async ({ input }) => { .query(async ({ input }) => {
const agent = new https.Agent({ rejectUnauthorized: false });
const config = getConfig(input.configName); const config = getConfig(input.configName);
const app = config.apps.find((app) => app.id === input.id); 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`, 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) => ({ .then((response) => ({
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,

View File

@@ -55,7 +55,6 @@ export const boardRouter = createTRPCRouter({
}); });
} }
const config = await getConfig(input.boardName); const config = await getConfig(input.boardName);
const lowestWrapper = config?.wrappers.sort((a, b) => a.position - b.position)[0]; const lowestWrapper = config?.wrappers.sort((a, b) => a.position - b.position)[0];
const newConfig = { const newConfig = {
@@ -67,14 +66,13 @@ export const boardRouter = createTRPCRouter({
const address = container.port const address = container.port
? `http://localhost:${container.port}` ? `http://localhost:${container.port}`
: 'http://localhost'; : 'http://localhost';
return { return {
...defaultApp, ...defaultApp,
name: container.name, name: container.name,
url: address, url: address,
appearance: { appearance: {
...defaultApp.appearance, ...defaultApp.appearance,
icon: container.icon, iconUrl: container.icon,
}, },
behaviour: { behaviour: {
...defaultApp.behaviour, ...defaultApp.behaviour,

View File

@@ -3,6 +3,7 @@ import Dockerode from 'dockerode';
import { z } from 'zod'; import { z } from 'zod';
import { adminProcedure, createTRPCRouter } from '../../trpc'; import { adminProcedure, createTRPCRouter } from '../../trpc';
import { IconRespositories } from '../icon';
import DockerSingleton from './DockerSingleton'; import DockerSingleton from './DockerSingleton';
const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']); const dockerActionSchema = z.enum(['remove', 'start', 'stop', 'restart']);
@@ -12,7 +13,27 @@ export const dockerRouter = createTRPCRouter({
try { try {
const docker = new Dockerode({}); const docker = new Dockerode({});
const containers = await docker.listContainers({ all: true }); 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) { } catch (err) {
throw new TRPCError({ throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR', code: 'INTERNAL_SERVER_ERROR',

View File

@@ -5,7 +5,7 @@ import { UnpkgIconsRepository } from '~/tools/server/images/unpkg-icons-reposito
import { createTRPCRouter, publicProcedure } from '../trpc'; import { createTRPCRouter, publicProcedure } from '../trpc';
const respositories = [ export const IconRespositories = [
new LocalIconsRepository(), new LocalIconsRepository(),
new GitHubIconsRepository( new GitHubIconsRepository(
GitHubIconsRepository.walkxcode, GitHubIconsRepository.walkxcode,
@@ -31,7 +31,7 @@ const respositories = [
export const iconRouter = createTRPCRouter({ export const iconRouter = createTRPCRouter({
all: publicProcedure.query(async () => { all: publicProcedure.query(async () => {
const fetches = respositories.map((rep) => rep.fetch()); const fetches = IconRespositories.map((rep) => rep.fetch());
const data = await Promise.all(fetches); const data = await Promise.all(fetches);
return data; return data;
}), }),

View File

@@ -1,4 +1,5 @@
export const boardNamespaces = [ export const boardNamespaces = [
'tools/docker',
'layout/element-selector/selector', 'layout/element-selector/selector',
'layout/modals/add-app', 'layout/modals/add-app',
'layout/modals/change-position', 'layout/modals/change-position',