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>
+ )
+}