diff --git a/src/modules/Docker/ContainerActionBar.tsx b/src/modules/Docker/ContainerActionBar.tsx index 31a792a5d..2eafcd818 100644 --- a/src/modules/Docker/ContainerActionBar.tsx +++ b/src/modules/Docker/ContainerActionBar.tsx @@ -22,35 +22,22 @@ import { AppType } from '../../types/app'; export interface ContainerActionBarProps { selected: Dockerode.ContainerInfo[]; 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 [isLoading, setLoading] = useState(false); - const { config } = useConfigContext(); - 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 ( - + {/* TODO: add some possibility to add a container to homarr */} ); } diff --git a/src/modules/Docker/DockerTable.tsx b/src/modules/Docker/DockerTable.tsx index 19d8ba0be..998163440 100644 --- a/src/modules/Docker/DockerTable.tsx +++ b/src/modules/Docker/DockerTable.tsx @@ -8,11 +8,11 @@ import { TextInput, createStyles, } from '@mantine/core'; -import { useElementSize } from '@mantine/hooks'; +import { useDebouncedValue, useElementSize } from '@mantine/hooks'; import { IconSearch } from '@tabler/icons-react'; -import Dockerode from 'dockerode'; +import Dockerode, { ContainerInfo } from 'dockerode'; 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 ContainerState from './ContainerState'; @@ -31,94 +31,35 @@ export default function DockerTable({ selection, setSelection, }: { - setSelection: any; - containers: Dockerode.ContainerInfo[]; - selection: Dockerode.ContainerInfo[]; + setSelection: Dispatch>; + containers: ContainerInfo[]; + selection: ContainerInfo[]; }) { - const [usedContainers, setContainers] = useState(containers); - const { classes, cx } = useStyles(); - const [search, setSearch] = useState(''); - const { ref, width, height } = useElementSize(); - const { t } = useTranslation('modules/docker'); + const [search, setSearch] = useState(''); + const { classes, cx } = useStyles(); + const { ref, width } = useElementSize(); - useEffect(() => { - setContainers(containers); - }, [containers]); + const filteredContainers = useMemo( + () => filterContainers(containers, search), + [containers, search] + ); const handleSearchChange = (event: React.ChangeEvent) => { - const { value } = event.currentTarget; - setSearch(value); - setContainers(filterContainers(containers, value)); + setSearch(event.currentTarget.value); }; - 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)) - ); - } - - const toggleRow = (container: Dockerode.ContainerInfo) => - setSelection((current: Dockerode.ContainerInfo[]) => - current.includes(container) ? current.filter((c) => c !== container) : [...current, container] + const toggleRow = (container: ContainerInfo) => + setSelection((selected: ContainerInfo[]) => + selected.includes(container) + ? selected.filter((c) => c !== container) + : [...selected, container] ); const toggleAll = () => - setSelection((current: any) => - current.length === usedContainers.length ? [] : usedContainers.map((c) => c) + setSelection((selected: ContainerInfo[]) => + selected.length === filteredContainers.length ? [] : filteredContainers.map((c) => c) ); - const rows = usedContainers.map((element) => { - const selected = selection.includes(element); - return ( - - - toggleRow(element)} - transitionDuration={0} - /> - - - - {element.Names[0].replace('/', '')} - - - {width > MIN_WIDTH_MOBILE && ( - - {element.Image} - - )} - {width > MIN_WIDTH_MOBILE && ( - - - {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) => ( - - {port.PrivatePort}:{port.PublicPort} - - ))} - {element.Ports.length > 3 && ( - - {t('table.body.portCollapse', { ports: element.Ports.length - 3 })} - - )} - - - )} - - - - - ); - }); - return ( 0} - indeterminate={selection.length > 0 && selection.length !== usedContainers.length} + checked={selection.length === filteredContainers.length && selection.length > 0} + indeterminate={ + selection.length > 0 && selection.length !== filteredContainers.length + } transitionDuration={0} - disabled={usedContainers.length === 0} + disabled={filteredContainers.length === 0} /> {t('table.header.name')} @@ -147,8 +90,83 @@ export default function DockerTable({ {t('table.header.state')} - {rows} + + {filteredContainers.map((container) => { + const selected = selection.includes(container); + return ( + + ); + })} + ); } + +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 ( + + + toggleRow(container)} transitionDuration={0} /> + + + + {container.Names[0].replace('/', '')} + + + {width > MIN_WIDTH_MOBILE && ( + + {container.Image} + + )} + {width > MIN_WIDTH_MOBILE && ( + + + {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) => ( + + {port.PrivatePort}:{port.PublicPort} + + ))} + {container.Ports.length > 3 && ( + + {t('table.body.portCollapse', { ports: container.Ports.length - 3 })} + + )} + + + )} + + + + + ); +}; + +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)) + ); +} diff --git a/src/pages/docker.tsx b/src/pages/docker.tsx new file mode 100644 index 000000000..b269e8c3a --- /dev/null +++ b/src/pages/docker.tsx @@ -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([]); + // 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 ( + + + + + + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ locale, req, res }) => { + const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res); + return { + props: { + ...translations, + }, + }; +};