mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-15 17:56:21 +01:00
🚧 WIP on docker import feature
This commit is contained in:
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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} />}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user