mirror of
https://github.com/ajnart/homarr.git
synced 2025-10-26 08:06:12 +01:00
feat: import to docker form board, docker button on boards (#1714)
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { closeModal } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconBox, IconBoxAlignTop, 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,17 @@ import { useStyles } from '../Shared/styles';
|
||||
interface AvailableElementTypesProps {
|
||||
modalId: string;
|
||||
onOpenIntegrations: () => void;
|
||||
onOpenStaticElements: () => void;
|
||||
}
|
||||
|
||||
export const AvailableElementTypes = ({
|
||||
modalId,
|
||||
onOpenIntegrations: onOpenWidgets,
|
||||
onOpenStaticElements,
|
||||
}: 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 () => {
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<SelectorBackArrow onClickBack={onClickBack} />
|
||||
|
||||
<Text mb="md" color="dimmed">
|
||||
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.
|
||||
</Text>
|
||||
|
||||
<Grid grow>
|
||||
{/*
|
||||
<GenericAvailableElementType
|
||||
name="Static Text"
|
||||
description="Display a fixed string on your dashboard"
|
||||
image={IconCursorText}
|
||||
handleAddition={async () => {}}
|
||||
/> */}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<Stack m="sm">
|
||||
<SelectorBackArrow onClickBack={onClickBack} />
|
||||
|
||||
<Text mb="md" color="dimmed">
|
||||
@@ -26,6 +26,6 @@ export const AvailableIntegrationElements = ({
|
||||
<WidgetElementType key={k} id={k} image={v.icon} widget={v} />
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,11 +2,10 @@ 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';
|
||||
|
||||
export const SelectElementModal = ({ context, id }: ContextModalProps) => {
|
||||
const [activeTab, setActiveTab] = useState<undefined | 'integrations' | 'static_elements'>();
|
||||
const [activeTab, setActiveTab] = useState<undefined | 'integrations' | 'dockerImport'>();
|
||||
|
||||
switch (activeTab) {
|
||||
case undefined:
|
||||
@@ -14,13 +13,10 @@ export const SelectElementModal = ({ context, id }: ContextModalProps) => {
|
||||
<AvailableElementTypes
|
||||
modalId={id}
|
||||
onOpenIntegrations={() => setActiveTab('integrations')}
|
||||
onOpenStaticElements={() => setActiveTab('static_elements')}
|
||||
/>
|
||||
);
|
||||
case 'integrations':
|
||||
return <AvailableIntegrationElements onClickBack={() => setActiveTab(undefined)} />;
|
||||
case 'static_elements':
|
||||
return <AvailableStaticTypes onClickBack={() => setActiveTab(undefined)} />;
|
||||
default:
|
||||
/* default to the main selection tab */
|
||||
setActiveTab(undefined);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')}
|
||||
@@ -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: <IconCheck />,
|
||||
autoClose: 2000,
|
||||
});
|
||||
notifications.cleanQueue();
|
||||
},
|
||||
onError: (err) => {
|
||||
notifications.update({
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Badge,
|
||||
Checkbox,
|
||||
Group,
|
||||
Image,
|
||||
ScrollArea,
|
||||
Table,
|
||||
Text,
|
||||
@@ -96,6 +97,7 @@ export default function ContainerTable({
|
||||
<Row
|
||||
key={container.Id}
|
||||
container={container}
|
||||
icon={(container as any).icon ?? undefined}
|
||||
selected={selected}
|
||||
toggleRow={toggleRow}
|
||||
width={width}
|
||||
@@ -113,10 +115,12 @@ type RowProps = {
|
||||
selected: boolean;
|
||||
toggleRow: (container: ContainerInfo) => 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 containerName = container.Names[0].replace('/', '');
|
||||
|
||||
return (
|
||||
<tr className={cx({ [classes.rowSelected]: selected })}>
|
||||
@@ -124,13 +128,16 @@ const Row = ({ container, selected, toggleRow, width }: RowProps) => {
|
||||
<Checkbox checked={selected} onChange={() => toggleRow(container)} transitionDuration={0} />
|
||||
</td>
|
||||
<td>
|
||||
<Text size="lg" weight={600}>
|
||||
{container.Names[0].replace('/', '')}
|
||||
</Text>
|
||||
<Group noWrap>
|
||||
<Image withPlaceholder src={icon} width={30} height={30} />
|
||||
<Text size="lg" weight={600}>
|
||||
{containerName}
|
||||
</Text>
|
||||
</Group>
|
||||
</td>
|
||||
{width > MIN_WIDTH_MOBILE && (
|
||||
<td>
|
||||
<Text size="lg">{container.Image}</Text>
|
||||
<Text size="lg">{container.Image.slice(0, 25)}</Text>
|
||||
</td>
|
||||
)}
|
||||
{width > MIN_WIDTH_MOBILE && (
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -14,7 +17,7 @@ const dockerSelectBoardSchema = z.object({
|
||||
});
|
||||
|
||||
type InnerProps = {
|
||||
containers: ContainerInfo[];
|
||||
containers: (ContainerInfo & { icon?: string })[];
|
||||
};
|
||||
type FormType = z.infer<typeof dockerSelectBoardSchema>;
|
||||
|
||||
@@ -22,13 +25,18 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps<Inn
|
||||
const { t } = useTranslation('tools/docker');
|
||||
const { mutateAsync, isLoading } = api.boards.addAppsForContainers.useMutation();
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
const { name: configName } = useConfigContext();
|
||||
|
||||
const updateConfig = useConfigStore((store) => 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,
|
||||
})),
|
||||
apps: newApps,
|
||||
boardName: values.board,
|
||||
},
|
||||
{
|
||||
@@ -39,7 +47,21 @@ export const DockerSelectBoardModal = ({ id, innerProps }: ContextModalProps<Inn
|
||||
icon: <IconCheck />,
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
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: () => {
|
||||
@@ -117,5 +139,5 @@ export const openDockerSelectBoardModal = (innerProps: InnerProps) => {
|
||||
),
|
||||
innerProps,
|
||||
});
|
||||
umami.track('Add to homarr modal')
|
||||
umami.track('Add to homarr modal');
|
||||
};
|
||||
|
||||
@@ -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 { 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';
|
||||
@@ -20,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 (
|
||||
<MainLayout autoFocusSearch={session?.user.autoFocusSearch} headerActions={<HeaderActions />}>
|
||||
<MainLayout autoFocusSearch={session?.user.autoFocusSearch} headerActions={<HeaderActions isDockerEnabled={isDockerEnabled} />}>
|
||||
<BoardHeadOverride />
|
||||
<BackgroundImage />
|
||||
{children}
|
||||
@@ -36,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;
|
||||
@@ -44,11 +55,55 @@ export const HeaderActions = () => {
|
||||
return (
|
||||
<>
|
||||
<ToggleEditModeButton />
|
||||
{isDockerEnabled && <DockerButton />}
|
||||
<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 { name } = useConfigContext();
|
||||
const { t } = useTranslation('boards/common');
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
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,14 +11,23 @@ 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,
|
||||
isDockerEnabled,
|
||||
initialContainers,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
useInitConfig(initialConfig);
|
||||
const { data } = api.docker.containers.useQuery(undefined, {
|
||||
initialData: initialContainers ?? undefined,
|
||||
enabled: isDockerEnabled,
|
||||
cacheTime: 60 * 1000 * 5,
|
||||
staleTime: 60 * 1000 * 1,
|
||||
});
|
||||
|
||||
return (
|
||||
<BoardLayout>
|
||||
<BoardLayout isDockerEnabled={isDockerEnabled}>
|
||||
<Dashboard />
|
||||
</BoardLayout>
|
||||
);
|
||||
@@ -28,33 +38,45 @@ type BoardGetServerSideProps = {
|
||||
_nextI18Next?: SSRConfig['_nextI18Next'];
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<BoardGetServerSideProps> = 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) {
|
||||
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
config,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -18,7 +17,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 +28,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,
|
||||
|
||||
@@ -41,6 +41,7 @@ export const boardRouter = createTRPCRouter({
|
||||
apps: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
icon: z.string().optional(),
|
||||
port: z.number().optional(),
|
||||
})
|
||||
),
|
||||
@@ -54,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 = {
|
||||
@@ -66,11 +66,14 @@ export const boardRouter = createTRPCRouter({
|
||||
const address = container.port
|
||||
? `http://localhost:${container.port}`
|
||||
: 'http://localhost';
|
||||
|
||||
return {
|
||||
...defaultApp,
|
||||
name: container.name,
|
||||
url: address,
|
||||
appearance: {
|
||||
...defaultApp.appearance,
|
||||
iconUrl: container.icon,
|
||||
},
|
||||
behaviour: {
|
||||
...defaultApp.behaviour,
|
||||
externalUrl: address,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
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';
|
||||
|
||||
export const IconRespositories = [
|
||||
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 fetches = IconRespositories.map((rep) => rep.fetch());
|
||||
const data = await Promise.all(fetches);
|
||||
return data;
|
||||
}),
|
||||
|
||||
@@ -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 { protectedProcedure, createTRPCRouter, publicProcedure } from '../trpc';
|
||||
|
||||
export const overseerrRouter = createTRPCRouter({
|
||||
search: publicProcedure
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
configName: z.string(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const boardNamespaces = [
|
||||
'tools/docker',
|
||||
'layout/element-selector/selector',
|
||||
'layout/modals/add-app',
|
||||
'layout/modals/change-position',
|
||||
|
||||
Reference in New Issue
Block a user