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
The following commit(s) were added to refs/heads/main by this push: new 275bc3a1 Added missed files 275bc3a1 is described below commit 275bc3a16ea941f232513c327e22ecfad30c4aa7 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Mon Nov 27 16:55:36 2023 -0500 Added missed files --- .../src/main/webui/src/project/ProjectPanel.tsx | 4 +- .../main/webui/src/project/builder/BuildPanel.tsx | 207 ++++++++++++++++++ .../main/webui/src/project/builder/ImagesPanel.tsx | 238 +++++++++++++++++++++ .../webui/src/project/builder/ProjectBuildTab.tsx | 32 +++ 4 files changed, 479 insertions(+), 2 deletions(-) diff --git a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx index 577b400c..d9b503b7 100644 --- a/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/project/ProjectPanel.tsx @@ -25,10 +25,10 @@ import {FilesTab} from "./files/FilesTab"; import {useAppConfigStore, useProjectStore} from "../api/ProjectStore"; import {DashboardTab} from "./dashboard/DashboardTab"; import {TraceTab} from "./trace/TraceTab"; -import {ProjectBuildTab} from "./build/ProjectBuildTab"; +import {ProjectBuildTab} from "./builder/ProjectBuildTab"; import {ProjectService} from "../api/ProjectService"; import {shallow} from "zustand/shallow"; -import {ImagesPanel} from "./build/ImagesPanel"; +import {ImagesPanel} from "./builder/ImagesPanel"; import {ProjectContainerTab} from "./container/ProjectContainerTab"; import {ProjectTopologyTab} from "./topology/ProjectTopologyTab"; diff --git a/karavan-web/karavan-app/src/main/webui/src/project/builder/BuildPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/builder/BuildPanel.tsx new file mode 100644 index 00000000..f1dedba6 --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/project/builder/BuildPanel.tsx @@ -0,0 +1,207 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, {useEffect, 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 {EventBus} from "../../designer/utils/EventBus"; +import {ProjectService} from "../../api/ProjectService"; + +export function BuildPanel () { + + 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] = + useStatusesStore((s) => [s.containers, s.deployments, s.camels], shallow); + const [isPushing, setIsPushing] = useState<boolean>(false); + const [isBuilding, setIsBuilding] = useState<boolean>(false); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false); + const [deleteEntityName, setDeleteEntityName] = useState<string>(); + const [tag, setTag] = useState<string>( + new Date().toISOString().substring(0,19).replaceAll(':', '').replaceAll('-', '') + ); + + function deleteEntity() { + const buildName = getBuildName(); + if (buildName) { + KaravanApi.manageContainer(config.environment, 'build', buildName, 'delete', res => { + EventBus.sendAlert("Container deleted", "Container " + buildName + " deleted", 'info') + setShowLog(false, 'container', undefined) + }); + } + } + + function build() { + setIsBuilding(true); + setShowLog(false,'none') + KaravanApi.buildProject(project, tag, res => { + if (res.status === 200 || res.status === 201) { + setIsBuilding(false); + } else { + console.log(res); + EventBus.sendAlert("Error", (res as any)?.response?.data, 'danger') + } + }); + } + + function buildButton() { + const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0); + const isRunning = status?.state === '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 getContainerStatus() { + return containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0); + } + + function getBuildName() { + const status = getContainerStatus(); + return status?.containerName; + } + + function getBuildState() { + const status = getContainerStatus(); + const buildName = getBuildName(); + 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 isFailed = state === 'failed'; + const color = (isRunning ? "blue" : (isFailed ? "red" : "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 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); + 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>{buildButton()}</FlexItem> + </Flex> + ) + } + + function getBuildTag() { + const status = containers.filter(c => c.projectId === project.projectId && c.type === 'build').at(0); + const state = status?.state; + const isRunning = state === 'running'; + const isExited = state === 'exited'; + const color = isExited ? "grey" : (isRunning ? "blue" : "grey"); + return ( + <Label isEditable={!isRunning} onEditComplete={(_, v) => setTag(v)} + icon={<TagIcon className="not-spinner"/>} + color={color}>{tag}</Label> + ) + } + + function getDeleteConfirmation() { + return (<Modal + className="modal-delete" + title="Confirmation" + isOpen={showDeleteConfirmation} + onClose={() => setShowDeleteConfirmation(false)} + actions={[ + <Button key="confirm" variant="primary" onClick={e => { + if (deleteEntityName && deleteEntity) { + deleteEntity(); + setShowDeleteConfirmation(false); + } + }}>Delete + </Button>, + <Button key="cancel" variant="link" + onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button> + ]} + onEscapePress={e => setShowDeleteConfirmation(false)}> + <div>{"Delete build " + deleteEntityName + "?"}</div> + </Modal>) + } + + return ( + <Card className="project-status"> + <CardBody> + <DescriptionList isHorizontal horizontalTermWidthModifier={{default: '20ch'}}> + <DescriptionListGroup> + <DescriptionListTerm>Tag</DescriptionListTerm> + <DescriptionListDescription> + {getBuildTag()} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Build container</DescriptionListTerm> + <DescriptionListDescription> + {getBuildState()} + </DescriptionListDescription> + </DescriptionListGroup> + </DescriptionList> + </CardBody> + {showDeleteConfirmation && getDeleteConfirmation()} + </Card> + ) +} diff --git a/karavan-web/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx b/karavan-web/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx new file mode 100644 index 00000000..22c22a93 --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/project/builder/ImagesPanel.tsx @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 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 === 'docker-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={20}>Image</Th> + <Th key='tag' width={10}>Tag</Th> + <Th key='actions' width={10}></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> + ) +} diff --git a/karavan-web/karavan-app/src/main/webui/src/project/builder/ProjectBuildTab.tsx b/karavan-web/karavan-app/src/main/webui/src/project/builder/ProjectBuildTab.tsx new file mode 100644 index 00000000..31e6b19a --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/project/builder/ProjectBuildTab.tsx @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import '../../designer/karavan.css'; +import {BuildPanel} from "./BuildPanel"; +import {PageSection} from "@patternfly/react-core"; + +export function ProjectBuildTab () { + + return ( + <PageSection className="project-tab-panel project-build-panel" padding={{default: "padding"}}> + <div> + <BuildPanel/> + </div> + </PageSection> + ) +}