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 e60eba696e5094480a67b99307f2a21973885d09 Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Fri Nov 18 19:43:10 2022 -0500 Templates and Kamelets project list --- .../apache/camel/karavan/api/TemplateResource.java | 61 ---- .../main/webui/src/projects/ProjectsTableRow.tsx | 363 +++++++++++++++++++++ 2 files changed, 363 insertions(+), 61 deletions(-) diff --git a/karavan-app/src/main/java/org/apache/camel/karavan/api/TemplateResource.java b/karavan-app/src/main/java/org/apache/camel/karavan/api/TemplateResource.java deleted file mode 100644 index 2be7c7e..0000000 --- a/karavan-app/src/main/java/org/apache/camel/karavan/api/TemplateResource.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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. - */ -package org.apache.camel.karavan.api; - -import org.apache.camel.karavan.model.Project; -import org.apache.camel.karavan.model.ProjectFile; -import org.apache.camel.karavan.service.InfinispanService; - -import javax.inject.Inject; -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import java.util.List; - -@Path("/api/template") -public class TemplateResource { - - @Inject - InfinispanService infinispanService; - - @GET - @Produces(MediaType.APPLICATION_JSON) - public Project get() throws Exception { - return infinispanService.getProject(Project.NAME_TEMPLATES); - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/files") - public List<ProjectFile> getFiles() throws Exception { - return infinispanService.getProjectFiles(Project.NAME_TEMPLATES); - } - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public ProjectFile save(ProjectFile file) throws Exception { - if (file.getProjectId().equalsIgnoreCase(Project.NAME_TEMPLATES)){ - infinispanService.saveProjectFile(file); - } - return file; - } -} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx b/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx new file mode 100644 index 0000000..a3be55d --- /dev/null +++ b/karavan-app/src/main/webui/src/projects/ProjectsTableRow.tsx @@ -0,0 +1,363 @@ +import React from 'react'; +import { + Toolbar, + ToolbarContent, + ToolbarItem, + TextInput, + PageSection, + TextContent, + Text, + Button, + Modal, + FormGroup, + ModalVariant, + Form, + Badge, + Tooltip, + Bullseye, + EmptyState, + EmptyStateVariant, + EmptyStateIcon, + Title, + OverflowMenu, + OverflowMenuContent, + OverflowMenuGroup, + OverflowMenuItem, + Flex, FlexItem, Radio, Spinner +} from '@patternfly/react-core'; +import '../designer/karavan.css'; +import {MainToolbar} from "../MainToolbar"; +import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon'; +import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon'; +import {DeploymentStatus, Project} from "./ProjectModels"; +import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; +import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import CopyIcon from "@patternfly/react-icons/dist/esm/icons/copy-icon"; +import {CamelUi} from "../designer/utils/CamelUi"; +import {KaravanApi} from "../api/KaravanApi"; +import {QuarkusIcon, SpringIcon} from "../designer/utils/KaravanIcons"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; + +interface Props { + config: any, + onSelect: (project: Project) => void + toast: (title: string, text: string, variant: 'success' | 'danger' | 'warning' | 'info' | 'default') => void +} + +interface State { + projects: Project[], + deploymentStatuses: DeploymentStatus[], + isCreateModalOpen: boolean, + isDeleteModalOpen: boolean, + isCopy: boolean, + loading: boolean, + projectToCopy?: Project, + projectToDelete?: Project, + filter: string, + name: string, + description: string, + projectId: string, + runtime: string, +} + +export class ProjectsPage extends React.Component<Props, State> { + + public state: State = { + projects: [], + deploymentStatuses: [], + isCreateModalOpen: false, + isDeleteModalOpen: false, + isCopy: false, + loading: true, + filter: '', + name: '', + description: '', + projectId: '', + runtime: this.props.config.runtime + }; + interval: any; + + componentDidMount() { + this.interval = setInterval(() => this.onGetProjects(), 1300); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + onProjectDelete = (project: Project) => { + this.setState({isDeleteModalOpen: true, projectToDelete: project}) + }; + + + deleteProject = () => { + if (this.state.projectToDelete) + KaravanApi.deleteProject(this.state.projectToDelete, res => { + if (res.status === 204) { + this.props.toast?.call(this, "Success", "Project deleted", "success"); + this.onGetProjects(); + } else { + this.props.toast?.call(this, "Error", res.statusText, "danger"); + } + }); + this.setState({isDeleteModalOpen: false}) + } + + onProjectCreate = (project: Project) => { + KaravanApi.postProject(project, res => { + console.log(res.status) + if (res.status === 200 || res.status === 201) { + this.props.toast?.call(this, "Success", "Project created", "success"); + } else { + this.props.toast?.call(this, "Error", res.status + ", " + res.statusText, "danger"); + } + }); + }; + + onGetProjects = () => { + this.setState({loading: true}); + KaravanApi.getProjects((projects: Project[]) => { + this.setState({projects: projects, loading: false}) + }); + KaravanApi.getDeploymentStatuses(this.props.config.environment, (statuses: DeploymentStatus[]) => { + this.setState({deploymentStatuses: statuses}); + }); + } + + tools = () => (<Toolbar id="toolbar-group-types"> + <ToolbarContent> + <ToolbarItem> + <Button variant="link" icon={<RefreshIcon/>} onClick={e => this.onGetProjects()}/> + </ToolbarItem> + <ToolbarItem> + <TextInput className="text-field" type="search" id="search" name="search" + autoComplete="off" placeholder="Search by name" + value={this.state.filter} + onChange={e => this.setState({filter: e})}/> + </ToolbarItem> + <ToolbarItem> + <Button icon={<PlusIcon/>} onClick={e => this.setState({isCreateModalOpen: true, isCopy: false})}>Create</Button> + </ToolbarItem> + </ToolbarContent> + </Toolbar>); + + title = () => (<TextContent> + <Text component="h2">Projects</Text> + </TextContent>); + + closeModal = () => { + this.setState({isCreateModalOpen: false, isCopy: false, name: this.props.config.groupId, description: '', projectId: '', runtime: this.props.config.runtime}); + this.onGetProjects(); + } + + saveAndCloseCreateModal = () => { + const {name, description, projectId, runtime} = this.state; + const p = new Project(projectId, name, description, runtime, ''); + this.onProjectCreate(p); + this.setState({isCreateModalOpen: false, isCopy: false, name: this.props.config.groupId, description: '', projectId: ''}); + } + + onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => { + if (event.key === 'Enter' && this.state.name !== undefined && this.state.description !== undefined && this.state.projectId !== undefined) { + this.saveAndCloseCreateModal(); + } + } + + createModalForm() { + const {isCopy, projectToCopy, projectId, name, isCreateModalOpen, description, runtime} = this.state; + const {runtimes} = this.props.config; + const isReady = projectId && name && description && !['templates', 'kamelets'].includes(projectId); + return ( + <Modal + title={!isCopy ? "Create new project" : "Copy project from " + projectToCopy?.projectId} + variant={ModalVariant.small} + isOpen={isCreateModalOpen} + onClose={this.closeModal} + onKeyDown={this.onKeyDown} + actions={[ + <Button key="confirm" variant="primary" isDisabled={!isReady} onClick={this.saveAndCloseCreateModal}>Save</Button>, + <Button key="cancel" variant="secondary" onClick={this.closeModal}>Cancel</Button> + ]} + className="new-project" + > + <Form isHorizontal={true} autoComplete="off"> + <FormGroup label="Name" fieldId="name" isRequired> + <TextInput className="text-field" type="text" id="name" name="name" + value={name} + onChange={e => this.setState({name: e})}/> + </FormGroup> + <FormGroup label="Description" fieldId="description" isRequired> + <TextInput className="text-field" type="text" id="description" name="description" + value={description} + onChange={e => this.setState({description: e})}/> + </FormGroup> + <FormGroup label="Project ID" fieldId="projectId" isRequired helperText="Unique project name"> + <TextInput className="text-field" type="text" id="projectId" name="projectId" + value={projectId} + onFocus={e => this.setState({projectId: projectId === '' ? CamelUi.nameFromTitle(name) : projectId})} + onChange={e => this.setState({projectId: CamelUi.nameFromTitle(e)})}/> + </FormGroup> + <FormGroup label="Runtime" fieldId="runtime" isRequired> + {runtimes?.map((r: string) => ( + <Radio key={r} id={r} name={r} className="radio" + isChecked={r === runtime} + onChange={checked => { + if (checked) this.setState({runtime: r}) + }} + body={ + <div className="runtime-radio"> + {runtime === 'quarkus' ? QuarkusIcon() : SpringIcon()} + <div className="runtime-label">{CamelUtil.capitalizeName(r)}</div> + </div>} + /> + ))} + </FormGroup> + </Form> + </Modal> + ) + } + + deleteModalForm() { + return ( + <Modal + title="Confirmation" + variant={ModalVariant.small} + isOpen={this.state.isDeleteModalOpen} + onClose={() => this.setState({isDeleteModalOpen: false})} + actions={[ + <Button key="confirm" variant="primary" onClick={e => this.deleteProject()}>Delete</Button>, + <Button key="cancel" variant="link" + onClick={e => this.setState({isDeleteModalOpen: false})}>Cancel</Button> + ]} + onEscapePress={e => this.setState({isDeleteModalOpen: false})}> + <div>{"Are you sure you want to delete the project " + this.state.projectToDelete?.projectId + "?"}</div> + </Modal> + ) + } + + getEnvironments(): string [] { + return this.props.config.environments && Array.isArray(this.props.config.environments) ? Array.from(this.props.config.environments) : []; + } + + getDeploymentByEnvironments(name: string): [string, DeploymentStatus | undefined] [] { + const deps = this.state.deploymentStatuses; + return this.getEnvironments().map(e => { + const env: string = e as string; + const dep = deps.find(d => d.name === name && d.env === env); + return [env, dep]; + }); + } + + getEmptyState() { + const {loading} = this.state; + 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> + ) + } + + isBuildIn(project: Project): boolean { + return ['kamelets', 'templates'].includes(project.projectId); + } + + getProjectsTable() { + const {projects, filter} = this.state; + const projs = projects.filter(p => p.name.toLowerCase().includes(filter) || p.description.toLowerCase().includes(filter)); + return ( + <TableComposable aria-label="Projects" variant={"compact"}> + <Thead> + <Tr> + <Th key='type'>Runtime</Th> + <Th key='projectId'>Project ID</Th> + <Th key='name'>Name</Th> + <Th key='description'>Description</Th> + <Th key='commit'>Commit</Th> + <Th key='deployment'>Environment</Th> + <Th key='action'></Th> + </Tr> + </Thead> + <Tbody> + {projs.map(project => ( + <Tr key={project.projectId}> + <Td modifier={"fitContent"}> + <Tooltip content={project.runtime} position={"left"}> + <Badge className="runtime-badge">{project.runtime.substring(0, 1).toUpperCase()}</Badge> + </Tooltip> + </Td> + <Td> + <Button style={{padding: '6px'}} variant={"link"} onClick={e => this.props.onSelect?.call(this, project)}> + {project.projectId} + </Button> + </Td> + <Td>{project.name}</Td> + <Td>{project.description}</Td> + <Td isActionCell> + <Tooltip content={project.lastCommit} position={"bottom"}> + <Badge>{project.lastCommit?.substr(0, 7)}</Badge> + </Tooltip> + </Td> + <Td noPadding style={{width: "180px"}}> + <Flex direction={{default: "row"}}> + {this.getDeploymentByEnvironments(project.projectId).map(value => ( + <FlexItem className="badge-flex-item" key={value[0]}> + <Badge className="badge" isRead={!value[1]}>{value[0]}</Badge> + </FlexItem> + ))} + </Flex> + </Td> + <Td isActionCell> + <OverflowMenu breakpoint="md"> + <OverflowMenuContent> + <OverflowMenuGroup groupType="button"> + <OverflowMenuItem> + <Tooltip content={"Copy project"} position={"bottom"}> + <Button variant={"plain"} icon={<CopyIcon/>} + onClick={e => this.setState({isCreateModalOpen: true, isCopy: true, projectToCopy: project})}></Button> + </Tooltip> + </OverflowMenuItem> + <OverflowMenuItem> + <Tooltip content={"Delete project"} position={"bottom"}> + <Button variant={"plain"} icon={<DeleteIcon/>} onClick={e => this.onProjectDelete(project)}></Button> + </Tooltip> + </OverflowMenuItem> + </OverflowMenuGroup> + </OverflowMenuContent> + </OverflowMenu> + </Td> + </Tr> + ))} + {projs.length === 0 && this.getEmptyState()} + </Tbody> + </TableComposable> + ) + } + + render() { + return ( + <PageSection className="kamelet-section projects-page" padding={{default: 'noPadding'}}> + <PageSection className="tools-section" padding={{default: 'noPadding'}}> + <MainToolbar title={this.title()} tools={this.tools()}/> + </PageSection> + <PageSection isFilled className="kamelets-page"> + {this.getProjectsTable()} + </PageSection> + {this.createModalForm()} + {this.deleteModalForm()} + </PageSection> + ) + } +} \ No newline at end of file