forked from github-starred/komodo
398 lines
11 KiB
TypeScript
398 lines
11 KiB
TypeScript
import { Section } from "@components/layouts";
|
|
import { ResourceLink, ResourcePageHeader } from "@components/resources/common";
|
|
import { useServer } from "@components/resources/server";
|
|
import {
|
|
ConfirmButton,
|
|
ContainerPortLink,
|
|
DOCKER_LINK_ICONS,
|
|
DockerLabelsSection,
|
|
DockerResourceLink,
|
|
} from "@components/util";
|
|
import {
|
|
useContainerPortsMap,
|
|
useLocalStorage,
|
|
useRead,
|
|
useSetTitle,
|
|
useWrite,
|
|
} from "@lib/hooks";
|
|
import { Button } from "@ui/button";
|
|
import { DataTable } from "@ui/data-table";
|
|
import {
|
|
ChevronLeft,
|
|
Clapperboard,
|
|
Info,
|
|
Loader2,
|
|
PlusCircle,
|
|
} from "lucide-react";
|
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
|
import { ContainerLogs } from "./log";
|
|
import { Actions } from "./actions";
|
|
import { ConnectExecQuery, Types } from "komodo_client";
|
|
import { container_state_intention } from "@lib/color";
|
|
import { UsableResource } from "@types";
|
|
import { Fragment } from "react/jsx-runtime";
|
|
import { usePermissions } from "@lib/hooks";
|
|
import { ResourceNotifications } from "@pages/resource-notifications";
|
|
import { ContainerTerminal } from "@components/terminal/container";
|
|
import { ContainerInspect } from "./inspect";
|
|
import { useMemo } from "react";
|
|
import {
|
|
MobileFriendlyTabsSelector,
|
|
TabNoContent,
|
|
} from "@ui/mobile-friendly-tabs";
|
|
|
|
export default function ContainerPage() {
|
|
const { type, id, container } = useParams() as {
|
|
type: string;
|
|
id: string;
|
|
container: string;
|
|
};
|
|
if (type !== "servers") {
|
|
return <div>This resource type does not have any containers.</div>;
|
|
}
|
|
return (
|
|
<ContainerPageInner id={id} container={decodeURIComponent(container)} />
|
|
);
|
|
}
|
|
|
|
const ContainerPageInner = ({
|
|
id,
|
|
container: container_name,
|
|
}: {
|
|
id: string;
|
|
container: string;
|
|
}) => {
|
|
const server = useServer(id);
|
|
useSetTitle(`${server?.name} | container | ${container_name}`);
|
|
const { canExecute } = usePermissions({ type: "Server", id });
|
|
const list_container = useRead(
|
|
"ListDockerContainers",
|
|
{
|
|
server: id,
|
|
},
|
|
{ refetchInterval: 10_000 }
|
|
).data?.find((container) => container.name === container_name);
|
|
const ports_map = useContainerPortsMap(list_container?.ports ?? []);
|
|
|
|
const state = list_container?.state ?? Types.ContainerStateStatusEnum.Empty;
|
|
const intention = container_state_intention(state);
|
|
|
|
return (
|
|
<div>
|
|
<div className="w-full flex items-center justify-between mb-12">
|
|
<Link to={"/servers/" + id}>
|
|
<Button className="gap-2" variant="secondary">
|
|
<ChevronLeft className="w-4" />
|
|
Back
|
|
</Button>
|
|
</Link>
|
|
<NewDeployment id={id} name={container_name} />
|
|
</div>
|
|
<div className="flex flex-col xl:flex-row gap-4">
|
|
{/** HEADER */}
|
|
<div className="w-full flex flex-col gap-4">
|
|
<div className="flex flex-col gap-2 border rounded-md">
|
|
{/* <Components.ResourcePageHeader id={id} /> */}
|
|
<ResourcePageHeader
|
|
type={undefined}
|
|
id={undefined}
|
|
intent={intention}
|
|
icon={
|
|
<DOCKER_LINK_ICONS.container
|
|
server_id={id}
|
|
name={container_name}
|
|
size={8}
|
|
/>
|
|
}
|
|
resource={undefined}
|
|
name={container_name}
|
|
state={state}
|
|
status={list_container?.status}
|
|
/>
|
|
<div className="flex flex-col pb-2 px-4">
|
|
<div className="flex items-center gap-x-4 gap-y-1 flex-wrap text-muted-foreground">
|
|
<ResourceLink type="Server" id={id} />
|
|
<AttachedResource id={id} container={container_name} />
|
|
{list_container?.image && (
|
|
<>
|
|
|
|
|
<DockerResourceLink
|
|
type="image"
|
|
server_id={id}
|
|
name={list_container.image}
|
|
id={list_container.image_id}
|
|
muted
|
|
/>
|
|
</>
|
|
)}
|
|
{list_container?.networks?.map((network) => (
|
|
<Fragment key={network}>
|
|
|
|
|
<DockerResourceLink
|
|
type="network"
|
|
server_id={id}
|
|
name={network}
|
|
muted
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
{list_container?.volumes?.map((volume) => (
|
|
<Fragment key={volume}>
|
|
|
|
|
<DockerResourceLink
|
|
type="volume"
|
|
server_id={id}
|
|
name={volume}
|
|
muted
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
{Object.keys(ports_map).map((host_port) => (
|
|
<Fragment key={host_port}>
|
|
|
|
|
<ContainerPortLink
|
|
host_port={host_port}
|
|
ports={ports_map[host_port]}
|
|
server_id={id}
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* <ResourceDescription type="Server" id={id} disabled={!canWrite} /> */}
|
|
</div>
|
|
{/** NOTIFICATIONS */}
|
|
<ResourceNotifications type="Server" id={id} />
|
|
</div>
|
|
|
|
<div className="mt-8 flex flex-col gap-12">
|
|
{/* Actions */}
|
|
{canExecute && (
|
|
<Section title="Actions" icon={<Clapperboard className="w-4 h-4" />}>
|
|
<div className="flex gap-4 items-center flex-wrap">
|
|
{Object.entries(Actions).map(([key, Action]) => (
|
|
<Action key={key} id={id} container={container_name} />
|
|
))}
|
|
</div>
|
|
</Section>
|
|
)}
|
|
|
|
<ContainerTabs server={id} container={container_name} state={state} />
|
|
|
|
{/* TOP LEVEL CONTAINER INFO */}
|
|
{list_container && (
|
|
<Section title="Details" icon={<Info className="w-4 h-4" />}>
|
|
<DataTable
|
|
tableKey="container-info"
|
|
data={[list_container]}
|
|
columns={[
|
|
{
|
|
header: "Id",
|
|
accessorKey: "id",
|
|
},
|
|
{
|
|
header: "Image",
|
|
accessorKey: "image",
|
|
},
|
|
{
|
|
header: "Network Mode",
|
|
accessorKey: "network_mode",
|
|
},
|
|
{
|
|
header: "Networks",
|
|
accessorKey: "networks",
|
|
},
|
|
]}
|
|
/>
|
|
</Section>
|
|
)}
|
|
|
|
<DockerLabelsSection labels={list_container?.labels} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
type ContainerTabsView = "Log" | "Inspect" | "Terminal";
|
|
|
|
const ContainerTabs = ({
|
|
server,
|
|
container,
|
|
state,
|
|
}: {
|
|
server: string;
|
|
container: string;
|
|
state: Types.ContainerStateStatusEnum;
|
|
}) => {
|
|
const [_view, setView] = useLocalStorage<ContainerTabsView>(
|
|
`server-${server}-${container}-tabs-v1`,
|
|
"Log"
|
|
);
|
|
const { specificLogs, specificInspect, specificTerminal } = usePermissions({
|
|
type: "Server",
|
|
id: server,
|
|
});
|
|
const container_terminals_disabled =
|
|
useServer(server)?.info.container_terminals_disabled ?? true;
|
|
const logDisabled =
|
|
!specificLogs || state === Types.ContainerStateStatusEnum.Empty;
|
|
const inspectDisabled =
|
|
!specificInspect || state === Types.ContainerStateStatusEnum.Empty;
|
|
const terminalDisabled =
|
|
!specificTerminal ||
|
|
container_terminals_disabled ||
|
|
state !== Types.ContainerStateStatusEnum.Running;
|
|
const view =
|
|
(inspectDisabled && _view === "Inspect") ||
|
|
(terminalDisabled && _view === "Terminal")
|
|
? "Log"
|
|
: _view;
|
|
|
|
const tabsNoContent = useMemo<TabNoContent<ContainerTabsView>[]>(
|
|
() => [
|
|
{
|
|
value: "Log",
|
|
hidden: !specificLogs,
|
|
disabled: logDisabled,
|
|
},
|
|
{
|
|
value: "Inspect",
|
|
hidden: !specificInspect,
|
|
disabled: inspectDisabled,
|
|
},
|
|
{
|
|
value: "Terminal",
|
|
hidden: !specificTerminal,
|
|
disabled: terminalDisabled,
|
|
},
|
|
],
|
|
[
|
|
specificLogs,
|
|
logDisabled,
|
|
specificInspect,
|
|
inspectDisabled,
|
|
specificTerminal,
|
|
terminalDisabled,
|
|
]
|
|
);
|
|
|
|
const Selector = (
|
|
<MobileFriendlyTabsSelector
|
|
tabs={tabsNoContent}
|
|
value={view}
|
|
onValueChange={setView as any}
|
|
tabsTriggerClassname="w-[110px]"
|
|
/>
|
|
);
|
|
|
|
const terminalQuery = useMemo(
|
|
() =>
|
|
({
|
|
type: "container",
|
|
query: {
|
|
server,
|
|
container,
|
|
// This is handled inside ContainerTerminal
|
|
shell: "",
|
|
},
|
|
}) as ConnectExecQuery,
|
|
[server, container]
|
|
);
|
|
|
|
switch (view) {
|
|
case "Log":
|
|
return (
|
|
<ContainerLogs
|
|
id={server}
|
|
container_name={container}
|
|
titleOther={Selector}
|
|
disabled={logDisabled}
|
|
/>
|
|
);
|
|
case "Inspect":
|
|
return (
|
|
<ContainerInspect
|
|
id={server}
|
|
container={container}
|
|
titleOther={Selector}
|
|
/>
|
|
);
|
|
case "Terminal":
|
|
return <ContainerTerminal query={terminalQuery} titleOther={Selector} />;
|
|
}
|
|
};
|
|
|
|
const AttachedResource = ({
|
|
id,
|
|
container,
|
|
}: {
|
|
id: string;
|
|
container: string;
|
|
}) => {
|
|
const { data: attached, isPending } = useRead(
|
|
"GetResourceMatchingContainer",
|
|
{ server: id, container },
|
|
{ refetchInterval: 10_000 }
|
|
);
|
|
|
|
if (isPending) {
|
|
return <Loader2 className="w-4 h-4 animate-spin" />;
|
|
}
|
|
|
|
if (!attached || !attached.resource) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
|
|
|
<ResourceLink
|
|
type={attached.resource.type as UsableResource}
|
|
id={attached.resource.id}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
const NewDeployment = ({ id, name }: { id: string; name: string }) => {
|
|
const { data: attached, isPending } = useRead(
|
|
"GetResourceMatchingContainer",
|
|
{ server: id, container: name }
|
|
);
|
|
|
|
if (isPending) {
|
|
return <Loader2 className="w-4 h-4 animate-spin" />;
|
|
}
|
|
|
|
if (!attached) {
|
|
return null;
|
|
}
|
|
|
|
if (!attached?.resource) {
|
|
return <NewDeploymentInner name={name} server_id={id} />;
|
|
}
|
|
};
|
|
|
|
const NewDeploymentInner = ({
|
|
server_id,
|
|
name,
|
|
}: {
|
|
name: string;
|
|
server_id: string;
|
|
}) => {
|
|
const nav = useNavigate();
|
|
const { mutateAsync, isPending } = useWrite("CreateDeploymentFromContainer");
|
|
return (
|
|
<ConfirmButton
|
|
title="New Deployment"
|
|
icon={<PlusCircle className="w-4 h-4" />}
|
|
onClick={async () => {
|
|
const id = (await mutateAsync({ name, server: server_id }))._id?.$oid!;
|
|
nav(`/deployments/${id}`);
|
|
}}
|
|
loading={isPending}
|
|
/>
|
|
);
|
|
};
|