Moving things around

This commit is contained in:
ajnart
2023-01-06 12:11:47 +09:00
parent 8f49b2ecfb
commit 94e36f3e0f
6 changed files with 8 additions and 8 deletions

View File

@@ -1,217 +0,0 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Button, Group } from '@mantine/core';
import { showNotification, updateNotification } from '@mantine/notifications';
import {
IconCheck,
IconPlayerPlay,
IconPlayerStop,
IconPlus,
IconRefresh,
IconRotateClockwise,
IconTrash,
} from '@tabler/icons';
import axios from 'axios';
import Dockerode from 'dockerode';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { TFunction } from 'react-i18next';
import { v4 as uuidv4 } from 'uuid';
import { useConfigContext } from '../../../../../config/provider';
import { tryMatchService } from '../../../../../tools/addToHomarr';
import { openContextModalGeneric } from '../../../../../tools/mantineModalManagerExtensions';
import { AppType } from '../../../../../types/app';
import { appTileDefinition } from '../../../../Dashboard/Tiles/Apps/AppTile';
let t: TFunction<'modules/docker', undefined>;
function sendDockerCommand(
action: string,
containerId: string,
containerName: string,
reload: () => void
) {
showNotification({
id: containerId,
loading: true,
title: `${t(`actions.${action}.start`)} ${containerName}`,
message: undefined,
autoClose: false,
disallowClose: true,
});
axios
.get(`/api/docker/container/${containerId}?action=${action}`)
.then((res) => {
updateNotification({
id: containerId,
title: containerName,
message: `${t(`actions.${action}.end`)} ${containerName}`,
icon: <IconCheck />,
autoClose: 2000,
});
})
.catch((err) => {
updateNotification({
id: containerId,
color: 'red',
title: t('errors.unknownError.title'),
message: err.response.data.reason,
autoClose: 2000,
});
})
.finally(() => {
reload();
});
}
export interface ContainerActionBarProps {
selected: Dockerode.ContainerInfo[];
reload: () => void;
}
export default function ContainerActionBar({ selected, reload }: ContainerActionBarProps) {
t = useTranslation('modules/docker').t;
const [isLoading, setisLoading] = useState(false);
const { name: configName, config } = useConfigContext();
const getLowestWrapper = () => config?.wrappers.sort((a, b) => a.position - b.position)[0];
return (
<Group spacing="xs">
<Button
leftIcon={<IconRefresh />}
onClick={() => {
setisLoading(true);
setTimeout(() => {
reload();
setisLoading(false);
}, 750);
}}
variant="light"
color="violet"
loading={isLoading}
radius="md"
>
{t('actionBar.refreshData.title')}
</Button>
<Button
leftIcon={<IconRotateClockwise />}
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('restart', container.Id, container.Names[0].substring(1), reload)
)
)
}
variant="light"
color="orange"
radius="md"
disabled={selected.length === 0}
>
{t('actionBar.restart.title')}
</Button>
<Button
leftIcon={<IconPlayerStop />}
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('stop', container.Id, container.Names[0].substring(1), reload)
)
)
}
variant="light"
color="red"
radius="md"
disabled={selected.length === 0}
>
{t('actionBar.stop.title')}
</Button>
<Button
leftIcon={<IconPlayerPlay />}
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('start', container.Id, container.Names[0].substring(1), reload)
)
)
}
variant="light"
color="green"
radius="md"
disabled={selected.length === 0}
>
{t('actionBar.start.title')}
</Button>
<Button
leftIcon={<IconTrash />}
color="red"
variant="light"
radius="md"
onClick={() =>
Promise.all(
selected.map((container) =>
sendDockerCommand('remove', container.Id, container.Names[0].substring(1), reload)
)
)
}
disabled={selected.length === 0}
>
{t('actionBar.remove.title')}
</Button>
<Button
leftIcon={<IconPlus />}
color="indigo"
variant="light"
radius="md"
disabled={selected.length === 0 || selected.length > 1}
onClick={() => {
const app = tryMatchService(selected.at(0)!);
const containerUrl = `http://localhost:${selected[0].Ports[0].PublicPort}`;
openContextModalGeneric<{ app: AppType; allowAppNamePropagation: boolean }>({
modal: 'editApp',
innerProps: {
app: {
id: uuidv4(),
name: app.name ? app.name : selected[0].Names[0].substring(1),
url: containerUrl,
appearance: {
iconUrl: app.icon ? app.icon : '/imgs/logo/logo.png',
},
network: {
enabledStatusChecker: true,
okStatus: [200],
},
behaviour: {
isOpeningNewTab: true,
externalUrl: '',
},
area: {
type: 'wrapper',
properties: {
id: getLowestWrapper()?.id ?? 'default',
},
},
shape: {
location: {
x: 0,
y: 0,
},
size: {
width: appTileDefinition.minWidth,
height: appTileDefinition.minHeight,
},
},
integration: {
type: null,
properties: [],
},
},
allowAppNamePropagation: true,
},
size: 'xl',
});
}}
>
{t('actionBar.addToHomarr.title')}
</Button>
</Group>
);
}

View File

@@ -1,53 +0,0 @@
import { Badge, BadgeVariant, MantineSize } from '@mantine/core';
import { useTranslation } from 'next-i18next';
import Dockerode from 'dockerode';
export interface ContainerStateProps {
state: Dockerode.ContainerInfo['State'];
}
export default function ContainerState(props: ContainerStateProps) {
const { state } = props;
const { t } = useTranslation('modules/docker');
const options: {
size: MantineSize;
radius: MantineSize;
variant: BadgeVariant;
} = {
size: 'md',
radius: 'md',
variant: 'outline',
};
switch (state) {
case 'running': {
return (
<Badge color="green" {...options}>
{t('table.states.running')}
</Badge>
);
}
case 'created': {
return (
<Badge color="cyan" {...options}>
{t('table.states.created')}
</Badge>
);
}
case 'exited': {
return (
<Badge color="red" {...options}>
{t('table.states.stopped')}
</Badge>
);
}
default: {
return (
<Badge color="purple" {...options}>
{t('table.states.unknown')}
</Badge>
);
}
}
}

View File

@@ -1,82 +0,0 @@
import { ActionIcon, Drawer, Text, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconBrandDocker, IconX } from '@tabler/icons';
import axios from 'axios';
import Docker from 'dockerode';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import { useConfigContext } from '../../../../../config/provider';
import ContainerActionBar from './ContainerActionBar';
import DockerTable from './DockerTable';
export default function DockerMenuButton(props: any) {
const [opened, setOpened] = useState(false);
const [containers, setContainers] = useState<Docker.ContainerInfo[]>([]);
const [selection, setSelection] = useState<Docker.ContainerInfo[]>([]);
const { config } = useConfigContext();
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
const { t } = useTranslation('modules/docker');
useEffect(() => {
reload();
}, [config?.settings]);
function reload() {
if (!dockerEnabled) {
return;
}
setTimeout(() => {
axios
.get('/api/docker/containers')
.then((res) => {
setContainers(res.data);
setSelection([]);
})
.catch(() => {
// Remove containers from the list
setContainers([]);
// Send an Error notification
showNotification({
autoClose: 1500,
title: <Text>{t('errors.integrationFailed.title')}</Text>,
color: 'red',
icon: <IconX />,
message: t('errors.integrationFailed.message'),
});
});
}, 300);
}
if (!dockerEnabled) {
return null;
}
return (
<>
<Drawer
opened={opened}
onClose={() => setOpened(false)}
padding="xl"
position="right"
size="full"
title={<ContainerActionBar selected={selection} reload={reload} />}
>
<DockerTable containers={containers} selection={selection} setSelection={setSelection} />
</Drawer>
<Tooltip label={t('actionIcon.tooltip')}>
<ActionIcon
variant="default"
radius="md"
size="xl"
color="blue"
onClick={() => setOpened(true)}
>
<IconBrandDocker />
</ActionIcon>
</Tooltip>
</>
);
}

View File

@@ -1,144 +0,0 @@
import {
Table,
Checkbox,
Group,
Badge,
createStyles,
ScrollArea,
TextInput,
useMantineTheme,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons';
import Dockerode from 'dockerode';
import { useTranslation } from 'next-i18next';
import { useEffect, useState } from 'react';
import ContainerState from './ContainerState';
const useStyles = createStyles((theme) => ({
rowSelected: {
backgroundColor:
theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors[theme.primaryColor][7], 0.2)
: theme.colors[theme.primaryColor][0],
},
}));
export default function DockerTable({
containers,
selection,
setSelection,
}: {
setSelection: any;
containers: Dockerode.ContainerInfo[];
selection: Dockerode.ContainerInfo[];
}) {
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
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');
useEffect(() => {
setContainers(containers);
}, [containers]);
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.currentTarget;
setSearch(value);
setContainers(filterContainers(containers, 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 toggleAll = () =>
setSelection((current: any) =>
current.length === usedContainers.length ? [] : usedContainers.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>{element.Names[0].replace('/', '')}</td>
{width > MIN_WIDTH_MOBILE && <td>{element.Image}</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 (
<ScrollArea style={{ height: '80vh' }}>
<TextInput
placeholder={t('search.placeholder')}
mt="md"
icon={<IconSearch size={14} />}
value={search}
onChange={handleSearchChange}
/>
<Table ref={ref} captionSide="bottom" highlightOnHover verticalSpacing="sm">
<thead>
<tr>
<th style={{ width: 40 }}>
<Checkbox
onChange={toggleAll}
checked={selection.length === usedContainers.length && selection.length > 0}
indeterminate={selection.length > 0 && selection.length !== usedContainers.length}
transitionDuration={0}
disabled={usedContainers.length === 0}
/>
</th>
<th>{t('table.header.name')}</th>
{width > MIN_WIDTH_MOBILE ? <th>{t('table.header.image')}</th> : null}
{width > MIN_WIDTH_MOBILE ? <th>{t('table.header.ports')}</th> : null}
<th>{t('table.header.state')}</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</Table>
</ScrollArea>
);
}

View File

@@ -4,7 +4,7 @@ import { CURRENT_VERSION, REPO_URL } from '../../../../data/constants';
import { useConfigContext } from '../../../config/provider';
import { Logo } from '../Logo';
import { useCardStyles } from '../useCardStyles';
import DockerMenuButton from './Actions/Docker/DockerModule';
import DockerMenuButton from '../../../modules/Docker/DockerModule';
import { ToggleEditModeAction } from './Actions/ToggleEditMode/ToggleEditMode';
import { Search } from './Search';
import { SettingsMenu } from './SettingsMenu';