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 df8c086d Kamelets Definition UI #315 df8c086d is described below commit df8c086ddfc902704fa9323d4a7fec28d3b22854 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Sun Oct 1 16:49:23 2023 -0400 Kamelets Definition UI #315 --- karavan-core/src/core/api/CamelDefinitionYaml.ts | 21 +- .../src/core/model/IntegrationDefinition.ts | 46 +++-- karavan-core/test/isIntegration.spec.ts | 12 +- karavan-core/test/kamelet.spec.ts | 22 +- karavan-core/test/topology.spec.ts | 1 - .../public/example/postgresql-source.kamelet.yaml | 114 +++++++++++ karavan-designer/src/App.tsx | 6 +- karavan-designer/src/designer/KaravanDesigner.tsx | 17 +- .../src/designer/beans/BeanProperties.tsx | 7 +- .../designer/kamelet/KameletAnnotationsPanel.tsx | 133 +++++++++++++ .../kamelet/KameletDefinitionPropertyCard.tsx | 221 +++++++++++++++++++++ .../designer/kamelet/KameletDefinitionsPanel.tsx | 108 ++++++++++ .../src/designer/kamelet/KameletDesigner.tsx | 132 ++++++++++++ .../KameletProperties.tsx} | 9 +- karavan-designer/src/designer/kamelet/kamelet.css | 47 +++++ .../src/designer/route/DslProperties.tsx | 3 +- .../src/designer/utils/IntegrationHeader.tsx | 19 +- .../src/designer/utils/KaravanIcons.tsx | 45 +++-- 18 files changed, 885 insertions(+), 78 deletions(-) diff --git a/karavan-core/src/core/api/CamelDefinitionYaml.ts b/karavan-core/src/core/api/CamelDefinitionYaml.ts index 44feabdd..aa6f425b 100644 --- a/karavan-core/src/core/api/CamelDefinitionYaml.ts +++ b/karavan-core/src/core/api/CamelDefinitionYaml.ts @@ -237,25 +237,24 @@ export class CamelDefinitionYaml { return integration; }; - static yamlIsIntegration = (text: string): boolean => { + static yamlIsIntegration = (text: string): 'crd' | 'plain' | 'kamelet' | 'none' => { try { const fromYaml: any = yaml.load(text); const camelized: any = CamelUtil.camelizeObject(fromYaml); - if ( - camelized?.apiVersion && - camelized.apiVersion.startsWith('camel.apache.org') && - camelized.kind && - camelized.kind === 'Integration' - ) { - return true; + if (camelized?.apiVersion && camelized.apiVersion.startsWith('camel.apache.org') && camelized.kind) { + if (camelized.kind === 'Integration') { + return 'crd'; + } else if (camelized.kind === 'Kamelet') { + return 'kamelet'; + } } else if (Array.isArray(camelized)) { - return true; + return 'plain'; } else { - return false; + return 'none'; } } catch (e) { - return false; } + return 'none'; }; static flowsToCamelElements = (flows: any[]): any[] => { const rules: { [key: string]: (flow: any) => any } = { diff --git a/karavan-core/src/core/model/IntegrationDefinition.ts b/karavan-core/src/core/model/IntegrationDefinition.ts index c6c53d46..696e2da1 100644 --- a/karavan-core/src/core/model/IntegrationDefinition.ts +++ b/karavan-core/src/core/model/IntegrationDefinition.ts @@ -17,7 +17,7 @@ import { v4 as uuidv4 } from 'uuid'; import { RegistryBeanDefinition } from './CamelDefinition'; -export class KameletDefinitionProperty { +export class DefinitionProperty { title: string = ''; description: string = ''; type: 'string' | 'integer' | 'boolean' = 'string'; @@ -25,27 +25,26 @@ export class KameletDefinitionProperty { example?: any; format?: string; "x-descriptors"?: string[]; - properties: any = {}; - public constructor(init?: Partial<KameletDefinition>) { + public constructor(init?: Partial<DefinitionProperty>) { Object.assign(this, init); } } -export class KameletDefinition { +export class Definition { title: string = ''; description: string = ''; required: string[] = []; type: string = 'object'; properties: any = {}; - public constructor(init?: Partial<KameletDefinition>) { + public constructor(init?: Partial<Definition>) { Object.assign(this, init); } } export class Spec { - definition?: KameletDefinition; + definition?: Definition; types?: any; flows?: any[] = []; template?: any; @@ -56,30 +55,31 @@ export class Spec { } } -export class MetadataLabel { - "camel.apache.org/kamelet.type": "sink" | "source" | "action" +export class MetadataLabels { + "camel.apache.org/kamelet.type": "sink" | "source" | "action" = 'source' - public constructor(init?: Partial<MetadataLabel>) { + public constructor(init?: Partial<MetadataLabels>) { Object.assign(this, init); } } -export class MetadataAnnotation { - "camel.apache.org/catalog.version"?: string; - "camel.apache.org/kamelet.icon"?: string; - "camel.apache.org/provider"?: string; - "camel.apache.org/kamelet.group"?: string; - "camel.apache.org/kamelet.namespace"?: string; +export class MetadataAnnotations { + "camel.apache.org/kamelet.support.level:": string = 'Preview'; + "camel.apache.org/catalog.version": string = ''; + "camel.apache.org/kamelet.icon": string = ''; + "camel.apache.org/provider": string = ''; + "camel.apache.org/kamelet.group": string = ''; + "camel.apache.org/kamelet.namespace": string = ''; - public constructor(init?: Partial<MetadataAnnotation>) { + public constructor(init?: Partial<MetadataAnnotations>) { Object.assign(this, init); } } export class Metadata { name: string = ''; - annotations?: MetadataAnnotation; - labels?: MetadataLabel[]; + annotations?: MetadataAnnotations; + labels?: MetadataLabels; public constructor(init?: Partial<Metadata>) { Object.assign(this, init); @@ -98,10 +98,18 @@ export class Integration { } static createNew(name?: string, type: 'crd' | 'plain' | 'kamelet' = 'plain'): Integration { - return new Integration({ type: type, + const i = new Integration({ type: type, metadata: new Metadata({ name: name }), kind : type === 'kamelet' ? 'Kamelet' : 'Integration', spec: new Spec({ flows: [] }) }); + + if (type === 'kamelet') { + i.metadata.annotations = new MetadataAnnotations({}) + i.spec.definition = new Definition({}) + i.spec.types = {} + } + + return i; } } diff --git a/karavan-core/test/isIntegration.spec.ts b/karavan-core/test/isIntegration.spec.ts index a14ed140..61f3c218 100644 --- a/karavan-core/test/isIntegration.spec.ts +++ b/karavan-core/test/isIntegration.spec.ts @@ -27,13 +27,19 @@ describe('Is Integration', () => { it('Is not integration', () => { const yaml = fs.readFileSync('test/is-not-integration.yaml',{encoding:'utf8', flag:'r'}); const i = CamelDefinitionYaml.yamlIsIntegration(yaml); - expect(i).to.equal(false); + expect(i).to.equal('none'); }); - it('Is integration', () => { + it('Is integration CRD', () => { const yaml = fs.readFileSync('test/integration1.yaml',{encoding:'utf8', flag:'r'}); const i = CamelDefinitionYaml.yamlIsIntegration(yaml); - expect(i).to.equal(true); + expect(i).to.equal('crd'); + }); + + it('Is integration plain', () => { + const yaml = fs.readFileSync('test/plain1.yaml',{encoding:'utf8', flag:'r'}); + const i = CamelDefinitionYaml.yamlIsIntegration(yaml); + expect(i).to.equal('plain'); }); }); \ No newline at end of file diff --git a/karavan-core/test/kamelet.spec.ts b/karavan-core/test/kamelet.spec.ts index 691bf48e..65e94987 100644 --- a/karavan-core/test/kamelet.spec.ts +++ b/karavan-core/test/kamelet.spec.ts @@ -14,29 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import {expect} from 'chai'; import * as fs from 'fs'; import 'mocha'; import {CamelDefinitionYaml} from "../src/core/api/CamelDefinitionYaml"; -import { - ChoiceDefinition, - ExpressionDefinition, - FilterDefinition, FromDefinition, LogDefinition, - ToDefinition, - WhenDefinition, -} from '../src/core/model/CamelDefinition'; +import { FromDefinition, LogDefinition, } from '../src/core/model/CamelDefinition'; import { RouteDefinition} from "../src/core/model/CamelDefinition"; -import { Beans, Integration, MetadataAnnotation } from '../src/core/model/IntegrationDefinition'; -import { KameletMetadata } from '../lib/model/KameletModels'; -import { RegistryBeanDefinition } from '../lib/model/CamelDefinition'; +import { Beans, Definition, Integration } from '../src/core/model/IntegrationDefinition'; +import { RegistryBeanDefinition } from '../src/core/model/CamelDefinition'; +import { MetadataAnnotations } from '../src/core/model/IntegrationDefinition'; describe('Kamelet <=> YAML', () => { it('Yaml to Kamelet', () => { const yaml = fs.readFileSync('test/postgresql-source.kamelet.yaml',{encoding:'utf8', flag:'r'}); const i = CamelDefinitionYaml.yamlToIntegration("postgresql-source.kamelet.yaml", yaml); - console.log(i) - console.log(CamelDefinitionYaml.integrationToYaml(i)) + // console.log(i) }); it('Kamelet to YAML with beans', () => { @@ -50,7 +42,7 @@ describe('Kamelet <=> YAML', () => { b.beans.push(new RegistryBeanDefinition({name: "beanDS1", type: "String.class"})); b.beans.push(new RegistryBeanDefinition({name: "beanDS2", type: "String.class"})); i.spec.flows?.push(b); - const a = new MetadataAnnotation({"camel.apache.org/kamelet.group" : "hello world"}) + const a = new MetadataAnnotations({"camel.apache.org/kamelet.group" : "hello world"}) i.metadata.annotations = a }); @@ -60,6 +52,8 @@ describe('Kamelet <=> YAML', () => { const flow1 = new FromDefinition({uri: "direct1"}); flow1.steps?.push(new LogDefinition({logName: 'log11', message: "hello11"})); i.spec.flows?.push(new RouteDefinition({from:flow1})); + + console.log(CamelDefinitionYaml.integrationToYaml(i)) }); diff --git a/karavan-core/test/topology.spec.ts b/karavan-core/test/topology.spec.ts index e76ead2b..db689f21 100644 --- a/karavan-core/test/topology.spec.ts +++ b/karavan-core/test/topology.spec.ts @@ -30,7 +30,6 @@ describe('Topology functions', () => { const tin = TopologyUtils.findTopologyIncomingNodes([i1, i2]); const trn = TopologyUtils.findTopologyRestNodes([i1, i2]); const ton = TopologyUtils.findTopologyOutgoingNodes([i1, i2]); - console.log(tin) }); }); diff --git a/karavan-designer/public/example/postgresql-source.kamelet.yaml b/karavan-designer/public/example/postgresql-source.kamelet.yaml new file mode 100644 index 00000000..2fcd541e --- /dev/null +++ b/karavan-designer/public/example/postgresql-source.kamelet.yaml @@ -0,0 +1,114 @@ +# --------------------------------------------------------------------------- +# 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. +# --------------------------------------------------------------------------- +apiVersion: camel.apache.org/v1 +kind: Kamelet +metadata: + name: postgresql-source + annotations: + camel.apache.org/kamelet.support.level: "Stable" + camel.apache.org/catalog.version: "4.1.0-SNAPSHOT" + camel.apache.org/kamelet.icon: "data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+UG9zdGdyZVNRTCBpY29uPC90aXRsZT48cGF0aCBkPSJNMTcuMTI4IDBhMTAuMTM0IDEwLjEzNCAwIDAgMC0yLjc1NS40MDNsLS4wNjMuMDJBMTAuOTIyIDEwLjkyMiAwIDAgMCAxMi42LjI1OEMxMS40MjIuMjM4IDEwLjQxLjUyNCA5LjU5NCAxIDguNzkuNzIxIDcuMTIyLjI0IDUuMzY0LjMzNiA0LjE0LjQwMyAyLjgwNC43NzUgMS44MTQgMS44Mi44MjcgMi44NjUuMzA1IDQuNDgyLjQxNSA2LjY4MmMuMDMuNjA3LjIwMyAxLjU5Ny [...] + camel.apache.org/provider: "Apache Software Foundation" + camel.apache.org/kamelet.group: "SQL" + camel.apache.org/kamelet.namespace: "Database" + labels: + camel.apache.org/kamelet.type: "source" +spec: + definition: + title: "PostgreSQL Source" + description: |- + Query data from a PostgreSQL Database. + required: + - serverName + - username + - password + - query + - databaseName + type: object + properties: + serverName: + title: Server Name + description: The server name for the data source. + type: string + example: localhost + serverPort: + title: Server Port + description: The server port for the data source. + type: string + default: 5432 + username: + title: Username + description: The username to access a secured PostgreSQL Database. + type: string + x-descriptors: + - urn:camel:group:credentials + password: + title: Password + description: The password to access a secured PostgreSQL Database. + type: string + format: password + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:password + - urn:camel:group:credentials + query: + title: Query + description: The query to execute against the PostgreSQL Database. + type: string + example: 'INSERT INTO accounts (username,city) VALUES (:#username,:#city)' + databaseName: + title: Database Name + description: The name of the PostgreSQL Database. + type: string + consumedQuery: + title: Consumed Query + description: A query to run on a tuple consumed. + type: string + example: 'DELETE FROM accounts where user_id = :#user_id' + delay: + title: Delay + description: The number of milliseconds before the next poll + type: integer + default: 500 + types: + out: + mediaType: application/json + dependencies: + - "camel:jackson" + - "camel:kamelet" + - "camel:sql" + - "mvn:org.postgresql:postgresql:42.6.0" + - "mvn:org.apache.commons:commons-dbcp2:2.10.0" + template: + beans: + - name: dsBean + type: "#class:org.apache.commons.dbcp2.BasicDataSource" + properties: + username: '{{username}}' + password: '{{password}}' + url: 'jdbc:postgresql://{{serverName}}:{{serverPort}}/{{databaseName}}' + driverClassName: 'org.postgresql.Driver' + from: + uri: "sql:{{query}}" + parameters: + dataSource: "#bean:{{dsBean}}" + onConsume: "{{?consumedQuery}}" + delay: "{{delay}}" + steps: + - marshal: + json: + library: Jackson + - to: "kamelet:sink" \ No newline at end of file diff --git a/karavan-designer/src/App.tsx b/karavan-designer/src/App.tsx index b71267ba..dcea7fc1 100644 --- a/karavan-designer/src/App.tsx +++ b/karavan-designer/src/App.tsx @@ -69,7 +69,8 @@ class App extends React.Component<Props, State> { fetch("components/components.json"), fetch("snippets/org.apache.camel.AggregationStrategy"), fetch("snippets/org.apache.camel.Processor"), - fetch("example/demo.camel.yaml") + // fetch("example/demo.camel.yaml") + fetch("example/postgresql-source.kamelet.yaml") // fetch("components/supported-components.json"), ]).then(responses => Promise.all(responses.map(response => response.text())) @@ -88,7 +89,8 @@ class App extends React.Component<Props, State> { TemplateApi.saveTemplate("org.apache.camel.Processor", data[3]); if (data[4]) { - this.setState({yaml: data[4], name: "demo.camel.yaml"}) + // this.setState({yaml: data[4], name: "demo.camel.yaml"}) + this.setState({yaml: data[4], name: "postgresql-source.kamelet.yaml"}) } if (data[5]) { diff --git a/karavan-designer/src/designer/KaravanDesigner.tsx b/karavan-designer/src/designer/KaravanDesigner.tsx index cb58a377..316a71be 100644 --- a/karavan-designer/src/designer/KaravanDesigner.tsx +++ b/karavan-designer/src/designer/KaravanDesigner.tsx @@ -42,6 +42,7 @@ import {RestDesigner} from "./rest/RestDesigner"; import {BeansDesigner} from "./beans/BeansDesigner"; import {CodeEditor} from "./editor/CodeEditor"; import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import {KameletDesigner} from "./kamelet/KameletDesigner"; interface Props { onSave: (filename: string, yaml: string, propertyOnly: boolean) => void @@ -71,7 +72,9 @@ export function KaravanDesigner(props: Props) { InfrastructureAPI.setOnSave(props.onSave); setSelectedStep(undefined); - setIntegration(makeIntegration(props.yaml, props.filename), false); + const i = makeIntegration(props.yaml, props.filename); + setIntegration(i, false); + setTab(i.kind === 'Kamelet' ? 'kamelet' : 'routes') reset(); setDark(props.dark); setHideLogDSL(props.hideLogDSL === true); @@ -84,8 +87,10 @@ export function KaravanDesigner(props: Props) { function makeIntegration(yaml: string, filename: string): Integration { try { - if (yaml && CamelDefinitionYaml.yamlIsIntegration(yaml)) { - return CamelDefinitionYaml.yamlToIntegration(props.filename, props.yaml) + const type = CamelDefinitionYaml.yamlIsIntegration(yaml); + if (yaml && type !== 'none') { + const i = CamelDefinitionYaml.yamlToIntegration(props.filename, props.yaml) + return i; } else { return Integration.createNew(filename, 'plain'); } @@ -126,6 +131,8 @@ export function KaravanDesigner(props: Props) { ) } + const isKamelet = integration.type === 'kamelet'; + return ( <PageSection variant={props.dark ? PageSectionVariants.darker : PageSectionVariants.light} className="page" isFilled padding={{default: 'noPadding'}}> @@ -137,8 +144,9 @@ export function KaravanDesigner(props: Props) { setSelectedStep(undefined); }} style={{width: "100%"}}> + {isKamelet && <Tab eventKey='kamelet' title={getTab("Definitions", "Kamelet Definitions", "kamelet")}></Tab>} <Tab eventKey='routes' title={getTab("Routes", "Integration flows", "routes")}></Tab> - <Tab eventKey='rest' title={getTab("REST", "REST services", "rest")}></Tab> + {!isKamelet && <Tab eventKey='rest' title={getTab("REST", "REST services", "rest")}></Tab>} <Tab eventKey='beans' title={getTab("Beans", "Beans Configuration", "beans")}></Tab> {props.showCodeTab && <Tab eventKey='code' title={getTab("YAML", "YAML Code", "code", true)}></Tab>} </Tabs> @@ -156,6 +164,7 @@ export function KaravanDesigner(props: Props) { {/* />*/} {/*</Tooltip>}*/} </div> + {tab === 'kamelet' && <KameletDesigner/>} {tab === 'routes' && <RouteDesigner/>} {tab === 'rest' && <RestDesigner/>} {tab === 'beans' && <BeansDesigner/>} diff --git a/karavan-designer/src/designer/beans/BeanProperties.tsx b/karavan-designer/src/designer/beans/BeanProperties.tsx index 15b8f086..c27ea28c 100644 --- a/karavan-designer/src/designer/beans/BeanProperties.tsx +++ b/karavan-designer/src/designer/beans/BeanProperties.tsx @@ -194,7 +194,8 @@ export function BeanProperties (props: Props) { const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> return ( <div key={"key-" + i} className="bean-property"> - <TextInput placeholder="Bean Field Name" className="text-field" isRequired type="text" id="key" name="key" value={key} + <TextInput placeholder="Bean Field Name" className="text-field" isRequired type="text" id={"key-" + i} + name={"key-" + i} value={key} onChange={(_, beanFieldName) => { propertyChanged(i, beanFieldName, value, showPassword) }}/> @@ -211,8 +212,8 @@ export function BeanProperties (props: Props) { type={isSecret && !showPassword ? "password" : "text"} className="text-field" isRequired - id="value" - name="value" + id={"value-" + i} + name={"value-" + i} value={value} onChange={(_, value) => { propertyChanged(i, key, value, showPassword) diff --git a/karavan-designer/src/designer/kamelet/KameletAnnotationsPanel.tsx b/karavan-designer/src/designer/kamelet/KameletAnnotationsPanel.tsx new file mode 100644 index 00000000..3c997173 --- /dev/null +++ b/karavan-designer/src/designer/kamelet/KameletAnnotationsPanel.tsx @@ -0,0 +1,133 @@ +/* + * 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 { + capitalize, + Card, + CardBody, + CardTitle, + Form, + FormGroup, Grid, GridItem, + InputGroup, + InputGroupItem, + InputGroupText, + TextInput, ToggleGroup, ToggleGroupItem, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../KaravanStore"; +import {shallow} from "zustand/shallow"; + +const PREFIX = 'camel.apache.org/'; + +export function KameletAnnotationsPanel() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + + function setValue(key: string, value: string) { + if (key && value && value.length > 0) { + (integration.metadata.annotations as any)[PREFIX + key] = value; + setIntegration(integration, true); + } + } + + function getValue(key: string): string { + const annotations = integration.metadata.annotations; + if (annotations) { + return (annotations as any)[PREFIX + key]; + } else { + return ''; + } + } + + function getElement(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key} isRequired> + <InputGroup> + <InputGroupItem isFill> + <TextInput className="text-field" type="text" id={key} name={key} + onChange={(_, value) => setValue(key, value)} + value={getValue(key)}/> + </InputGroupItem> + </InputGroup> + </FormGroup> + </GridItem> + ) + } + + function getElementToggleGroup(key: string, label: string, values: string[], span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key} isRequired> + {/* eslint-disable-next-line react/jsx-no-undef */} + <ToggleGroup aria-label={key}> + {values.map(value => + <ToggleGroupItem + key={value} + text={capitalize(value)} + buttonId="toggle-group-single-1" + isSelected={getValue(key) === value} + onChange={(_, selected) => setValue(key, value) } + /> + )} + </ToggleGroup> + </FormGroup> + </GridItem> + ) + } + + function getElementIcon(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key} isRequired> + <InputGroup> + <InputGroupText id="username"> + <svg className="icon"> + <image href={getValue(key)} className="icon"/> + </svg> + </InputGroupText> + <InputGroupItem isFill> + <TextInput className="text-field" type="text" id={key} name={key} + onChange={(_, value) => setValue(key, value)} + value={getValue(key)}/> + </InputGroupItem> + </InputGroup> + </FormGroup> + </GridItem> + ) + } + + return ( + <Card isCompact ouiaId="AnnotationsCard"> + <CardTitle>Annotations</CardTitle> + <CardBody> + <Form> + <Grid hasGutter md={6}> + {getElementToggleGroup('kamelet.support.level', 'Support Level', ['Preview', 'Stable'], 2)} + {getElementIcon('kamelet.icon', 'Icon', 10)} + {getElement('catalog.version', 'Version', 3)} + {getElement('provider', 'Provider', 3)} + {getElement('kamelet.group', 'Group', 3)} + {getElement('kamelet.namespace', 'Namespace', 3)} + </Grid> + </Form> + </CardBody> + </Card> + ) +} diff --git a/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx b/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx new file mode 100644 index 00000000..a478c101 --- /dev/null +++ b/karavan-designer/src/designer/kamelet/KameletDefinitionPropertyCard.tsx @@ -0,0 +1,221 @@ +/* + * 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, + Card, + CardBody, + CardTitle, Flex, FlexItem, + FormGroup, FormSelect, FormSelectOption, + Grid, + GridItem, Label, Modal, Switch, + TextInput, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../KaravanStore"; +import {shallow} from "zustand/shallow"; +import {DefinitionProperty} from "karavan-core/lib/model/IntegrationDefinition"; + +interface Props { + index: number + propKey: string + property: DefinitionProperty +} + +export function KameletDefinitionPropertyCard(props: Props) { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false); + + const key = props.propKey; + const required = integration.spec.definition?.required || []; + + function setPropertyValue(field: string, value: string) { + if (integration.spec.definition?.properties) { + (integration.spec.definition?.properties as any)[key][field] = value; + setIntegration(integration, true); + } + } + + function getPropertyValue(field: string) { + const properties: any = integration.spec.definition?.properties; + if (properties) { + return properties[key][field]; + } + return undefined; + } + + + function getPropertyField(field: string, label: string, isRequired: boolean, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key + field} isRequired={isRequired}> + <TextInput className="text-field" type="text" id={key + field} name={key + field} + onChange={(_, value) => setPropertyValue(field, value)} + value={getPropertyValue(field)}/> + </FormGroup> + </GridItem> + ) + } + + function getPropertyTypeField(field: string, label: string, isRequired: boolean, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key + field} isRequired={isRequired}> + <FormSelect + value={getPropertyValue(field)} + onChange={(_, value) => setPropertyValue(field, value)} + aria-label="FormSelect Input" + ouiaId="BasicFormSelect" + > + {['string', 'number', 'boolean'].map((option, index) => ( + <FormSelectOption key={option} isDisabled={false} id={key + field} name={key + field} value={option} label={option} /> + ))} + </FormSelect> + </FormGroup> + </GridItem> + ) + } + + function renameProperty(newKey: string) { + const oldKey = key; + newKey = newKey.replace(/[\W_]+/g,''); + if (oldKey !== newKey) { + if (integration.spec.definition?.properties) { + const o = (integration.spec.definition?.properties as any) + const newObject: any = {}; + Object.keys(o).forEach(k => { + if (k !== oldKey) { + newObject[k] = o[k]; + } else { + newObject[newKey] = o[k]; + } + }) + integration.spec.definition.properties = newObject; + setIntegration(integration, true); + } + } + } + + function deleteProperty() { + if (integration.spec.definition?.properties) { + delete integration.spec.definition.properties[key]; + setIntegration(integration, true); + } + } + + function getDeleteConfirmation() { + return (<Modal + className="modal-delete" + title="Confirmation" + isOpen={showDeleteConfirmation} + onClose={() => setShowDeleteConfirmation(false)} + actions={[ + <Button key="confirm" variant="primary" onClick={e => deleteProperty()}>Delete</Button>, + <Button key="cancel" variant="link" + onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button> + ]} + onEscapePress={e => setShowDeleteConfirmation(false)}> + <div> + Delete {key} property? + </div> + </Modal>) + } + + function setRequired(checked: boolean) { + console.log(required, key) + const newRequired = [...required]; + if (checked && !newRequired.includes(key)) { + newRequired.push(key); + } else if (!checked && newRequired.includes(key)) { + const index = newRequired.findIndex(r => r === key); + newRequired.splice(index, 1); + } + // console.log(newRequired) + if (integration.spec.definition?.required) { + integration.spec.definition.required.length = 0; + integration.spec.definition.required.push(...newRequired) + } + setIntegration(integration, true); + } + + function getTitle() { + return ( + <Flex> + <FlexItem> + <Label + color="blue" + onClose={() => { + setShowDeleteConfirmation(true); + }} + closeBtnAriaLabel="Delete Property" + onEditCancel={(_, previousText) => { + }} + onEditComplete={(event, newText) => { + if (event.type === 'mousedown') { + renameProperty(newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Tab') { + renameProperty(newText) + } else if (event.type === 'keydown' && (event as KeyboardEvent).key === 'Enter') { + renameProperty(newText) + } else { + renameProperty(key) + } + }} + isEditable + editableProps={{ + 'aria-label': `Editable property with text ${key}`, + id: 'editable-property' + }} + > + {key} + </Label> + </FlexItem> + <FlexItem align={{default: "alignRight"}}> + <Switch + label={"Required"} + isChecked={required.includes(key)} + onChange={(_, checked) => setRequired(checked)} + isReversed + /> + </FlexItem> + </Flex> + ) + } + + + return ( + <Card isClickable isCompact isFlat ouiaId="PropertyCard" className="property-card"> + <CardTitle> + {getTitle()} + </CardTitle> + <CardBody> + <Grid hasGutter> + {getPropertyField("title", "Title", true, 3)} + {getPropertyField("description", "Description", true, 6)} + {getPropertyTypeField("type", "Type", true, 3)} + {getPropertyField("format", "Format", false, 3)} + {getPropertyField("example", "Example", false, 6)} + {getPropertyField("default", "Default", false, 3)} + {/*{getPropertyField("x-descriptors", "Descriptors", false, 12)}*/} + </Grid> + </CardBody> + {getDeleteConfirmation()} + </Card> + ) +} diff --git a/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx b/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx new file mode 100644 index 00000000..4c638ee3 --- /dev/null +++ b/karavan-designer/src/designer/kamelet/KameletDefinitionsPanel.tsx @@ -0,0 +1,108 @@ +/* + * 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 { + Button, + Card, + CardBody, + CardTitle, Flex, FlexItem, + Form, + FormGroup, + Grid, + GridItem, + TextInput, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {useIntegrationStore} from "../KaravanStore"; +import {shallow} from "zustand/shallow"; +import AddIcon from "@patternfly/react-icons/dist/js/icons/plus-circle-icon"; +import {KameletDefinitionPropertyCard} from "./KameletDefinitionPropertyCard"; + +export function KameletDefinitionsPanel() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + + function setValue(key: string, value: string) { + if (key && value && value.length > 0) { + (integration.spec.definition as any)[key] = value; + setIntegration(integration, true); + } + } + + function getValue(key: string): string { + const annotations = integration.spec.definition; + if (annotations) { + return (annotations as any)[key]; + } else { + return ''; + } + } + + function getElement(key: string, label: string, span: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) { + return ( + <GridItem span={span}> + <FormGroup label={label} fieldId={key} isRequired> + <TextInput className="text-field" type="text" id={key} name={key} + onChange={(_, value) => setValue(key, value)} + value={getValue(key)}/> + </FormGroup> + </GridItem> + ) + } + + const properties = integration.spec.definition?.properties ? Object.keys(integration.spec.definition?.properties) : []; + return ( + <> + <Card isCompact ouiaId="DefinitionsCard"> + <CardTitle>Definitions</CardTitle> + <CardBody> + <Form> + <Grid hasGutter> + {getElement('title', 'Title', 4)} + {getElement('description', 'Description', 6)} + {getElement('type', 'Type', 2)} + </Grid> + </Form> + </CardBody> + </Card> + <div style={{height: "20px"}}/> + <Card isCompact ouiaId="PropertiesCard"> + <CardTitle> + <Flex> + <FlexItem>Properties</FlexItem> + <FlexItem align={{default: "alignRight"}}> + <Button variant={"link"} icon={<AddIcon/>}>Add property</Button> + </FlexItem> + </Flex> + </CardTitle> + <CardBody> + <Form> + {properties.map((key: string, index: number) => { + const property = (integration.spec.definition?.properties as any)[key]; + return <KameletDefinitionPropertyCard key={key} + index={index} + propKey={key} + property={property}/> + })} + </Form> + </CardBody> + </Card> + </> + + ) +} diff --git a/karavan-designer/src/designer/kamelet/KameletDesigner.tsx b/karavan-designer/src/designer/kamelet/KameletDesigner.tsx new file mode 100644 index 00000000..3c0b5fa8 --- /dev/null +++ b/karavan-designer/src/designer/kamelet/KameletDesigner.tsx @@ -0,0 +1,132 @@ +/* + * 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 { + Button, Card, CardBody, CardFooter, CardTitle, Divider, + Drawer, + DrawerContent, + DrawerContentBody, + DrawerPanelContent, Flex, FlexItem, Gallery, GalleryItem, + Modal, + PageSection, +} from '@patternfly/react-core'; +import '../karavan.css'; +import './kamelet.css'; +import {RegistryBeanDefinition} from "karavan-core/lib/model/CamelDefinition"; +import {CamelUi} from "../utils/CamelUi"; +import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; +import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt"; +import {CamelUtil} from "karavan-core/lib/api/CamelUtil"; +import {useDesignerStore, useIntegrationStore} from "../KaravanStore"; +import {shallow} from "zustand/shallow"; +import {BeanProperties} from "../beans/BeanProperties"; +import {BeanCard} from "../beans/BeanCard"; +import {KameletAnnotationsPanel} from "./KameletAnnotationsPanel"; +import {KameletDefinitionsPanel} from "./KameletDefinitionsPanel"; +import {KameletProperties} from "./KameletProperties"; + +export function KameletDesigner() { + + const [integration, setIntegration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow) + const [dark, selectedStep, showDeleteConfirmation, setShowDeleteConfirmation, setSelectedStep] = useDesignerStore((s) => + [s.dark, s.selectedStep, s.showDeleteConfirmation, s.setShowDeleteConfirmation, s.setSelectedStep], shallow) + + + function onShowDeleteConfirmation(bean: RegistryBeanDefinition) { + setSelectedStep(bean); + setShowDeleteConfirmation(true); + } + + function deleteBean() { + const i = CamelDefinitionApiExt.deleteBeanFromIntegration(integration, selectedStep); + setIntegration(i, false); + setShowDeleteConfirmation(false); + setSelectedStep(undefined); + } + + function changeBean(bean: RegistryBeanDefinition) { + const clone = CamelUtil.cloneIntegration(integration); + const i = CamelDefinitionApiExt.addBeanToIntegration(clone, bean); + setIntegration(i, false); + setSelectedStep(bean); + } + + function getDeleteConfirmation() { + return (<Modal + className="modal-delete" + title="Confirmation" + isOpen={showDeleteConfirmation} + onClose={() => setShowDeleteConfirmation(false)} + actions={[ + <Button key="confirm" variant="primary" onClick={e => deleteBean()}>Delete</Button>, + <Button key="cancel" variant="link" + onClick={e => setShowDeleteConfirmation(false)}>Cancel</Button> + ]} + onEscapePress={e => setShowDeleteConfirmation(false)}> + <div> + Delete bean from integration? + </div> + </Modal>) + } + + function selectBean(bean?: RegistryBeanDefinition) { + setSelectedStep(bean); + } + + function unselectBean(evt: React.MouseEvent<HTMLDivElement, MouseEvent>) { + if ((evt.target as any).dataset.click === 'BEANS') { + evt.stopPropagation() + setSelectedStep(undefined); + } + }; + + function createBean() { + changeBean(new RegistryBeanDefinition()); + } + + function getPropertiesPanel() { + return ( + <DrawerPanelContent isResizable + hasNoBorder + defaultSize={'400px'} + maxSize={'800px'} + minSize={'400px'}> + <KameletProperties integration={integration} + dark={dark} + onChange={changeBean} + onClone={changeBean}/> + </DrawerPanelContent> + ) + } + + return ( + <PageSection className="kamelet-designer" isFilled padding={{default: 'noPadding'}}> + <Drawer isExpanded isInline> + <DrawerContent panelContent={getPropertiesPanel()}> + <DrawerContentBody> + <PageSection className="main"> + <KameletAnnotationsPanel/> + <div style={{height:"20px"}}/> + <KameletDefinitionsPanel/> + </PageSection> + </DrawerContentBody> + </DrawerContent> + </Drawer> + {getDeleteConfirmation()} + </PageSection> + ) +} diff --git a/karavan-designer/src/designer/beans/BeanProperties.tsx b/karavan-designer/src/designer/kamelet/KameletProperties.tsx similarity index 97% copy from karavan-designer/src/designer/beans/BeanProperties.tsx copy to karavan-designer/src/designer/kamelet/KameletProperties.tsx index 15b8f086..93c696fa 100644 --- a/karavan-designer/src/designer/beans/BeanProperties.tsx +++ b/karavan-designer/src/designer/kamelet/KameletProperties.tsx @@ -51,7 +51,7 @@ interface Props { onClone: (bean: RegistryBeanDefinition) => void } -export function BeanProperties (props: Props) { +export function KameletProperties (props: Props) { const [selectedStep] = useDesignerStore((s) => [s.selectedStep], shallow); const [infrastructureSelector, setInfrastructureSelector] = useState<boolean>(false); @@ -194,7 +194,8 @@ export function BeanProperties (props: Props) { const icon = InfrastructureAPI.infrastructure === 'kubernetes' ? <KubernetesIcon/> : <DockerIcon/> return ( <div key={"key-" + i} className="bean-property"> - <TextInput placeholder="Bean Field Name" className="text-field" isRequired type="text" id="key" name="key" value={key} + <TextInput placeholder="Bean Field Name" className="text-field" isRequired type="text" id={"key-" + i} + name={"key-" + i} value={key} onChange={(_, beanFieldName) => { propertyChanged(i, beanFieldName, value, showPassword) }}/> @@ -211,8 +212,8 @@ export function BeanProperties (props: Props) { type={isSecret && !showPassword ? "password" : "text"} className="text-field" isRequired - id="value" - name="value" + id={"value-" + i} + name={"value-" + i} value={value} onChange={(_, value) => { propertyChanged(i, key, value, showPassword) diff --git a/karavan-designer/src/designer/kamelet/kamelet.css b/karavan-designer/src/designer/kamelet/kamelet.css new file mode 100644 index 00000000..bbdf28a9 --- /dev/null +++ b/karavan-designer/src/designer/kamelet/kamelet.css @@ -0,0 +1,47 @@ +/* + * 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. + */ + +.karavan .kamelet-designer { + display: block; + height: 100vh; + width: 100%; + overflow-y: auto; + padding-bottom: 106px; +} + +.karavan .kamelet-designer .main { + background-color: var(--pf-v5-global--BackgroundColor--light-300); +} +.karavan .kamelet-designer .icon { + height: 20px; + width: 20px; + border: none; + -webkit-user-select: none; + -o-user-select: none; + user-select: none; +} + +.karavan .kamelet-designer .properties { + padding: 10px 10px 10px 10px; + background: transparent; + width: 100%; + height: 100%; + overflow: auto; + display: flex; + flex-direction: column; + justify-content: space-between; +} \ No newline at end of file diff --git a/karavan-designer/src/designer/route/DslProperties.tsx b/karavan-designer/src/designer/route/DslProperties.tsx index 69729121..ef933585 100644 --- a/karavan-designer/src/designer/route/DslProperties.tsx +++ b/karavan-designer/src/designer/route/DslProperties.tsx @@ -46,8 +46,7 @@ export function DslProperties(props: Props) { const {cloneElement, onDataFormatChange, onPropertyChange, onParametersChange, onExpressionChange} = usePropertiesHook(props.isRouteDesigner); - const [selectedStep, dark, setSelectedStep, setSelectedUuids] = useDesignerStore((s) => - [s.selectedStep, s.dark, s.setSelectedStep, s.setSelectedUuids], shallow) + const [selectedStep, dark] = useDesignerStore((s) => [s.selectedStep, s.dark], shallow) const [showAdvanced, setShowAdvanced] = useState<boolean>(false); const [isDescriptionExpanded, setIsDescriptionExpanded] = useState<boolean>(false); diff --git a/karavan-designer/src/designer/utils/IntegrationHeader.tsx b/karavan-designer/src/designer/utils/IntegrationHeader.tsx index c117d434..0725d1e1 100644 --- a/karavan-designer/src/designer/utils/IntegrationHeader.tsx +++ b/karavan-designer/src/designer/utils/IntegrationHeader.tsx @@ -23,19 +23,36 @@ export function IntegrationHeader () { const [integration] = useIntegrationStore((state) => [state.integration], shallow) + const isKamelet = integration.type === 'kamelet'; + + function getKameletType(): string { + // const labels = integration.metadata.labels; + // if (labels && labels.l) + // "camel.apache.org/kamelet.type" + return ''; + } + return ( <div className="headers"> - <Title headingLevel="h1" size="md">Integration</Title> + {/*<Title headingLevel="h1" size="md">Integration</Title>*/} {/*<FormGroup label="Title" fieldId="title" isRequired>*/} {/* <TextInput className="text-field" type="text" id="title" name="title" isReadOnly*/} {/* value={*/} {/* CamelUi.titleFromName(this.props.integration.metadata.name)*/} {/* }/>*/} {/*</FormGroup>*/} + <FormGroup label="Kind" fieldId="kind" isRequired> + <TextInput className="text-field" type="text" id="kind" name="kind" + value={integration.kind} readOnlyVariant="default"/> + </FormGroup> <FormGroup label="Name" fieldId="name" isRequired> <TextInput className="text-field" type="text" id="name" name="name" value={integration.metadata.name} readOnlyVariant="default"/> </FormGroup> + {isKamelet && <FormGroup label="Kamelet type" fieldId="type" isRequired> + <TextInput className="text-field" type="text" id="type" name="type" + value={integration.metadata.labels?.["camel.apache.org/kamelet.type"]} readOnlyVariant="default"/> + </FormGroup>} </div> ) } diff --git a/karavan-designer/src/designer/utils/KaravanIcons.tsx b/karavan-designer/src/designer/utils/KaravanIcons.tsx index 677945f5..906ba86e 100644 --- a/karavan-designer/src/designer/utils/KaravanIcons.tsx +++ b/karavan-designer/src/designer/utils/KaravanIcons.tsx @@ -263,21 +263,38 @@ export function CamelIcon(props?: (JSX.IntrinsicAttributes & React.SVGProps<SVGS } export function getDesignerIcon(icon: string) { - if (icon === 'code') return ( - <svg - className="top-icon" id="icon" - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - fill="none" - viewBox="0 0 24 24" - > - <path - fill="#000000" - d="M8.502 5.387a.75.75 0 00-1.004-1.115L5.761 5.836c-.737.663-1.347 1.212-1.767 1.71-.44.525-.754 1.088-.754 1.784 0 .695.313 1.258.754 1.782.42.499 1.03 1.049 1.767 1.711l1.737 1.564a.75.75 0 101.004-1.115l-1.697-1.527c-.788-.709-1.319-1.19-1.663-1.598-.33-.393-.402-.622-.402-.817 0-.196.072-.425.402-.818.344-.409.875-.889 1.663-1.598l1.697-1.527zM14.18 4.275a.75.75 0 01.532.918l-3.987 15a.75.75 0 11-1.45-.386l3.987-15a.75.75 0 01.918-.532zM15.443 10.498a.75.75 0 011.059-.05 [...] - ></path> - </svg> + if (icon === 'kamelet') return ( + <svg + className="top-icon" id="icon" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 32 32" + > + <title>{"application"}</title> + <path d="M16 18H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2ZM6 6v10h10V6ZM26 12v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2ZM26 22v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2ZM16 22v4h-4v-4h4m0-2h-4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-4a2 2 0 0 0-2-2Z" /> + <path + d="M0 0h32v32H0z" + data-name="<Transparent Rectangle>" + style={{ + fill: "none", + }} + /> + </svg> ) + if (icon === 'code') return ( + <svg + + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="none" + viewBox="0 0 24 24" + > + <path + fill="#000000" + d="M8.502 5.387a.75.75 0 00-1.004-1.115L5.761 5.836c-.737.663-1.347 1.212-1.767 1.71-.44.525-.754 1.088-.754 1.784 0 .695.313 1.258.754 1.782.42.499 1.03 1.049 1.767 1.711l1.737 1.564a.75.75 0 101.004-1.115l-1.697-1.527c-.788-.709-1.319-1.19-1.663-1.598-.33-.393-.402-.622-.402-.817 0-.196.072-.425.402-.818.344-.409.875-.889 1.663-1.598l1.697-1.527zM14.18 4.275a.75.75 0 01.532.918l-3.987 15a.75.75 0 11-1.45-.386l3.987-15a.75.75 0 01.918-.532zM15.443 10.498a.75.75 0 011.059 [...] + ></path> + </svg> + ) if (icon === 'routes') return ( <svg className="top-icon" width="32px" height="32px" viewBox="0 0 32 32" id="icon"> <defs>