Add docker page

This commit is contained in:
Meier Lukas
2023-07-30 09:48:35 +02:00
parent e1aaf82602
commit 07b7b3acec
3 changed files with 154 additions and 156 deletions

View File

@@ -22,35 +22,22 @@ import { AppType } from '../../types/app';
export interface ContainerActionBarProps { export interface ContainerActionBarProps {
selected: Dockerode.ContainerInfo[]; selected: Dockerode.ContainerInfo[];
reload: () => void; reload: () => void;
isLoading: boolean;
} }
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) { export default function ContainerActionBar({
selected,
reload,
isLoading,
}: ContainerActionBarProps) {
const { t } = useTranslation('modules/docker'); const { t } = useTranslation('modules/docker');
const [isLoading, setLoading] = useState(false);
const { config } = useConfigContext();
const sendDockerCommand = useDockerActionMutation(); const sendDockerCommand = useDockerActionMutation();
if (!config) {
return null;
}
const getLowestWrapper = () =>
config.wrappers.sort((wrapper1, wrapper2) => wrapper1.position - wrapper2.position)[0];
if (process.env.DISABLE_EDIT_MODE === 'true') {
return null;
}
return ( return (
<Group spacing="xs"> <Group spacing="xs">
<Button <Button
leftIcon={<IconRefresh />} leftIcon={<IconRefresh />}
onClick={() => { onClick={reload}
setLoading(true);
setTimeout(() => {
reload();
setLoading(false);
}, 750);
}}
variant="light" variant="light"
color="violet" color="violet"
loading={isLoading} loading={isLoading}
@@ -106,58 +93,7 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
> >
{t('actionBar.remove.title')} {t('actionBar.remove.title')}
</Button> </Button>
<Button {/* TODO: add some possibility to add a container to homarr */}
leftIcon={<IconPlus />}
color="indigo"
variant="light"
radius="md"
disabled={selected.length !== 1}
onClick={() => {
const containerInfo = selected[0];
const port = containerInfo.Ports.at(0)?.PublicPort;
const address = port ? `http://localhost:${port}` : `http://localhost`;
const name = containerInfo.Names.at(0) ?? 'App';
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
modal: 'editApp',
zIndex: 202,
innerProps: {
app: {
id: uuidv4(),
name: name,
url: address,
appearance: {
iconUrl: '/imgs/logo/logo.png',
},
network: {
enabledStatusChecker: true,
statusCodes: ['200', '301', '302']
},
behaviour: {
isOpeningNewTab: true,
externalUrl: address
},
area: {
type: 'wrapper',
properties: {
id: getLowestWrapper()?.id ?? 'default',
},
},
shape: {},
integration: {
type: null,
properties: [],
},
},
allowAppNamePropagation: true,
},
size: 'xl',
});
}}
>
{t('actionBar.addToHomarr.title')}
</Button>
</Group> </Group>
); );
} }

View File

@@ -8,11 +8,11 @@ import {
TextInput, TextInput,
createStyles, createStyles,
} from '@mantine/core'; } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useDebouncedValue, useElementSize } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import Dockerode from 'dockerode'; import Dockerode, { ContainerInfo } from 'dockerode';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
import { MIN_WIDTH_MOBILE } from '../../constants/constants'; import { MIN_WIDTH_MOBILE } from '../../constants/constants';
import ContainerState from './ContainerState'; import ContainerState from './ContainerState';
@@ -31,94 +31,35 @@ export default function DockerTable({
selection, selection,
setSelection, setSelection,
}: { }: {
setSelection: any; setSelection: Dispatch<SetStateAction<ContainerInfo[]>>;
containers: Dockerode.ContainerInfo[]; containers: ContainerInfo[];
selection: Dockerode.ContainerInfo[]; selection: ContainerInfo[];
}) { }) {
const [usedContainers, setContainers] = useState<Dockerode.ContainerInfo[]>(containers);
const { classes, cx } = useStyles();
const [search, setSearch] = useState('');
const { ref, width, height } = useElementSize();
const { t } = useTranslation('modules/docker'); const { t } = useTranslation('modules/docker');
const [search, setSearch] = useState('');
const { classes, cx } = useStyles();
const { ref, width } = useElementSize();
useEffect(() => { const filteredContainers = useMemo(
setContainers(containers); () => filterContainers(containers, search),
}, [containers]); [containers, search]
);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.currentTarget; setSearch(event.currentTarget.value);
setSearch(value);
setContainers(filterContainers(containers, value));
}; };
function filterContainers(data: Dockerode.ContainerInfo[], search: string) { const toggleRow = (container: ContainerInfo) =>
const query = search.toLowerCase().trim(); setSelection((selected: ContainerInfo[]) =>
return data.filter((item) => selected.includes(container)
item.Names.some((name) => name.toLowerCase().includes(query) || item.Image.includes(query)) ? selected.filter((c) => c !== container)
); : [...selected, container]
}
const toggleRow = (container: Dockerode.ContainerInfo) =>
setSelection((current: Dockerode.ContainerInfo[]) =>
current.includes(container) ? current.filter((c) => c !== container) : [...current, container]
); );
const toggleAll = () => const toggleAll = () =>
setSelection((current: any) => setSelection((selected: ContainerInfo[]) =>
current.length === usedContainers.length ? [] : usedContainers.map((c) => c) selected.length === filteredContainers.length ? [] : filteredContainers.map((c) => c)
); );
const rows = usedContainers.map((element) => {
const selected = selection.includes(element);
return (
<tr key={element.Id} className={cx({ [classes.rowSelected]: selected })}>
<td>
<Checkbox
checked={selection.includes(element)}
onChange={() => toggleRow(element)}
transitionDuration={0}
/>
</td>
<td>
<Text size="lg" weight={600}>
{element.Names[0].replace('/', '')}
</Text>
</td>
{width > MIN_WIDTH_MOBILE && (
<td>
<Text size="lg">{element.Image}</Text>
</td>
)}
{width > MIN_WIDTH_MOBILE && (
<td>
<Group>
{element.Ports.sort((a, b) => a.PrivatePort - b.PrivatePort)
// Remove duplicates with filter function
.filter(
(port, index, self) =>
index === self.findIndex((t) => t.PrivatePort === port.PrivatePort)
)
.slice(-3)
.map((port) => (
<Badge key={port.PrivatePort} variant="outline">
{port.PrivatePort}:{port.PublicPort}
</Badge>
))}
{element.Ports.length > 3 && (
<Badge variant="filled">
{t('table.body.portCollapse', { ports: element.Ports.length - 3 })}
</Badge>
)}
</Group>
</td>
)}
<td>
<ContainerState state={element.State} />
</td>
</tr>
);
});
return ( return (
<ScrollArea style={{ height: '100%' }} offsetScrollbars> <ScrollArea style={{ height: '100%' }} offsetScrollbars>
<TextInput <TextInput
@@ -135,10 +76,12 @@ export default function DockerTable({
<th style={{ width: 40 }}> <th style={{ width: 40 }}>
<Checkbox <Checkbox
onChange={toggleAll} onChange={toggleAll}
checked={selection.length === usedContainers.length && selection.length > 0} checked={selection.length === filteredContainers.length && selection.length > 0}
indeterminate={selection.length > 0 && selection.length !== usedContainers.length} indeterminate={
selection.length > 0 && selection.length !== filteredContainers.length
}
transitionDuration={0} transitionDuration={0}
disabled={usedContainers.length === 0} disabled={filteredContainers.length === 0}
/> />
</th> </th>
<th>{t('table.header.name')}</th> <th>{t('table.header.name')}</th>
@@ -147,8 +90,83 @@ export default function DockerTable({
<th>{t('table.header.state')}</th> <th>{t('table.header.state')}</th>
</tr> </tr>
</thead> </thead>
<tbody>{rows}</tbody> <tbody>
{filteredContainers.map((container) => {
const selected = selection.includes(container);
return (
<Row
key={container.Id}
container={container}
selected={selected}
toggleRow={toggleRow}
width={width}
/>
);
})}
</tbody>
</Table> </Table>
</ScrollArea> </ScrollArea>
); );
} }
type RowProps = {
container: ContainerInfo;
selected: boolean;
toggleRow: (container: ContainerInfo) => void;
width: number;
};
const Row = ({ container, selected, toggleRow, width }: RowProps) => {
const { t } = useTranslation('modules/docker');
const { classes, cx } = useStyles();
return (
<tr className={cx({ [classes.rowSelected]: selected })}>
<td>
<Checkbox checked={selected} onChange={() => toggleRow(container)} transitionDuration={0} />
</td>
<td>
<Text size="lg" weight={600}>
{container.Names[0].replace('/', '')}
</Text>
</td>
{width > MIN_WIDTH_MOBILE && (
<td>
<Text size="lg">{container.Image}</Text>
</td>
)}
{width > MIN_WIDTH_MOBILE && (
<td>
<Group>
{container.Ports.sort((a, b) => a.PrivatePort - b.PrivatePort)
// Remove duplicates with filter function
.filter(
(port, index, self) =>
index === self.findIndex((t) => t.PrivatePort === port.PrivatePort)
)
.slice(-3)
.map((port) => (
<Badge key={port.PrivatePort} variant="outline">
{port.PrivatePort}:{port.PublicPort}
</Badge>
))}
{container.Ports.length > 3 && (
<Badge variant="filled">
{t('table.body.portCollapse', { ports: container.Ports.length - 3 })}
</Badge>
)}
</Group>
</td>
)}
<td>
<ContainerState state={container.State} />
</td>
</tr>
);
};
function filterContainers(data: Dockerode.ContainerInfo[], search: string) {
const query = search.toLowerCase().trim();
return data.filter((item) =>
item.Names.some((name) => name.toLowerCase().includes(query) || item.Image.includes(query))
);
}

44
src/pages/docker.tsx Normal file
View File

@@ -0,0 +1,44 @@
import { Stack } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { ContainerInfo } from 'dockerode';
import { GetServerSideProps } from 'next';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { MainLayout } from '~/components/layout/main';
import ContainerActionBar from '~/modules/Docker/ContainerActionBar';
import DockerTable from '~/modules/Docker/DockerTable';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
import { dashboardNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api';
export default function DockerPage() {
const [selection, setSelection] = useState<ContainerInfo[]>([]);
// TODO: read that from somewhere else?
const dockerEnabled = true;
const { data, refetch, isRefetching } = api.docker.containers.useQuery(undefined, {
enabled: dockerEnabled,
});
const reload = () => {
refetch();
setSelection([]);
};
return (
<MainLayout>
<Stack>
<ContainerActionBar selected={selection} reload={reload} isLoading={isRefetching} />
<DockerTable containers={data ?? []} selection={selection} setSelection={setSelection} />
</Stack>
</MainLayout>
);
}
export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => {
const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res);
return {
props: {
...translations,
},
};
};