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/09/08 23:48:35 UTC

[camel-karavan] 02/05: Delete images for #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 72cece8c348bcce3f90bdb79ddd6bb6af0cabbb1
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Sep 8 18:37:20 2023 -0400

    Delete images for #817
---
 .../apache/camel/karavan/api/ImagesResource.java   |   3 +
 .../apache/camel/karavan/docker/DockerService.java |   5 +
 .../src/main/webui/src/api/KaravanApi.tsx          |  12 +
 .../main/webui/src/project/build/BuildPanel.tsx    | 297 +++++++++++++++++++++
 .../webui/src/project/build/ContainersPanel.tsx    | 112 ++++++++
 .../main/webui/src/project/build/ImagesPanel.tsx   | 222 +++++++++++++++
 6 files changed, 651 insertions(+)

diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
index fe1a9f7a..b4605110 100644
--- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
+++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ImagesResource.java
@@ -26,6 +26,7 @@ import org.apache.camel.karavan.infinispan.model.Project;
 import org.apache.camel.karavan.service.ConfigService;
 import org.apache.camel.karavan.service.ProjectService;
 import org.apache.camel.karavan.service.RegistryService;
+import org.jose4j.base64url.Base64;
 
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
@@ -73,6 +74,8 @@ public class ImagesResource {
     @Produces(MediaType.APPLICATION_JSON)
     @Path("/{imageName}")
     public Response deleteImage(@HeaderParam("username") String username, @PathParam("imageName") String imageName) {
+        imageName= new String(Base64.decode(imageName));
+        System.out.println(imageName);
         if (ConfigService.inKubernetes()) {
             return Response.ok().build();
         } else {
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 9d5ac75f..f15d5b4e 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
@@ -393,5 +393,10 @@ public class DockerService extends DockerServiceUtils {
     }
 
     public void deleteImage(String imageName) {
+        Optional<Image> image = getDockerClient().listImagesCmd().withShowAll(true).exec().stream()
+                .filter(i -> Arrays.stream(i.getRepoTags()).anyMatch(s -> Objects.equals(s, imageName))).findFirst();
+        if (image.isPresent()) {
+            getDockerClient().removeImageCmd(image.get().getId()).exec();
+        }
     }
 }
diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
index fd66d5cd..63573e24 100644
--- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx
@@ -528,6 +528,18 @@ export class KaravanApi {
         });
     }
 
+    static async deleteImage(imageName: string, after: () => void) {
+        instance.delete('/api/image/' + Buffer.from(imageName).toString('base64'))
+            .then(res => {
+                console.log(res.status)
+                if (res.status === 200) {
+                    after();
+                }
+            }).catch(err => {
+            console.log(err);
+        });
+    }
+
     static async getSecrets(after: (any: []) => void) {
         instance.get('/api/infrastructure/secrets')
             .then(res => {
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
new file mode 100644
index 00000000..752f496c
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/BuildPanel.tsx
@@ -0,0 +1,297 @@
+import React, {useState} from 'react';
+import {
+    Button,
+    DescriptionList,
+    DescriptionListTerm,
+    DescriptionListGroup,
+    DescriptionListDescription, Spinner, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal, Badge, CardBody, Card
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {KaravanApi} from "../../api/KaravanApi";
+import BuildIcon from "@patternfly/react-icons/dist/esm/icons/build-icon";
+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 ClockIcon from "@patternfly/react-icons/dist/esm/icons/clock-icon";
+import TagIcon from "@patternfly/react-icons/dist/esm/icons/tag-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
+import {useAppConfigStore, useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {ContainersPanel} from "./ContainersPanel";
+
+interface Props {
+    env: string,
+}
+
+export function BuildPanel (props: Props) {
+
+    const [config] = useAppConfigStore((state) => [state.config], shallow)
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+    const [containers, deployments, camels, pipelineStatuses] =
+        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
+    const [isPushing, setIsPushing] = useState<boolean>(false);
+    const [isBuilding, setIsBuilding] = useState<boolean>(false);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [deleteEntityType, setDeleteEntityType] = useState<'pod' | 'deployment' | 'build'>('pod');
+    const [deleteEntityName, setDeleteEntityName] = useState<string>();
+    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
+    const [tag, setTag] = useState<string>(new Date().toISOString().substring(0,19).replaceAll(':', '-'));
+
+    function deleteEntity(type: 'pod' | 'deployment' | 'build', name: string, environment: string) {
+        switch (type) {
+            case "deployment":
+                KaravanApi.deleteDeployment(environment, name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+            case "pod":
+                KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+            case "build":
+                KaravanApi.stopBuild(environment, name, (res: any) => {
+                    // if (Array.isArray(res) && Array.from(res).length > 0)
+                    // onRefresh();
+                });
+                break;
+        }
+    }
+
+    function build() {
+        setIsBuilding(true);
+        setShowLog(false,'none')
+        KaravanApi.buildProject(project, tag, res => {
+            if (res.status === 200 || res.status === 201) {
+                setIsBuilding(false);
+            } else {
+                // Todo notification
+            }
+        });
+    }
+
+    function buildButton(env: string) {
+        const status = pipelineStatuses.filter(p => p.projectId === project.projectId).at(0);
+        const isRunning = status?.result === 'Running';
+        return (<Tooltip content="Start build" position={"left"}>
+            <Button isLoading={isBuilding ? true : undefined}
+                    isDisabled={isBuilding || isRunning || isPushing}
+                    size="sm"
+                    variant="secondary"
+                    className="project-button"
+                    icon={!isBuilding ? <BuildIcon/> : <div></div>}
+                    onClick={e => build()}>
+                {isBuilding ? "..." : "Build"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function deleteDeploymentButton(env: string) {
+        return (<Tooltip content="Delete deployment" position={"left"}>
+            <Button size="sm" variant="secondary"
+                    className="project-button"
+                    icon={<DeleteIcon/>}
+                    onClick={e => {
+                        setShowDeleteConfirmation(true);
+                        setDeleteEntityType("deployment");
+                        setDeleteEntityEnv(env);
+                        setDeleteEntityName(project?.projectId);
+                    }}>
+                {"Delete"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function getReplicasPanel(env: string) {
+        const deploymentStatus = deployments.find(d => d.name === project?.projectId);
+        const ok = (deploymentStatus && deploymentStatus?.readyReplicas > 0
+            && (deploymentStatus.unavailableReplicas === 0 || deploymentStatus.unavailableReplicas === undefined || deploymentStatus.unavailableReplicas === null)
+            && deploymentStatus?.replicas === deploymentStatus?.readyReplicas)
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    {deploymentStatus && <LabelGroup numLabels={3}>
+                        <Tooltip content={"Ready Replicas / Replicas"} position={"left"}>
+                            <Label icon={ok ? <UpIcon/> : <DownIcon/>}
+                                   color={ok ? "green" : "grey"}>{"Replicas: " + deploymentStatus.readyReplicas + " / " + deploymentStatus.replicas}</Label>
+                        </Tooltip>
+                        {deploymentStatus.unavailableReplicas > 0 &&
+                            <Tooltip content={"Unavailable replicas"} position={"right"}>
+                                <Label icon={<DownIcon/>} color={"red"}>{deploymentStatus.unavailableReplicas}</Label>
+                            </Tooltip>
+                        }
+                    </LabelGroup>}
+                    {deploymentStatus === undefined && <Label icon={<DownIcon/>} color={"grey"}>No deployments</Label>}
+                </FlexItem>
+                <FlexItem>{env === "dev" && deleteDeploymentButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getPipelineState(env: string) {
+        const status = pipelineStatuses.filter(p => p.projectId === project.projectId).at(0);
+        const pipeline = status?.pipelineName;
+        const pipelineResult = status?.result;
+        let lastPipelineRunTime = 0;
+        if (status?.startTime) {
+            const start: Date = new Date(status.startTime);
+            const finish: Date = status.completionTime !== undefined && status.completionTime !== null ? new Date(status.completionTime) : new Date();
+            lastPipelineRunTime = Math.round((finish.getTime() - start.getTime()) / 1000);
+        }
+        const showTime = lastPipelineRunTime && lastPipelineRunTime > 0;
+        const isRunning = pipelineResult === 'Running';
+        const isFailed = pipelineResult === 'Failed';
+        const isSucceeded = pipelineResult === 'Succeeded';
+        const color = isSucceeded ? "green" : (isFailed ? "red" : (isRunning ? "blue" : "grey"))
+        const icon = isSucceeded ? <UpIcon className="not-spinner"/> : <DownIcon className="not-spinner"/>
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    <Tooltip content={pipelineResult} position={"right"}>
+                        <LabelGroup numLabels={2}>
+                            <Label icon={isRunning ? <Spinner diameter="16px" className="spinner"/> : icon}
+                                   color={color}>
+                                {pipeline
+                                    ? <Button className='labeled-button' variant="link" onClick={e =>
+                                        useLogStore.setState({showLog: true, type: 'build', podName: pipeline})
+                                    }>
+                                        {pipeline}
+                                    </Button>
+                                    : "No builder"}
+                                {isRunning && <Tooltip content={"Stop build"}>
+                                    <Button
+                                        icon={<DeleteIcon/>}
+                                        className="labeled-button"
+                                        variant="link" onClick={e => {
+                                        setShowDeleteConfirmation(true);
+                                        setDeleteEntityType("build");
+                                        setDeleteEntityEnv(env);
+                                        setDeleteEntityName(pipeline);
+                                    }}></Button>
+                                </Tooltip>}
+                            </Label>
+                            {pipeline !== undefined && showTime === true && lastPipelineRunTime !== undefined &&
+                                <Label icon={<ClockIcon className="not-spinner"/>}
+                                       color={color}>{lastPipelineRunTime + "s"}</Label>}
+                        </LabelGroup>
+                    </Tooltip>
+                </FlexItem>
+                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getBuildState(env: string) {
+        const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0);
+        const buildName = status?.containerName;
+        const state = status?.state;
+        let buildTime = 0;
+        if (status?.created) {
+            const start: Date = new Date(status.created);
+            const finish: Date = status.finished !== undefined && status.finished !== null ? new Date(status.finished) : new Date();
+            buildTime = Math.round((finish.getTime() - start.getTime()) / 1000);
+        }
+        const showTime = buildTime && buildTime > 0;
+        const isRunning = state === 'running';
+        const isExited = state === 'exited';
+        const color = isExited ? "grey" : (isRunning ? "blue" : "grey");
+        const icon = isExited ? <UpIcon className="not-spinner"/> : <DownIcon className="not-spinner"/>
+        return (
+            <Flex justifyContent={{default: "justifyContentSpaceBetween"}} alignItems={{default: "alignItemsCenter"}}>
+                <FlexItem>
+                    <LabelGroup numLabels={3}>
+                        <Label isEditable={!isRunning} onEditComplete={(_, v) => setTag(v)}
+                               icon={<TagIcon className="not-spinner"/>}
+                               color={color}>{tag}</Label>
+                        <Label icon={isRunning ? <Spinner diameter="16px" className="spinner"/> : icon}
+                               color={color}>
+                            {buildName
+                                ? <Button className='labeled-button' variant="link" onClick={e =>
+                                    useLogStore.setState({showLog: true, type: 'build', podName: buildName})
+                                }>
+                                    {buildName}
+                                </Button>
+                                : "No builder"}
+                            {status !== undefined && <Tooltip content={"Delete build"}>
+                                <Button
+                                    icon={<DeleteIcon/>}
+                                    className="labeled-button"
+                                    variant="link" onClick={e => {
+                                    setShowDeleteConfirmation(true);
+                                    setDeleteEntityType("build");
+                                    setDeleteEntityEnv(env);
+                                    setDeleteEntityName(buildName);
+                                }}></Button>
+                            </Tooltip>}
+                        </Label>
+                        {buildName !== undefined && showTime === true && buildTime !== undefined &&
+                            <Label icon={<ClockIcon className="not-spinner"/>}
+                                   color={color}>{buildTime + "s"}</Label>}
+                    </LabelGroup>
+                </FlexItem>
+                <FlexItem>{env === "dev" && buildButton(env)}</FlexItem>
+            </Flex>
+        )
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (deleteEntityEnv && deleteEntityName && deleteEntity) {
+                        deleteEntity(deleteEntityType, deleteEntityName, deleteEntityEnv);
+                        setShowDeleteConfirmation(false);
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete " + deleteEntityType + " " + deleteEntityName + "?"}</div>
+        </Modal>)
+    }
+
+    const env = props.env;
+    return (
+        <Card className="project-status">
+            <CardBody>
+                <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Environment</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            <Badge className="badge">{env}</Badge>
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Build container with tag</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            {getBuildState(env)}
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    {config.infrastructure === 'kubernetes' &&
+                        <DescriptionListGroup>
+                        <DescriptionListTerm>Deployment</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            {getReplicasPanel(env)}
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                    }
+                    <DescriptionListGroup>
+                        <DescriptionListTerm>Containers</DescriptionListTerm>
+                        <DescriptionListDescription>
+                            <ContainersPanel env={props.env}/>
+                        </DescriptionListDescription>
+                    </DescriptionListGroup>
+                </DescriptionList>
+            </CardBody>
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </Card>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
new file mode 100644
index 00000000..87a43f0f
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ContainersPanel.tsx
@@ -0,0 +1,112 @@
+import React, {useState} from 'react';
+import {
+    Button, Tooltip, Flex, FlexItem, LabelGroup, Label, Modal
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {KaravanApi} from "../../api/KaravanApi";
+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 DeleteIcon from "@patternfly/react-icons/dist/esm/icons/times-circle-icon";
+import {ContainerStatus} from "../../api/ProjectModels";
+import {useLogStore, useProjectStore, useStatusesStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+
+interface Props {
+    env: string,
+}
+
+export function ContainersPanel (props: Props) {
+
+    const [project] = useProjectStore((s) => [s.project], shallow);
+    const [setShowLog] = useLogStore((s) => [s.setShowLog], shallow);
+    const [containers, deployments, camels, pipelineStatuses] =
+        useStatusesStore((s) => [s.containers, s.deployments, s.camels, s.pipelineStatuses], shallow);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [deleteEntityName, setDeleteEntityName] = useState<string>();
+    const [deleteEntityEnv, setDeleteEntityEnv] = useState<string>();
+
+    function deleteContainer(name: string, environment: string) {
+        KaravanApi.deleteContainer(environment, 'project', name, (res: any) => {
+            // if (Array.isArray(res) && Array.from(res).length > 0)
+            // onRefresh();
+        });
+    }
+
+    function deleteButton(env: string) {
+        return (<Tooltip content="Delete container" position={"left"}>
+            <Button size="sm" variant="secondary"
+                    className="project-button"
+                    icon={<DeleteIcon/>}
+                    onClick={e => {
+                        setShowDeleteConfirmation(true);
+                        setDeleteEntityEnv(env);
+                        setDeleteEntityName(project?.projectId);
+                    }}>
+                {"Delete"}
+            </Button>
+        </Tooltip>)
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (deleteEntityEnv && deleteEntityName) {
+                        deleteContainer(deleteEntityName, deleteEntityEnv);
+                        setShowDeleteConfirmation(false);
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete container " + deleteEntityName + "?"}</div>
+        </Modal>)
+    }
+
+    const env = props.env;
+    const conts = containers.filter(d => d.projectId === project?.projectId && d.type === 'project');
+    return (
+        <Flex justifyContent={{default: "justifyContentSpaceBetween"}}
+              alignItems={{default: "alignItemsFlexStart"}}>
+            <FlexItem>
+                {conts.length === 0 && <Label icon={<DownIcon/>} color={"grey"}>No pods</Label>}
+                <LabelGroup numLabels={2} isVertical>
+                    {conts.map((pod: ContainerStatus) => {
+                            const ready = pod.state === 'running';
+                            return (
+                                <Tooltip key={pod.containerName} content={pod.state}>
+                                    <Label icon={ready ? <UpIcon/> : <DownIcon/>} color={ready ? "green" : "red"}>
+                                        <Button variant="link" className="labeled-button"
+                                                onClick={e => {
+                                                    setShowLog(true,'container', pod.containerName);
+                                                }}>
+                                            {pod.containerName}
+                                        </Button>
+                                        <Tooltip content={"Delete Container"}>
+                                            <Button icon={<DeleteIcon/>}
+                                                    className="labeled-button"
+                                                    variant="link"
+                                                    onClick={e => {
+                                                        setShowDeleteConfirmation(true);
+                                                        setDeleteEntityEnv(env);
+                                                        setDeleteEntityName(pod.containerName);
+                                                    }}></Button>
+                                        </Tooltip>
+                                    </Label>
+                                </Tooltip>
+                            )
+                        }
+                    )}
+                </LabelGroup>
+            </FlexItem>
+            <FlexItem>{env === "dev" && deleteButton(env)}</FlexItem>
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </Flex>
+    )
+}
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
new file mode 100644
index 00000000..9c829267
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/project/build/ImagesPanel.tsx
@@ -0,0 +1,222 @@
+import React, {useState} from 'react';
+import {
+    Button,
+    Tooltip,
+    Flex,
+    FlexItem,
+    Modal,
+    Panel,
+    PanelHeader,
+    TextContent,
+    Text,
+    TextVariants,
+    Bullseye, EmptyState, EmptyStateVariant, EmptyStateHeader, EmptyStateIcon, PageSection, Switch, TextInput
+} from '@patternfly/react-core';
+import '../../designer/karavan.css';
+import {useFilesStore, useProjectStore} from "../../api/ProjectStore";
+import {shallow} from "zustand/shallow";
+import {Table} from "@patternfly/react-table/deprecated";
+import {Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table";
+import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon";
+import SetIcon from "@patternfly/react-icons/dist/esm/icons/check-icon";
+import {KaravanApi} from "../../api/KaravanApi";
+import {ProjectService} from "../../api/ProjectService";
+import {ServicesYaml} from "../../api/ServiceModels";
+import CopyIcon from "@patternfly/react-icons/dist/esm/icons/copy-icon";
+import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon";
+import {EventBus} from "../../designer/utils/EventBus";
+
+export function ImagesPanel () {
+
+    const [project, images] = useProjectStore((s) => [s.project, s.images], shallow);
+    const [files] = useFilesStore((s) => [s.files], shallow);
+    const [showSetConfirmation, setShowSetConfirmation] = useState<boolean>(false);
+    const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false);
+    const [imageName, setImageName] = useState<string>();
+    const [commitChanges, setCommitChanges] = useState<boolean>(false);
+    const [commitMessage, setCommitMessage] = useState('');
+
+    function setProjectImage() {
+        if (imageName) {
+            KaravanApi.setProjectImage(project.projectId, imageName, commitChanges, commitMessage, (res: any) => {
+                ProjectService.refreshProjectData(project.projectId);
+            });
+        }
+    }
+
+    function getProjectImage(): string | undefined {
+        const file = files.filter(f => f.name === 'project-compose.yaml').at(0);
+        if (file) {
+            const dc = ServicesYaml.yamlToServices(file.code);
+            const dcs = dc.services.filter(s => s.container_name === project.projectId).at(0);
+            return dcs?.image;
+        }
+        return undefined;
+    }
+
+    function getSetConfirmation() {
+        const index = imageName?.lastIndexOf(":");
+        const name = imageName?.substring(0, index);
+        const tag = index ? imageName?.substring(index+1) : "";
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showSetConfirmation}
+            onClose={() => setShowSetConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (imageName) {
+                        setProjectImage();
+                        setShowSetConfirmation(false);
+                        setCommitChanges(false);
+                    }
+                }}>Set
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => {
+                            setShowSetConfirmation(false);
+                            setCommitChanges(false);
+                        }}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowSetConfirmation(false)}>
+            <Flex direction={{default:"column"}} justifyContent={{default:"justifyContentFlexStart"}}>
+                <FlexItem>
+                    <div>{"Set image for project " + project.projectId + ":"}</div>
+                    <div>{"Name: " + name}</div>
+                    <div>{"Tag: " + tag}</div>
+                </FlexItem>
+                <FlexItem>
+                    <Switch
+                        id="commit-switch"
+                        label="Commit changes"
+                        isChecked={commitChanges}
+                        onChange={(event, checked) => setCommitChanges(checked)}
+                        isReversed
+                    />
+                </FlexItem>
+                {commitChanges && <FlexItem>
+                    <TextInput value={commitMessage} type="text"
+                               onChange={(_, value) => setCommitMessage(value)}
+                               aria-label="commit message"/>
+                </FlexItem>}
+            </Flex>
+        </Modal>)
+    }
+
+    function getDeleteConfirmation() {
+        return (<Modal
+            className="modal-delete"
+            title="Confirmation"
+            isOpen={showDeleteConfirmation}
+            onClose={() => setShowDeleteConfirmation(false)}
+            actions={[
+                <Button key="confirm" variant="primary" onClick={e => {
+                    if (imageName) {
+                        KaravanApi.deleteImage(imageName, () => {
+                            EventBus.sendAlert("Image deleted", "Image " + imageName + " deleted", 'info');
+                            setShowDeleteConfirmation(false);
+                        });
+                    }
+                }}>Delete
+                </Button>,
+                <Button key="cancel" variant="link"
+                        onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button>
+            ]}
+            onEscapePress={e => setShowDeleteConfirmation(false)}>
+            <div>{"Delete image:"}</div>
+            <div>{imageName}</div>
+        </Modal>)
+    }
+
+    const projectImage = getProjectImage();
+    return (
+        <PageSection className="project-tab-panel project-images-panel" padding={{default: "padding"}}>
+            <Panel>
+                <PanelHeader>
+                    <Flex direction={{default: "row"}} justifyContent={{default:"justifyContentFlexStart"}}>
+                        <FlexItem>
+                            <TextContent>
+                                <Text component={TextVariants.h6}>Images</Text>
+                            </TextContent>
+                        </FlexItem>
+                        <FlexItem>
+
+                        </FlexItem>
+                    </Flex>
+                </PanelHeader>
+            </Panel>
+            <Table aria-label="Images" variant={"compact"} className={"table"}>
+                <Thead>
+                    <Tr>
+                        <Th key='status' width={10}></Th>
+                        <Th key='image' width={30}>Image</Th>
+                        <Th key='tag' width={10}>Tag</Th>
+                        <Th key='actions'></Th>
+                    </Tr>
+                </Thead>
+                <Tbody>
+                    {images.map(image => {
+                        const index = image.lastIndexOf(":");
+                        const name = image.substring(0, index);
+                        const tag = image.substring(index+1);
+                        return <Tr key={image}>
+                            <Td modifier={"fitContent"} >
+                                {image === projectImage ? <SetIcon/> : <div/>}
+                            </Td>
+                            <Td>
+                                {name}
+                            </Td>
+                            <Td>
+                                {tag}
+                            </Td>
+                            <Td modifier={"fitContent"} isActionCell>
+                                <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}}
+                                      spaceItems={{default: 'spaceItemsNone'}}>
+                                    <FlexItem>
+                                        <Tooltip content={"Delete image"} position={"bottom"}>
+                                            <Button variant={"plain"}
+                                                    icon={<DeleteIcon/>}
+                                                    isDisabled={image === projectImage}
+                                                    onClick={e => {
+                                                        setImageName(image);
+                                                        setShowDeleteConfirmation(true);
+                                                    }}>
+                                            </Button>
+                                        </Tooltip>
+                                    </FlexItem>
+                                    <FlexItem>
+                                        <Tooltip content="Set project image" position={"bottom"}>
+                                            <Button style={{padding: '0'}}
+                                                    variant={"plain"}
+                                                    isDisabled={image === projectImage}
+                                                    onClick={e => {
+                                                        setImageName(image);
+                                                        setCommitMessage(commitMessage === '' ? new Date().toLocaleString() : commitMessage);
+                                                        setShowSetConfirmation(true);
+                                                    }}>
+                                                <SetIcon/>
+                                            </Button>
+                                        </Tooltip>
+                                    </FlexItem>
+                                </Flex>
+                            </Td>
+                        </Tr>
+                    })}
+                    {images.length === 0 &&
+                        <Tr>
+                            <Td colSpan={8}>
+                                <Bullseye>
+                                    <EmptyState variant={EmptyStateVariant.sm}>
+                                        <EmptyStateHeader titleText="No results found" icon={<EmptyStateIcon icon={SearchIcon}/>} headingLevel="h2" />
+                                    </EmptyState>
+                                </Bullseye>
+                            </Td>
+                        </Tr>
+                    }
+                </Tbody>
+            </Table>
+            {showSetConfirmation && getSetConfirmation()}
+            {showDeleteConfirmation && getDeleteConfirmation()}
+        </PageSection>
+    )
+}