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 9df8d7da7e673f5a4abd6302f2cefd644eb1cf62 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Thu Feb 1 20:00:51 2024 -0500 Bean config wizard --- .../camel/karavan/api/ProjectFileResource.java | 13 ++- .../org/apache/camel/karavan/code/CodeService.java | 12 ++- .../camel/karavan/service/ProjectService.java | 9 ++ .../snippets/database-bean-template.camel.yaml | 22 ++++ .../snippets/messaging-bean-template.camel.yaml | 9 ++ .../src/main/webui/src/api/KaravanApi.tsx | 11 ++ .../src/main/webui/src/api/ProjectStore.ts | 13 +++ .../src/main/webui/src/designer/DesignerStore.ts | 2 +- .../src/main/webui/src/project/ProjectPanel.tsx | 30 +++--- .../main/webui/src/project/beans/BeanWizard.tsx | 118 +++++++++++++++++++++ 10 files changed, 223 insertions(+), 16 deletions(-) diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java index 77776f6c..41b78850 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/ProjectFileResource.java @@ -21,6 +21,7 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import org.apache.camel.karavan.code.CodeService; import org.apache.camel.karavan.infinispan.InfinispanService; +import org.apache.camel.karavan.infinispan.model.Project; import org.apache.camel.karavan.infinispan.model.ProjectFile; import org.apache.camel.karavan.validation.project.ProjectFileCreateValidator; @@ -46,13 +47,21 @@ public class ProjectFileResource { @GET @Produces(MediaType.APPLICATION_JSON) @Path("/{projectId}") - public List<ProjectFile> get(@HeaderParam("username") String username, - @PathParam("projectId") String projectId) throws Exception { + public List<ProjectFile> get(@PathParam("projectId") String projectId) throws Exception { return infinispanService.getProjectFiles(projectId).stream() .sorted(Comparator.comparing(ProjectFile::getName)) .collect(Collectors.toList()); } + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/templates/beans") + public List<ProjectFile> getBeanTemplates() throws Exception { + return codeService.getBeanTemplateNames().stream() + .map(s -> infinispanService.getProjectFile(Project.Type.templates.name(), s)) + .toList(); + } + @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java index e519b5a4..12644148 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/code/CodeService.java @@ -58,6 +58,7 @@ public class CodeService { private static final Logger LOGGER = Logger.getLogger(CodeService.class.getName()); public static final String APPLICATION_PROPERTIES_FILENAME = "application.properties"; public static final String BUILD_SCRIPT_FILENAME = "build.sh"; + public static final String BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml"; public static final String DEV_SERVICES_FILENAME = "devservices.yaml"; public static final String PROJECT_COMPOSE_FILENAME = "docker-compose.yaml"; public static final String MARKDOWN_EXTENSION = ".md"; @@ -86,6 +87,7 @@ public class CodeService { @Inject Vertx vertx; + List<String> beansTemplates = List.of("database", "messaging"); List<String> targets = List.of("openshift", "kubernetes", "docker"); List<String> interfaces = List.of("org.apache.camel.AggregationStrategy.java", "org.apache.camel.Processor.java"); @@ -185,6 +187,10 @@ public class CodeService { return null; } + public List<String> getBeanTemplateNames(){ + return beansTemplates.stream().map(name -> name + BEAN_TEMPLATE_SUFFIX_FILENAME).toList(); + } + public Map<String, String> getTemplates() { Map<String, String> result = new HashMap<>(); @@ -192,10 +198,14 @@ public class CodeService { files.addAll(targets.stream().map(target -> target + "-" + APPLICATION_PROPERTIES_FILENAME).toList()); files.addAll(targets.stream().map(target -> target + "-" + BUILD_SCRIPT_FILENAME).toList()); + files.addAll(getBeanTemplateNames()); + files.forEach(file -> { String templatePath = SNIPPETS_PATH + file; String templateText = getResourceFile(templatePath); - result.put(file, templateText); + if (templateText != null) { + result.put(file, templateText); + } }); result.put(PROJECT_COMPOSE_FILENAME, getResourceFile(SNIPPETS_PATH + PROJECT_COMPOSE_FILENAME)); diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java index 826990ec..aaebf8ae 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java @@ -386,6 +386,15 @@ public class ProjectService implements HealthCheck { infinispanService.saveProjectFile(file); }); commitAndPushProject(Project.Type.templates.name(), "Add default templates"); + } else { + LOGGER.info("Add new templates if any"); + codeService.getTemplates().forEach((name, value) -> { + ProjectFile f = infinispanService.getProjectFile(Project.Type.templates.name(), name); + if (f == null) { + ProjectFile file = new ProjectFile(name, value, Project.Type.templates.name(), Instant.now().toEpochMilli()); + infinispanService.saveProjectFile(file); + } + }); } } catch (Exception e) { LOGGER.error("Error during templates project creation", e); diff --git a/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml b/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml new file mode 100644 index 00000000..b2e8bc61 --- /dev/null +++ b/karavan-web/karavan-app/src/main/resources/snippets/database-bean-template.camel.yaml @@ -0,0 +1,22 @@ +- beans: + - name: PostgresDatabase + properties: + username: 'username' + password: 'password' + url: 'jdbc:postgresql://serverName:serverPort/databaseName' + driverClassName: org.postgresql.Driver + type: '#class:org.apache.commons.dbcp2.BasicDataSource' + - name: MySqlDatabase + properties: + username: 'username' + password: 'password' + url: 'jdbc:mysql://serverName:serverPort/databaseName' + driverClassName: com.mysql.cj.jdbc.Driver + type: '#class:org.apache.commons.dbcp2.BasicDataSource' + - name: OracleDatabase + properties: + username: 'username' + password: 'password' + url: 'jdbc:oracle:thin:@serverName:serverPort/databaseName' + driverClassName: oracle.jdbc.driver.OracleDriver + type: '#class:org.apache.commons.dbcp2.BasicDataSource' \ No newline at end of file diff --git a/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml b/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml new file mode 100644 index 00000000..ee05cbff --- /dev/null +++ b/karavan-web/karavan-app/src/main/resources/snippets/messaging-bean-template.camel.yaml @@ -0,0 +1,9 @@ +- beans: + - name: JMSArtemis + properties: + brokerURL: 'tcp://serverHost:serverPort' + type: '#class:org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory' + - name: AMQP + properties: + brokerURL: 'amqp://serverHost:serverPort' + type: "#class:org.apache.qpid.jms.JmsConnectionFactory" \ No newline at end of file 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 4cef25c0..0e963238 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 @@ -318,6 +318,17 @@ export class KaravanApi { }); } + static async getBeanTemplatesFiles( after: (files: ProjectFile []) => void) { + instance.get('/api/file/templates/beans') + .then(res => { + if (res.status === 200) { + after(res.data); + } + }).catch(err => { + console.log(err); + }); + } + static async getDevModePodStatus(projectId: string, after: (res: AxiosResponse<ContainerStatus>) => void) { instance.get('/api/devmode/container/' + projectId) .then(res => { diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts index d0078549..ccbb0399 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectStore.ts @@ -239,6 +239,19 @@ export const useFileStore = createWithEqualityFn<FileState>((set) => ({ }, }), shallow) + + +interface WizardState { + showWizard: boolean; + setShowWizard: (showWizard: boolean) => void; +} +export const useWizardStore = createWithEqualityFn<WizardState>((set) => ({ + showWizard: false, + setShowWizard: (showWizard: boolean) => { + set({showWizard: showWizard}) + }, +}), shallow) + interface DevModeState { podName?: string, status: "none" | "wip", diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts b/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts index 8a836e5f..ef95b8d4 100644 --- a/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts +++ b/karavan-web/karavan-app/src/main/webui/src/designer/DesignerStore.ts @@ -19,7 +19,7 @@ import {CamelElement, Integration} from "karavan-core/lib/model/IntegrationDefin import {DslPosition, EventBus} from "./utils/EventBus"; import {createWithEqualityFn} from "zustand/traditional"; import {shallow} from "zustand/shallow"; -import {RegistryBeanDefinition} from "karavan-core/src/core/model/CamelDefinition"; +import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition"; interface IntegrationState { integration: Integration; 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 e9c842af..52d14df8 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 @@ -15,14 +15,14 @@ * limitations under the License. */ -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import { Flex, - FlexItem, PageSection + FlexItem, Modal, ModalVariant, PageSection } from '@patternfly/react-core'; import '../designer/karavan.css'; import {FilesTab} from "./files/FilesTab"; -import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore} from "../api/ProjectStore"; +import {useAppConfigStore, useFilesStore, useFileStore, useProjectStore, useWizardStore} from "../api/ProjectStore"; import {DashboardTab} from "./dashboard/DashboardTab"; import {TraceTab} from "./trace/TraceTab"; import {ProjectBuildTab} from "./builder/ProjectBuildTab"; @@ -36,6 +36,7 @@ import {Buffer} from "buffer"; import {CreateFileModal} from "./files/CreateFileModal"; import {ProjectType} from "../api/ProjectModels"; import {ReadmeTab} from "./readme/ReadmeTab"; +import {BeanWizard} from "./beans/BeanWizard"; export function ProjectPanel() { @@ -43,6 +44,7 @@ export function ProjectPanel() { const [project, tab, setTab] = useProjectStore((s) => [s.project, s.tabIndex, s.setTabIndex], shallow); const [setFile] = useFileStore((s) => [s.setFile], shallow); const [files] = useFilesStore((s) => [s.files], shallow); + const [setShowWizard] = useWizardStore((s) => [s.setShowWizard], shallow) useEffect(() => { onRefresh(); @@ -70,21 +72,25 @@ export function ProjectPanel() { const isTopology = tab === 'topology'; const iFiles = files.map(f => new IntegrationFile(f.name, f.code)); - const codes = iFiles.map(f=>f.code).join(""); + const codes = iFiles.map(f => f.code).join(""); const key = Buffer.from(codes).toString('base64') return isTopology ? ( <> - <TopologyTab key={key} - hideToolbar={false} - files={files.map(f => new IntegrationFile(f.name, f.code))} - onClickAddRoute={() => setFile('create', undefined, 'routes')} - onClickAddREST={() => setFile('create', undefined, 'rest')} - onClickAddBean={() => setFile('create', undefined, 'beans')} - onSetFile={(fileName) => selectFile(fileName)} - /> + <TopologyTab key={key} + hideToolbar={false} + files={files.map(f => new IntegrationFile(f.name, f.code))} + onClickAddRoute={() => setFile('create', undefined, 'routes')} + onClickAddREST={() => setFile('create', undefined, 'rest')} + onClickAddBean={() => { + // setFile('create', undefined, 'beans'); + setShowWizard(true) + }} + onSetFile={(fileName) => selectFile(fileName)} + /> <CreateFileModal types={['INTEGRATION']} isKameletsProject={false}/> + <BeanWizard/> </> ) : (<PageSection padding={{default: 'noPadding'}} className="scrollable-out"> diff --git a/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx new file mode 100644 index 00000000..1adc5d0f --- /dev/null +++ b/karavan-web/karavan-app/src/main/webui/src/project/beans/BeanWizard.tsx @@ -0,0 +1,118 @@ +/* + * 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, useMemo, useState} from 'react'; +import { + Badge, + capitalize, + Flex, + Form, FormGroup, + Modal, + ModalVariant, + Radio, Text, TextInput, + Wizard, + WizardHeader, + WizardStep +} from '@patternfly/react-core'; +import {KaravanApi} from "../../api/KaravanApi"; +import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition"; +import {CodeUtils} from "../../util/CodeUtils"; +import {ProjectFile, ProjectType} from "../../api/ProjectModels"; +import {useWizardStore} from "../../api/ProjectStore"; +import {shallow} from "zustand/shallow"; +import {ProjectService} from "../../api/ProjectService"; + +const BEAN_TEMPLATE_SUFFIX_FILENAME = "-bean-template.camel.yaml"; + +export function BeanWizard() { + + const [showWizard, setShowWizard] = useWizardStore((s) => [s.showWizard, s.setShowWizard], shallow) + const [files, setFiles] = useState<ProjectFile[]>([]); + const [templateNames, setTemplateNames] = useState<string[]>([]); + const [templateName, setTemplateName] = useState<string>(''); + const [beanName, setBeanName] = useState<string>(''); + + useEffect(() => { + if (showWizard) { + KaravanApi.getBeanTemplatesFiles(files => { + const templateNames = files.map(file => file.name.replace(BEAN_TEMPLATE_SUFFIX_FILENAME, '')); + setFiles(prevState => { + return [...files]; + }); + setTemplateNames(prevState => { + return [...templateNames]; + }); + setTemplateName(''); + setBeanName(''); + }); + } + }, [showWizard]); + + + function getRegistryBeanDefinitions():RegistryBeanDefinition[] { + const fs = files + .filter(f => f.name === templateName.concat(BEAN_TEMPLATE_SUFFIX_FILENAME)); + return CodeUtils.getBeans(fs); + } + + const getBeans = useMemo(() => getRegistryBeanDefinitions(), [templateName]); + + return ( + <Modal title={"Bean"} onClose={_ => setShowWizard(false)} + variant={ModalVariant.medium} isOpen={showWizard} onEscapePress={() => setShowWizard(false)}> + <Wizard height={600} title="Bean configuration" onClose={() => setShowWizard(false)}> + <WizardStep name={"Type"} id="type" + footer={{ isNextDisabled: !templateNames.includes(templateName) }} + > + <Flex direction={{default:"column"}} gap={{default:'gapLg'}}> + {templateNames.map(n => <Radio id={n} label={capitalize(n)} name={n} isChecked={n === templateName} + onChange={_ => setTemplateName(n)} />)} + </Flex> + </WizardStep> + <WizardStep name={"Template"} id="template" + isDisabled={templateName.length == 0} + footer={{ isNextDisabled: !getBeans.map(b=> b.name).includes(beanName) }} + > + <Flex direction={{default:"column"}} gap={{default:'gapLg'}}> + {getBeans.map(b => <Radio id={b.name} label={b.name} name={b.name} isChecked={b.name === beanName} + onChange={_ => setBeanName(b.name)} />)} + </Flex> + </WizardStep> + <WizardStep name="Properties" id="properties" + isDisabled={templateName.length == 0 || beanName.length == 0} + footer={{ nextButtonText: 'Add bean' }} + > + <Form> + {getBeans.filter(b=> b.name === beanName).map(b => ( + <> + {Object.getOwnPropertyNames(b.properties).map(prop => ( + <FormGroup label={prop} fieldId={prop}> + <TextInput + value={b.properties[prop]} + id={prop} + aria-describedby={prop} + onChange={(_, value) => {}} + /> + </FormGroup> + ))} + </> + ))} + </Form> + </WizardStep> + </Wizard> + </Modal> + ) +} \ No newline at end of file