You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2023/07/24 19:57:46 UTC

[camel-karavan] 01/02: List of Docker containers #817

This is an automated email from the ASF dual-hosted git repository.

marat pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit 0050ccf796aa087fc6912947f2b896415052d2fa
Author: Marat Gubaidullin <ma...@gmail.com>
AuthorDate: Sun Jul 23 18:35:59 2023 -0400

    List of Docker containers #817
---
 .../camel/karavan/docker/DockerEventListener.java  |   3 +-
 .../apache/camel/karavan/docker/DockerService.java |   8 +-
 .../karavan-app/src/main/webui/src/Main.tsx        |   2 +
 .../src/main/webui/src/api/ProjectModels.ts        |   1 +
 .../webui/src/containers/ContainerTableRow.tsx     |  94 ++++++++++++
 .../main/webui/src/containers/ContainersPage.tsx   | 160 +++++++++++++++++++++
 .../src/main/webui/src/dashboard/DashboardPage.tsx |  15 +-
 .../main/webui/src/projects/ProjectsTableRow.tsx   |  17 ++-
 .../src/main/webui/src/services/ServicesPage.tsx   |   2 +-
 .../karavan/infinispan/model/ContainerStatus.java  |   4 +-
 10 files changed, 282 insertions(+), 24 deletions(-)

diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventListener.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventListener.java
index 8d4c9cc3..b055716d 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventListener.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerEventListener.java
@@ -96,13 +96,14 @@ public class DockerEventListener implements ResultCallback<Event> {
         String created = Instant.ofEpochSecond(container.getCreated()).toString();
         ContainerStatus ci = infinispanService.getContainerStatus(name, environment, name);
         if (ci == null) {
-            ci = ContainerStatus.createWithId(name, environment, container.getId(), ports, type, lc, created);
+            ci = ContainerStatus.createWithId(name, environment, container.getId(), container.getImage(), ports, type, lc, created);
         } else {
             ci.setContainerId(container.getId());
             ci.setPorts(ports);
             ci.setType(type);
             ci.setLifeCycle(lc);
             ci.setCreated(created);
+            ci.setImage(container.getImage());
         }
         infinispanService.saveContainerStatus(ci);
     }
diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
index 9f1f4198..27d5a6b3 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java
@@ -145,13 +145,7 @@ public class DockerService {
 
     public void collectContainersStatuses() {
         getDockerClient().listContainersCmd().exec().forEach(container -> {
-            String name = container.getNames()[0].replace("/", "");
-            if (!Objects.equals(name, INFINISPAN_CONTAINER_NAME)) {
-                dockerEventListener.onCreateContainer(container, new Event(container.getStatus(), container.getId(), container.getImage(), container.getCreated()));
-            }
-            if (Objects.equals(container.getLabels().get(LABEL_TYPE), ContainerStatus.CType.devmode.name())) {
-                dockerEventListener.onCreateContainer(container, new Event(container.getStatus(), container.getId(), container.getImage(), container.getCreated()));
-            }
+            dockerEventListener.onCreateContainer(container, new Event(container.getStatus(), container.getId(), container.getImage(), container.getCreated()));
         });
     }
 
diff --git a/karavan-web/karavan-app/src/main/webui/src/Main.tsx b/karavan-web/karavan-app/src/main/webui/src/Main.tsx
index 9e36ffa3..8adfe79d 100644
--- a/karavan-web/karavan-app/src/main/webui/src/Main.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/Main.tsx
@@ -24,6 +24,7 @@ import ServicesIcon from "@patternfly/react-icons/dist/js/icons/services-icon";
 import ComponentsIcon from "@patternfly/react-icons/dist/js/icons/module-icon";
 import {MainLogin} from "./MainLogin";
 import {DashboardPage} from "./dashboard/DashboardPage";
+import {ContainersPage} from "./containers/ContainersPage";
 import {ProjectEventBus} from "./api/ProjectEventBus";
 import {AppConfig, ContainerStatus, Project, ToastMessage} from "./api/ProjectModels";
 import {ProjectPage} from "./project/ProjectPage";
@@ -216,6 +217,7 @@ export const Main = () => {
                         {pageId === 'projects' && <ProjectsPage key={request}/>}
                         {pageId === 'project' && <ProjectPage key="projects"/>}
                         {pageId === 'services' && <ServicesPage key="services"/>}
+                        {pageId === 'containers' && <ContainersPage key="containers"/>}
                         {pageId === 'knowledgebase' && <KnowledgebasePage dark={false}/>}
                     </FlexItem>
                 </Flex>
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
index 73be3256..fe0fb19e 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
+++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts
@@ -69,6 +69,7 @@ export class ServiceStatus {
 
 export class ContainerStatus {
     containerName: string = '';
+    containerId: string = '';
     lifeCycle: string = '';
     deployment: string = '';
     projectId: string = '';
diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx
new file mode 100644
index 00000000..29241c44
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx
@@ -0,0 +1,94 @@
+import React, {useState} from 'react';
+import {
+    Button,
+    Tooltip,
+    Flex, FlexItem, Label, Badge
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import {ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table";
+import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon";
+import PlayIcon from "@patternfly/react-icons/dist/esm/icons/play-icon";
+import {ContainerStatus} from "../api/ProjectModels";
+
+interface Props {
+    index: number
+    container: ContainerStatus
+}
+
+export const ContainerTableRow = (props: Props) => {
+
+    const [isExpanded, setIsExpanded] = useState<boolean>(false);
+    const [running, setRunning] = useState<boolean>(false);
+
+    const container = props.container;
+    const env = container.env;
+    const ports = container.ports;
+    const icon = running ? <StopIcon/> : <PlayIcon/>;
+    const tooltip = running ? "Stop container" : "Start container";
+    const color = container.lifeCycle === 'ready' ? "green" : "grey";
+    return (
+        <Tbody isExpanded={isExpanded}>
+            <Tr key={container.containerName}>
+                <Td expand={
+                    container.containerName
+                        ? {
+                            rowIndex: props.index,
+                            isExpanded: isExpanded,
+                            onToggle: () => setIsExpanded(!isExpanded),
+                            expandId: 'composable-expandable-example'
+                        }
+                        : undefined}
+                    modifier={"fitContent"}>
+                </Td>
+                <Td style={{verticalAlign: "middle"}} modifier={"fitContent"}>
+                    <Badge className="badge">{container.type}</Badge>
+                </Td>
+                <Td>
+                    <Label color={color}>{container.containerName}</Label>
+                </Td>
+                <Td>{container.image}</Td>
+                <Td>
+                    <Label color={color}>{container.cpuInfo}</Label>
+                </Td>
+                <Td>
+                    <Label color={color}>{container.memoryInfo}</Label>
+                </Td>
+                {/*<Td>{container.environment}</Td>*/}
+                <Td className="project-action-buttons">
+                    <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}
+                          spaceItems={{default: 'spaceItemsNone'}}>
+                        <FlexItem>
+                            <Tooltip content={tooltip} position={"bottom"}>
+                                <Button variant={"plain"} icon={icon} onClick={e => {
+                                    // setProject(project, "delete");
+                                }}></Button>
+                            </Tooltip>
+                        </FlexItem>
+                    </Flex>
+                </Td>
+            </Tr>
+            {<Tr isExpanded={isExpanded}>
+                <Td></Td>
+                <Td colSpan={2}>Container ID</Td>
+                <Td colSpan={2}>
+                    <ExpandableRowContent>
+                        <Flex direction={{default: "column"}} cellPadding={"0px"}>
+                            {container.containerId}
+                        </Flex>
+                    </ExpandableRowContent>
+                </Td>
+            </Tr>}
+            {ports !== undefined && ports.length > 0 && <Tr isExpanded={isExpanded}>
+                <Td></Td>
+                <Td colSpan={2}>Ports</Td>
+                <Td colSpan={2}>
+                    <ExpandableRowContent>
+                        <Flex direction={{default: "row"}} cellPadding={"0px"}>
+                            {ports.map(port => <FlexItem>{port}</FlexItem>)}
+                        </Flex>
+                    </ExpandableRowContent>
+                </Td>
+            </Tr>}
+        </Tbody>
+    )
+}
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
new file mode 100644
index 00000000..34d44ff7
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx
@@ -0,0 +1,160 @@
+import React, {useEffect, useState} from 'react';
+import {
+    Badge, Bullseye,
+    Button, EmptyState, EmptyStateIcon, EmptyStateVariant,
+    Flex,
+    FlexItem, HelperText, HelperTextItem, Label, LabelGroup,
+    PageSection, Spinner,
+    Text,
+    TextContent,
+    TextInput, Title, ToggleGroup, ToggleGroupItem,
+    Toolbar,
+    ToolbarContent,
+    ToolbarItem, Tooltip
+} from '@patternfly/react-core';
+import '../designer/karavan.css';
+import {CamelStatus, ContainerStatus, DeploymentStatus, Project, ServiceStatus} from "../api/ProjectModels";
+import {TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import {camelIcon, CamelUi} from "../designer/utils/CamelUi";
+import {KaravanApi} from "../api/KaravanApi";
+import Icon from "../Logo";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
+import RefreshIcon from "@patternfly/react-icons/dist/esm/icons/sync-alt-icon";
+import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
+import {MainToolbar} from "../designer/MainToolbar";
+import {useAppConfigStore, useProjectsStore, useStatusesStore} from "../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {Service} from "../api/ServiceModels";
+import {ServicesTableRow} from "../services/ServicesTableRow";
+import {ContainerTableRow} from "./ContainerTableRow";
+
+export const ContainersPage = () => {
+
+    const [config] = useAppConfigStore((state) => [state.config], shallow)
+    const [containers, setContainers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow);
+    const [filter, setFilter] = useState<string>('');
+    const [loading, setLoading] = useState<boolean>(true);
+    const [selectedEnv, setSelectedEnv] = useState<string[]>([config.environment]);
+
+    useEffect(() => {
+        const interval = setInterval(() => {
+            onGetProjects()
+        }, 2000);
+        return () => {
+            clearInterval(interval)
+        };
+    }, []);
+
+    function onGetProjects() {
+        KaravanApi.getConfiguration((config: any) => {
+            KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => {
+                setContainers(statuses);
+                setLoading(false);
+            });
+        });
+    }
+
+    function selectEnvironment(name: string, selected: boolean) {
+        if (selected && !selectedEnv.includes(name)) {
+            setSelectedEnv((state: string[]) => {
+                state.push(name);
+                return state;
+            })
+        } else if (!selected && selectedEnv.includes(name)) {
+            setSelectedEnv((state: string[]) => {
+                return state.filter(e => e !== name)
+            })
+        }
+    }
+
+    function tools() {
+        return (<Toolbar id="toolbar-group-types">
+            <ToolbarContent>
+                <ToolbarItem>
+                    <Button variant="link" icon={<RefreshIcon/>} onClick={e => onGetProjects()}/>
+                </ToolbarItem>
+                <ToolbarItem>
+                    <ToggleGroup aria-label="Default with single selectable">
+                        {config.environments.map(env => (
+                            <ToggleGroupItem key={env} text={env} buttonId={env} isSelected={selectedEnv.includes(env)} onChange={selected => selectEnvironment(env, selected)}/>
+                        ))}
+                    </ToggleGroup>
+                </ToolbarItem>
+                <ToolbarItem>
+                    <TextInput className="text-field" type="search" id="search" name="search"
+                               autoComplete="off" placeholder="Search by name"
+                               value={filter}
+                               onChange={e => setFilter(e)}/>
+                </ToolbarItem>
+            </ToolbarContent>
+        </Toolbar>);
+    }
+
+    function title() {
+        return (<TextContent>
+            <Text component="h2">Containers</Text>
+        </TextContent>);
+    }
+
+    function getSelectedEnvironments(): string [] {
+        return config.environments.filter(e => selectedEnv.includes(e));
+    }
+
+    function getContainerByEnvironments(name: string): [string, ContainerStatus | undefined] [] {
+        return selectedEnv.map(e => {
+            const env: string = e as string;
+            const container = containers.find(d => d.containerName === name && d.env === env);
+            return [env, container];
+        });
+    }
+
+    function getEmptyState() {
+        return (
+            <Tr>
+                <Td colSpan={8}>
+                    <Bullseye>
+                        {loading && <Spinner className="progress-stepper" isSVG diameter="80px" aria-label="Loading..."/>}
+                        {!loading &&
+                            <EmptyState variant={EmptyStateVariant.small}>
+                                <EmptyStateIcon icon={SearchIcon}/>
+                                <Title headingLevel="h2" size="lg">
+                                    No results found
+                                </Title>
+                            </EmptyState>
+                        }
+                    </Bullseye>
+                </Td>
+            </Tr>
+        )
+    }
+
+    const conts = containers.filter(d => d.containerName.toLowerCase().includes(filter));
+    return (
+        <PageSection className="kamelet-section dashboard-page" padding={{default: 'noPadding'}}>
+            <PageSection className="tools-section" padding={{default: 'noPadding'}}>
+                <MainToolbar title={title()} tools={tools()}/>
+            </PageSection>
+            <PageSection isFilled className="kamelets-page">
+                <TableComposable aria-label="Projects" variant={TableVariant.compact}>
+                    <Thead>
+                        <Tr>
+                            <Th />
+                            <Th key='type'>Type</Th>
+                            <Th key='name'>Name</Th>
+                            <Th key='image'>Image</Th>
+                            <Th key='cpuInfo'>CPU</Th>
+                            <Th key='memoryInfo'>Memory</Th>
+                            <Th key='action'></Th>
+                        </Tr>
+                    </Thead>
+                    {conts?.map((container: ContainerStatus, index: number) => (
+                        <ContainerTableRow key={container.containerName} index={index} container={container}/>
+                    ))}
+                    {conts?.length === 0 && getEmptyState()}
+                </TableComposable>
+            </PageSection>
+        </PageSection>
+    )
+
+}
\ No newline at end of file
diff --git a/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx b/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx
index 4f2cc9ca..b338b3bc 100644
--- a/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/dashboard/DashboardPage.tsx
@@ -63,6 +63,7 @@ export const DashboardPage = () => {
             selectedEnv.forEach(env => {
                 KaravanApi.getAllCamelStatuses(env, (statuses: CamelStatus[]) => {
                     setCamels(statuses);
+                    setLoading(false);
                     // setState((state) => {
                     //     statuses.forEach(newStatus => {
                     //         const index = state.camelStatuses.findIndex(s => s.projectId === newStatus.projectId && s.env === newStatus.env);
@@ -75,7 +76,6 @@ export const DashboardPage = () => {
                     // })
                 });
             });
-            setLoading(false);
         });
     }
 
@@ -107,7 +107,7 @@ export const DashboardPage = () => {
                 </ToolbarItem>
                 <ToolbarItem>
                     <TextInput className="text-field" type="search" id="search" name="search"
-                               autoComplete="off" placeholder="Search deployment by name"
+                               autoComplete="off" placeholder="Search by name"
                                value={filter}
                                onChange={e => setFilter(e)}/>
                 </ToolbarItem>
@@ -322,7 +322,7 @@ export const DashboardPage = () => {
                     {conts.map(container => (
                         <Tr key={container.containerName}>
                             <Td style={{verticalAlign: "middle"}} modifier={"fitContent"}>
-                                <Label variant={"outline"}>{container.type}</Label>
+                                <Badge className="badge">{container.type}</Badge>
                             </Td>
                             <Td style={{verticalAlign: "middle"}}>
                                 <Label color={container.lifeCycle === 'ready' ? "green" : 'grey'}>
@@ -337,8 +337,9 @@ export const DashboardPage = () => {
                             <Td>
                                 <Flex direction={{default: "column"}}>
                                     {container.ports.map(port => (
-                                        <FlexItem className="badge-flex-item" key={port}><Badge className="badge"
-                                                                                                    isRead={true}>{port}</Badge></FlexItem>
+                                        <FlexItem className="badge-flex-item" key={port}>
+                                            <Badge className="badge" isRead={true}>{port}</Badge>
+                                        </FlexItem>
                                     ))}
                                 </Flex>
                             </Td>
@@ -346,9 +347,9 @@ export const DashboardPage = () => {
                                 <Flex direction={{default: "column"}}>
                                     {getContainerByEnvironments(container.containerName).map(value => (
                                         <FlexItem className="badge-flex-item" key={value[0]}>
-                                            <Badge className={"badge"}>
+                                            <Label color={"green"}>
                                                 {value[1] ? value[1]?.env : ""}
-                                            </Badge>
+                                            </Label>
                                         </FlexItem>
                                     ))}
                                 </Flex>
diff --git a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx
index 43b38dbf..d2a7323a 100644
--- a/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx
@@ -3,7 +3,7 @@ import {
     Button,
     Badge,
     Tooltip,
-    Flex, FlexItem
+    Flex, FlexItem, Label
 } from '@patternfly/react-core';
 import '../designer/karavan.css';
 import { Td, Tr} from "@patternfly/react-table";
@@ -17,6 +17,8 @@ import {
 } from "../api/ProjectStore";
 import {ProjectEventBus} from "../api/ProjectEventBus";
 import {shallow} from "zustand/shallow";
+import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
+import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon";
 
 interface Props {
     project: Project
@@ -66,17 +68,20 @@ export const ProjectsTableRow = (props: Props) => {
             <Td>{project.description}</Td>
             <Td isActionCell>
                 <Tooltip content={project.lastCommit} position={"bottom"}>
-                    <Badge>{project.lastCommit?.substr(0, 7)}</Badge>
+                    <Badge className="badge">{project.lastCommit?.substr(0, 7)}</Badge>
                 </Tooltip>
             </Td>
             <Td noPadding style={{width: "180px"}}>
                 {!isBuildIn &&
                     <Flex direction={{default: "row"}}>
-                        {getStatusByEnvironments(project.projectId).map(value => (
-                            <FlexItem className="badge-flex-item" key={value[0]}>
-                                <Badge className="badge" isRead={!value[1]}>{value[0]}</Badge>
+                        {getStatusByEnvironments(project.projectId).map(value => {
+                            const active = value[1];
+                            const color = active ? "green" : "grey"
+                            const style = active ? {fontWeight: "bold"} : {}
+                            return <FlexItem className="badge-flex-item" key={value[0]}>
+                                <Label style={style} color={color} >{value[0]}</Label>
                             </FlexItem>
-                        ))}
+                        })}
                     </Flex>
                 }
             </Td>
diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
index 940723a0..54a101be 100644
--- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx
@@ -67,7 +67,7 @@ export const ServicesPage = () => {
 
     function title() {
         return <TextContent>
-            <Text component="h2">Services</Text>
+            <Text component="h2">Dev Services</Text>
         </TextContent>
     }
 
diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
index 66d983d8..de1fea4e 100644
--- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
+++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java
@@ -92,8 +92,8 @@ public class ContainerStatus {
         return new ContainerStatus(projectId, projectId, null, null, null, env, CType.devmode, null, null, null,  Lifecycle.init, false, false);
     }
 
-    public static ContainerStatus createWithId(String name, String env, String containerId, List<Integer> ports, CType type, Lifecycle lifeCycle, String created) {
-        return new ContainerStatus(name, name, containerId, null, ports, env, type,
+    public static ContainerStatus createWithId(String name, String env, String containerId, String image, List<Integer> ports, CType type, Lifecycle lifeCycle, String created) {
+        return new ContainerStatus(name, name, containerId, image, ports, env, type,
                 null, null, created,  lifeCycle, false, false);
     }