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

Reply via email to