You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ma...@apache.org on 2023/10/27 23:44:21 UTC
(camel-karavan) branch main updated: Create Kamelet from Existing #315
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 4ce5f9f9 Create Kamelet from Existing #315
4ce5f9f9 is described below
commit 4ce5f9f9e92c4c7b0bbcf0434dc6dc82efb10221
Author: Marat Gubaidullin <ma...@talismancloud.io>
AuthorDate: Fri Oct 27 19:44:10 2023 -0400
Create Kamelet from Existing #315
---
.../route/property/KameletPropertyField.tsx | 1 -
.../src/designer/ui/TypeaheadSelect.tsx | 233 +++++++++++++++++++++
karavan-designer/src/designer/utils/CamelUi.tsx | 2 +-
karavan-space/src/designer/KaravanDesigner.tsx | 2 -
.../src/designer/icons/ComponentIcons.tsx | 23 +-
karavan-space/src/designer/icons/KaravanIcons.tsx | 3 +-
.../src/designer/route/DslConnections.tsx | 2 +
karavan-space/src/designer/route/DslElement.tsx | 14 +-
karavan-space/src/designer/route/DslSelector.tsx | 3 +-
karavan-space/src/designer/route/RouteDesigner.tsx | 53 +++--
.../route/property/KameletPropertyField.tsx | 1 -
.../src/designer/route/useRouteDesignerHook.tsx | 42 +++-
karavan-space/src/designer/ui/TypeaheadSelect.tsx | 233 +++++++++++++++++++++
karavan-space/src/designer/utils/CamelUi.tsx | 12 +-
.../main/webui/src/designer/ui/TypeaheadSelect.tsx | 233 +++++++++++++++++++++
.../src/main/webui/src/designer/utils/CamelUi.tsx | 2 +-
.../webui/src/project/files/CreateFileModal.tsx | 86 ++------
17 files changed, 844 insertions(+), 101 deletions(-)
diff --git a/karavan-designer/src/designer/route/property/KameletPropertyField.tsx b/karavan-designer/src/designer/route/property/KameletPropertyField.tsx
index 0bac1c32..96b880c1 100644
--- a/karavan-designer/src/designer/route/property/KameletPropertyField.tsx
+++ b/karavan-designer/src/designer/route/property/KameletPropertyField.tsx
@@ -34,7 +34,6 @@ import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon";
import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon";
import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon";
import {usePropertiesHook} from "../usePropertiesHook";
-import {CamelUi} from "../../utils/CamelUi";
import {Select, SelectDirection, SelectOption, SelectVariant} from "@patternfly/react-core/deprecated";
interface Props {
diff --git a/karavan-designer/src/designer/ui/TypeaheadSelect.tsx b/karavan-designer/src/designer/ui/TypeaheadSelect.tsx
new file mode 100644
index 00000000..a529db43
--- /dev/null
+++ b/karavan-designer/src/designer/ui/TypeaheadSelect.tsx
@@ -0,0 +1,233 @@
+/*
+ * 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 {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectOptionProps,
+ MenuToggle,
+ MenuToggleElement,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Button
+} from '@patternfly/react-core';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+export interface Value {
+ value: string
+ children: string
+}
+
+interface Props {
+ listOfValues: Value[]
+ onSelect: (value: string) => void
+}
+
+export function TypeaheadSelect(props: Props) {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState<string>('');
+ const [inputValue, setInputValue] = React.useState<string>('');
+ const [filterValue, setFilterValue] = React.useState<string>('');
+ const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(props.listOfValues);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | undefined>(undefined);
+ const [activeItem, setActiveItem] = React.useState<string | undefined>(undefined);
+ const textInputRef = React.useRef<HTMLInputElement>();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = props.listOfValues;
+
+ // Filter menu items based on the text input value when one exists
+ if (filterValue) {
+ newSelectOptions = props.listOfValues.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display 'No results found'
+ if (!newSelectOptions.length) {
+ newSelectOptions = [
+ { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' }
+ ];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setActiveItem(undefined);
+ setFocusedItemIndex(undefined);
+ }, [filterValue]);
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
+ if (value) {
+ props.onSelect( value.toString())
+ }
+
+ if (value && value !== 'no results') {
+ setInputValue(value as string);
+ setFilterValue('');
+ const text = props.listOfValues.filter(v => v.value === value).at(0)?.children;
+ setSelected(text || value.toString());
+ setInputValue(text || value.toString());
+ }
+ setIsOpen(false);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
+ setInputValue(value);
+ setFilterValue(value);
+ };
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ if (indexToFocus !== undefined) {
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions[indexToFocus];
+ setActiveItem(`select-typeahead-${focusedItem.value}`);
+ }
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (isOpen && focusedItem.value !== 'no results') {
+ setInputValue(String(focusedItem.children));
+ setFilterValue('');
+ setSelected(String(focusedItem.children));
+ props.onSelect(focusedItem.value)
+ }
+
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(undefined);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
+ <MenuToggle ref={toggleRef} variant="typeahead" onClick={onToggleClick} isExpanded={isOpen} isFullWidth>
+ <TextInputGroup isPlain>
+ <TextInputGroupMain
+ value={inputValue}
+ onClick={onToggleClick}
+ onChange={onTextInputChange}
+ onKeyDown={onInputKeyDown}
+ id="typeahead-select-input"
+ autoComplete="off"
+ innerRef={textInputRef}
+ placeholder="Select a state"
+ {...(activeItem && { 'aria-activedescendant': activeItem })}
+ role="combobox"
+ isExpanded={isOpen}
+ aria-controls="select-typeahead-listbox"
+ />
+
+ <TextInputGroupUtilities>
+ {!!inputValue && (
+ <Button
+ variant="plain"
+ onClick={() => {
+ setSelected('');
+ setInputValue('');
+ setFilterValue('');
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+ <TimesIcon aria-hidden />
+ </Button>
+ )}
+ </TextInputGroupUtilities>
+ </TextInputGroup>
+ </MenuToggle>
+ );
+
+ return (
+ <Select
+ id="typeahead-select"
+ isOpen={isOpen}
+ selected={selected}
+ onSelect={onSelect}
+ onOpenChange={() => {
+ setIsOpen(false);
+ }}
+ toggle={toggle}
+ >
+ <SelectList id="select-typeahead-listbox">
+ {selectOptions.map((option, index) => (
+ <SelectOption
+ key={option.value || option.children}
+ isFocused={focusedItemIndex === index}
+ className={option.className}
+ onClick={() => setSelected(option.value)}
+ id={`select-typeahead-${option.value}`}
+ {...option}
+ ref={null}
+ />
+ ))}
+ </SelectList>
+ </Select>
+ );
+};
diff --git a/karavan-designer/src/designer/utils/CamelUi.tsx b/karavan-designer/src/designer/utils/CamelUi.tsx
index 2b672a8d..ec4d5796 100644
--- a/karavan-designer/src/designer/utils/CamelUi.tsx
+++ b/karavan-designer/src/designer/utils/CamelUi.tsx
@@ -108,7 +108,7 @@ const StepElements: string[] = [
// "ErrorHandlerDefinition",
"FilterDefinition",
"IdempotentConsumerDefinition",
- "KameletDefinition",
+ // "KameletDefinition",
"LogDefinition",
"LoopDefinition",
"MarshalDefinition",
diff --git a/karavan-space/src/designer/KaravanDesigner.tsx b/karavan-space/src/designer/KaravanDesigner.tsx
index 4b7b3c7a..b1af73c5 100644
--- a/karavan-space/src/designer/KaravanDesigner.tsx
+++ b/karavan-space/src/designer/KaravanDesigner.tsx
@@ -24,8 +24,6 @@ import {
Tabs,
TabTitleIcon,
TabTitleText,
- Tooltip,
- TooltipPosition,
} from '@patternfly/react-core';
import './karavan.css';
import {RouteDesigner} from "./route/RouteDesigner";
diff --git a/karavan-space/src/designer/icons/ComponentIcons.tsx b/karavan-space/src/designer/icons/ComponentIcons.tsx
index a05f5e70..bda35d53 100644
--- a/karavan-space/src/designer/icons/ComponentIcons.tsx
+++ b/karavan-space/src/designer/icons/ComponentIcons.tsx
@@ -1165,7 +1165,28 @@ export function ApiIcon() {
);
}
-export function MonitoringIcon() {
+export function KameletIcon() {
+ return (
+ <svg
+ className="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>
+ )
+}
+
+
+ export function MonitoringIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
diff --git a/karavan-space/src/designer/icons/KaravanIcons.tsx b/karavan-space/src/designer/icons/KaravanIcons.tsx
index b7165b93..b0b8042f 100644
--- a/karavan-space/src/designer/icons/KaravanIcons.tsx
+++ b/karavan-space/src/designer/icons/KaravanIcons.tsx
@@ -260,7 +260,7 @@ export function CamelIcon(props?: (JSX.IntrinsicAttributes & React.SVGProps<SVGS
);
}
-export function getDesignerIcon(icon: string) {
+export function getDesignerIcon(icon: string): React.JSX.Element {
if (icon === 'kamelet') return (
<svg
className="top-icon" id="icon"
@@ -429,6 +429,7 @@ export function getDesignerIcon(icon: string) {
<rect id="_Transparent_Rectangle_" data-name="<Transparent Rectangle>" className="cls-1" width="32"
height="32" transform="translate(0 32) rotate(-90)"/>
</svg>)
+ return <></>;
}
diff --git a/karavan-space/src/designer/route/DslConnections.tsx b/karavan-space/src/designer/route/DslConnections.tsx
index de6a0060..a636339e 100644
--- a/karavan-space/src/designer/route/DslConnections.tsx
+++ b/karavan-space/src/designer/route/DslConnections.tsx
@@ -63,6 +63,7 @@ export function DslConnections() {
.filter(pos => ["FromDefinition"].includes(pos.step.dslName))
.filter(pos => !TopologyUtils.isElementInternalComponent(pos.step))
.filter(pos => !(pos.step.dslName === 'FromDefinition' && TopologyUtils.hasInternalUri(pos.step)))
+ .filter(pos => !(pos.step.dslName === 'FromDefinition' && (pos.step as any).uri === 'kamelet:source'))
.sort((pos1: DslPosition, pos2: DslPosition) => {
const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
@@ -142,6 +143,7 @@ export function DslConnections() {
.filter(pos => pos.step.dslName === 'ToDefinition' && !CamelUi.isActionKamelet(pos.step) && !TopologyUtils.isElementInternalComponent(pos.step))
.filter(pos => !(outgoingDefinitions.includes(pos.step.dslName) && TopologyUtils.hasInternalUri(pos.step)))
.filter(pos => pos.step.dslName !== 'SagaDefinition')
+ .filter(pos => !CamelUi.isKameletSink(pos.step))
.sort((pos1: DslPosition, pos2: DslPosition) => {
const y1 = pos1.headerRect.y + pos1.headerRect.height / 2;
const y2 = pos2.headerRect.y + pos2.headerRect.height / 2;
diff --git a/karavan-space/src/designer/route/DslElement.tsx b/karavan-space/src/designer/route/DslElement.tsx
index 305b7859..9e4a8c70 100644
--- a/karavan-space/src/designer/route/DslElement.tsx
+++ b/karavan-space/src/designer/route/DslElement.tsx
@@ -42,7 +42,7 @@ interface Props {
export function DslElement(props: Props) {
const headerRef = React.useRef<HTMLDivElement>(null);
- const {selectElement, moveElement, onShowDeleteConfirmation, openSelector} = useRouteDesignerHook();
+ const {selectElement, moveElement, onShowDeleteConfirmation, openSelector, isKamelet, isSourceKamelet, isActionKamelet} = useRouteDesignerHook();
const [integration] = useIntegrationStore((s) => [s.integration, s.setIntegration], shallow)
@@ -241,9 +241,19 @@ export function DslElement(props: Props) {
)
}
+ function getHeaderText(step: CamelElement): string {
+ if (isKamelet() && step.dslName === 'ToDefinition' && (step as any).uri === 'kamelet:sink') {
+ return "Sink";
+ } else if (isKamelet() && step.dslName === 'FromDefinition' && (step as any).uri === 'kamelet:source') {
+ return "Source";
+ } else {
+ return (step as any).description ? (step as any).description : CamelUi.getElementTitle(props.step);
+ }
+ }
+
function getHeaderTextWithTooltip(step: CamelElement) {
const checkRequired = CamelUtil.checkRequired(step);
- const title = (step as any).description ? (step as any).description : CamelUi.getElementTitle(props.step);
+ const title = getHeaderText(step);
let className = hasWideChildrenElement() ? "text text-right" : "text text-bottom";
if (!checkRequired[0]) className = className + " header-text-required";
if (checkRequired[0]) {
diff --git a/karavan-space/src/designer/route/DslSelector.tsx b/karavan-space/src/designer/route/DslSelector.tsx
index 8f815536..a80525ba 100644
--- a/karavan-space/src/designer/route/DslSelector.tsx
+++ b/karavan-space/src/designer/route/DslSelector.tsx
@@ -24,7 +24,7 @@ import {
import '../karavan.css';
import {CamelUi} from "../utils/CamelUi";
import {DslMetaModel} from "../utils/DslMetaModel";
-import {useDesignerStore, useSelectorStore} from "../DesignerStore";
+import {useDesignerStore, useIntegrationStore, useSelectorStore} from "../DesignerStore";
import {shallow} from "zustand/shallow";
import {useRouteDesignerHook} from "./useRouteDesignerHook";
@@ -40,7 +40,6 @@ export function DslSelector (props: Props) {
[s.showSelector, s.showSteps, s.parentId, s.parentDsl, s.selectorTabIndex, s.setShowSelector, s.setSelectorTabIndex,
s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel], shallow)
-
const [dark] = useDesignerStore((s) => [s.dark], shallow)
const {onDslSelect} = useRouteDesignerHook();
diff --git a/karavan-space/src/designer/route/RouteDesigner.tsx b/karavan-space/src/designer/route/RouteDesigner.tsx
index 215f334b..c57260db 100644
--- a/karavan-space/src/designer/route/RouteDesigner.tsx
+++ b/karavan-space/src/designer/route/RouteDesigner.tsx
@@ -40,7 +40,8 @@ import {DslElementMoveModal} from "./DslElementMoveModal";
export function RouteDesigner() {
- const {openSelector, createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement} = useRouteDesignerHook();
+ const {openSelector, createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement, onDslSelect,
+ isSourceKamelet, isActionKamelet, isKamelet, isSinkKamelet} = useRouteDesignerHook();
const [integration] = useIntegrationStore((state) => [state.integration], shallow)
const [showDeleteConfirmation, setPosition, width, height, top, left, hideLogDSL, showMoveConfirmation, setShowMoveConfirmation] =
@@ -104,6 +105,37 @@ export function RouteDesigner() {
)
}
+ function getGraphButtons() {
+ const routes = CamelUi.getRoutes(integration);
+ const showNewRoute = (isKamelet() && routes.length === 0) || !isKamelet();
+ const showNewRouteConfiguration = !isKamelet();
+ return (
+ <div className="add-flow">
+ {showNewRoute && <Button
+ variant={routes.length === 0 ? "primary" : "secondary"}
+ icon={<PlusIcon/>}
+ onClick={e => {
+ if (isSinkKamelet() || isActionKamelet()) {
+ const dsl = CamelUi.getDslMetaModel('FromDefinition');
+ dsl.uri = 'kamelet:source';
+ onDslSelect(dsl, '', undefined);
+ } else {
+ openSelector(undefined, undefined)
+ }
+ }}
+ >
+ Create route
+ </Button>}
+ {showNewRouteConfiguration && <Button
+ variant="secondary"
+ icon={<PlusIcon/>}
+ onClick={e => createRouteConfiguration()}
+ >
+ Create configuration
+ </Button>}
+ </div>
+ )
+ }
function getGraph() {
const routes = CamelUi.getRoutes(integration);
const routeConfigurations = CamelUi.getRouteConfigurations(integration);
@@ -129,24 +161,7 @@ export function RouteDesigner() {
step={route}
parent={undefined}/>
))}
- <div className="add-flow">
- <Button
- variant={routes.length === 0 ? "primary" : "secondary"}
- icon={<PlusIcon/>}
- onClick={e => {
- openSelector(undefined, undefined)
- }}
- >
- Create route
- </Button>
- <Button
- variant="secondary"
- icon={<PlusIcon/>}
- onClick={e => createRouteConfiguration()}
- >
- Create configuration
- </Button>
- </div>
+ {getGraphButtons()}
</div>
</div>)
}
diff --git a/karavan-space/src/designer/route/property/KameletPropertyField.tsx b/karavan-space/src/designer/route/property/KameletPropertyField.tsx
index 0bac1c32..96b880c1 100644
--- a/karavan-space/src/designer/route/property/KameletPropertyField.tsx
+++ b/karavan-space/src/designer/route/property/KameletPropertyField.tsx
@@ -34,7 +34,6 @@ import ShowIcon from "@patternfly/react-icons/dist/js/icons/eye-icon";
import HideIcon from "@patternfly/react-icons/dist/js/icons/eye-slash-icon";
import DockerIcon from "@patternfly/react-icons/dist/js/icons/docker-icon";
import {usePropertiesHook} from "../usePropertiesHook";
-import {CamelUi} from "../../utils/CamelUi";
import {Select, SelectDirection, SelectOption, SelectVariant} from "@patternfly/react-core/deprecated";
interface Props {
diff --git a/karavan-space/src/designer/route/useRouteDesignerHook.tsx b/karavan-space/src/designer/route/useRouteDesignerHook.tsx
index fe61d410..f48fc850 100644
--- a/karavan-space/src/designer/route/useRouteDesignerHook.tsx
+++ b/karavan-space/src/designer/route/useRouteDesignerHook.tsx
@@ -19,7 +19,7 @@ import '../karavan.css';
import {DslMetaModel} from "../utils/DslMetaModel";
import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
import {ChoiceDefinition, FromDefinition, LogDefinition, RouteConfigurationDefinition, RouteDefinition} from "karavan-core/lib/model/CamelDefinition";
-import {CamelElement} from "karavan-core/lib/model/IntegrationDefinition";
+import {CamelElement, MetadataLabels} from "karavan-core/lib/model/IntegrationDefinition";
import {CamelDefinitionApiExt} from "karavan-core/lib/api/CamelDefinitionApiExt";
import {CamelDefinitionApi} from "karavan-core/lib/api/CamelDefinitionApi";
import {Command, EventBus} from "../utils/EventBus";
@@ -27,6 +27,7 @@ import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
import {toPng} from 'html-to-image';
import {useDesignerStore, useIntegrationStore, useSelectorStore} from "../DesignerStore";
import {shallow} from "zustand/shallow";
+import {v4 as uuidv4} from 'uuid';
export function useRouteDesignerHook () {
@@ -46,6 +47,34 @@ export function useRouteDesignerHook () {
}
}
+ function isKamelet(): boolean {
+ return integration.type === 'kamelet';
+ }
+
+ function isSourceKamelet(): boolean {
+ if (isKamelet()){
+ const m: MetadataLabels | undefined = integration.metadata.labels;
+ return m !== undefined && m["camel.apache.org/kamelet.type"] === 'source';
+ }
+ return false;
+ }
+
+ function isSinkKamelet(): boolean {
+ if (isKamelet()){
+ const m: MetadataLabels | undefined = integration.metadata.labels;
+ return m !== undefined && m["camel.apache.org/kamelet.type"] === 'sink';
+ }
+ return false;
+ }
+
+ function isActionKamelet(): boolean {
+ if (isKamelet()){
+ const m: MetadataLabels | undefined = integration.metadata.labels;
+ return m !== undefined && m["camel.apache.org/kamelet.type"] === 'action';
+ }
+ return false;
+ }
+
const onShowDeleteConfirmation = (id: string) => {
let message: string;
const uuidsToDelete:string [] = [id];
@@ -193,13 +222,17 @@ export function useRouteDesignerHook () {
setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'kamelet' : 'eip');
}
- const onDslSelect = (dsl: DslMetaModel, parentId: string, position?: number | undefined) => {
+ function onDslSelect (dsl: DslMetaModel, parentId: string, position?: number | undefined) {
switch (dsl.dsl) {
case 'FromDefinition' :
- const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri})});
+ const nodePrefixId = isKamelet() ? integration.metadata.name : 'route-' + uuidv4().substring(0,3);
+ const route = CamelDefinitionApi.createRouteDefinition({from: new FromDefinition({uri: dsl.uri}), nodePrefixId: nodePrefixId});
addStep(route, parentId, position)
break;
case 'ToDefinition' :
+ if (dsl.uri === undefined && isKamelet()) {
+ dsl.uri = 'kamelet:sink';
+ }
const to = CamelDefinitionApi.createStep(dsl.dsl, {uri: dsl.uri});
addStep(to, parentId, position)
break;
@@ -291,5 +324,6 @@ export function useRouteDesignerHook () {
}
return { deleteElement, selectElement, moveElement, onShowDeleteConfirmation, onDslSelect, openSelector,
- createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement}
+ createRouteConfiguration, onCommand, handleKeyDown, handleKeyUp, unselectElement, isKamelet, isSourceKamelet,
+ isActionKamelet, isSinkKamelet}
}
\ No newline at end of file
diff --git a/karavan-space/src/designer/ui/TypeaheadSelect.tsx b/karavan-space/src/designer/ui/TypeaheadSelect.tsx
new file mode 100644
index 00000000..a529db43
--- /dev/null
+++ b/karavan-space/src/designer/ui/TypeaheadSelect.tsx
@@ -0,0 +1,233 @@
+/*
+ * 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 {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectOptionProps,
+ MenuToggle,
+ MenuToggleElement,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Button
+} from '@patternfly/react-core';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+export interface Value {
+ value: string
+ children: string
+}
+
+interface Props {
+ listOfValues: Value[]
+ onSelect: (value: string) => void
+}
+
+export function TypeaheadSelect(props: Props) {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState<string>('');
+ const [inputValue, setInputValue] = React.useState<string>('');
+ const [filterValue, setFilterValue] = React.useState<string>('');
+ const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(props.listOfValues);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | undefined>(undefined);
+ const [activeItem, setActiveItem] = React.useState<string | undefined>(undefined);
+ const textInputRef = React.useRef<HTMLInputElement>();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = props.listOfValues;
+
+ // Filter menu items based on the text input value when one exists
+ if (filterValue) {
+ newSelectOptions = props.listOfValues.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display 'No results found'
+ if (!newSelectOptions.length) {
+ newSelectOptions = [
+ { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' }
+ ];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setActiveItem(undefined);
+ setFocusedItemIndex(undefined);
+ }, [filterValue]);
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
+ if (value) {
+ props.onSelect( value.toString())
+ }
+
+ if (value && value !== 'no results') {
+ setInputValue(value as string);
+ setFilterValue('');
+ const text = props.listOfValues.filter(v => v.value === value).at(0)?.children;
+ setSelected(text || value.toString());
+ setInputValue(text || value.toString());
+ }
+ setIsOpen(false);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
+ setInputValue(value);
+ setFilterValue(value);
+ };
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ if (indexToFocus !== undefined) {
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions[indexToFocus];
+ setActiveItem(`select-typeahead-${focusedItem.value}`);
+ }
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (isOpen && focusedItem.value !== 'no results') {
+ setInputValue(String(focusedItem.children));
+ setFilterValue('');
+ setSelected(String(focusedItem.children));
+ props.onSelect(focusedItem.value)
+ }
+
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(undefined);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
+ <MenuToggle ref={toggleRef} variant="typeahead" onClick={onToggleClick} isExpanded={isOpen} isFullWidth>
+ <TextInputGroup isPlain>
+ <TextInputGroupMain
+ value={inputValue}
+ onClick={onToggleClick}
+ onChange={onTextInputChange}
+ onKeyDown={onInputKeyDown}
+ id="typeahead-select-input"
+ autoComplete="off"
+ innerRef={textInputRef}
+ placeholder="Select a state"
+ {...(activeItem && { 'aria-activedescendant': activeItem })}
+ role="combobox"
+ isExpanded={isOpen}
+ aria-controls="select-typeahead-listbox"
+ />
+
+ <TextInputGroupUtilities>
+ {!!inputValue && (
+ <Button
+ variant="plain"
+ onClick={() => {
+ setSelected('');
+ setInputValue('');
+ setFilterValue('');
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+ <TimesIcon aria-hidden />
+ </Button>
+ )}
+ </TextInputGroupUtilities>
+ </TextInputGroup>
+ </MenuToggle>
+ );
+
+ return (
+ <Select
+ id="typeahead-select"
+ isOpen={isOpen}
+ selected={selected}
+ onSelect={onSelect}
+ onOpenChange={() => {
+ setIsOpen(false);
+ }}
+ toggle={toggle}
+ >
+ <SelectList id="select-typeahead-listbox">
+ {selectOptions.map((option, index) => (
+ <SelectOption
+ key={option.value || option.children}
+ isFocused={focusedItemIndex === index}
+ className={option.className}
+ onClick={() => setSelected(option.value)}
+ id={`select-typeahead-${option.value}`}
+ {...option}
+ ref={null}
+ />
+ ))}
+ </SelectList>
+ </Select>
+ );
+};
diff --git a/karavan-space/src/designer/utils/CamelUi.tsx b/karavan-space/src/designer/utils/CamelUi.tsx
index 6864361b..ec4d5796 100644
--- a/karavan-space/src/designer/utils/CamelUi.tsx
+++ b/karavan-space/src/designer/utils/CamelUi.tsx
@@ -51,7 +51,7 @@ import {
IgniteIcon,
InfinispanIcon,
IotIcon,
- KafkaIcon,
+ KafkaIcon, KameletIcon,
KubernetesIcon,
MachineLearningIcon,
MailIcon,
@@ -93,6 +93,7 @@ import {
import React from "react";
import {TopologyUtils} from "karavan-core/lib/api/TopologyUtils";
import {CamelDisplayUtil} from "karavan-core/lib/api/CamelDisplayUtil";
+import {getDesignerIcon} from "../icons/KaravanIcons";
const StepElements: string[] = [
"AggregateDefinition",
@@ -107,6 +108,7 @@ const StepElements: string[] = [
// "ErrorHandlerDefinition",
"FilterDefinition",
"IdempotentConsumerDefinition",
+ // "KameletDefinition",
"LogDefinition",
"LoopDefinition",
"MarshalDefinition",
@@ -306,6 +308,10 @@ export class CamelUi {
else return false;
}
+ static isKameletSink = (element: CamelElement): boolean => {
+ return element.dslName === 'ToDefinition' && (element as any).uri === 'kamelet:sink';
+ }
+
static getInternalRouteUris = (integration: Integration, componentName: string, showComponentName: boolean = true): string[] => {
const result: string[] = [];
integration.spec.flows?.filter(f => f.dslName === 'RouteDefinition')
@@ -519,7 +525,7 @@ export class CamelUi {
}
}
- static getIconForDsl = (dsl: DslMetaModel): JSX.Element => {
+ static getIconForDsl = (dsl: DslMetaModel): React.JSX.Element => {
if (dsl.dsl && (dsl.dsl === "KameletDefinition" || dsl.navigation === 'kamelet')) {
return this.getIconFromSource(CamelUi.getKameletIconByName(dsl.name));
} else if ((dsl.dsl && dsl.dsl === "FromDefinition")
@@ -687,6 +693,8 @@ export class CamelUi {
return <ApiIcon/>;
case 'HeadDefinition' :
return <ApiIcon/>;
+ case 'KameletDefinition' :
+ return <KameletIcon/>;
default:
return this.getIconFromSource(CamelUi.getIconSrcForName(dslName))
}
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/ui/TypeaheadSelect.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/ui/TypeaheadSelect.tsx
new file mode 100644
index 00000000..a529db43
--- /dev/null
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/ui/TypeaheadSelect.tsx
@@ -0,0 +1,233 @@
+/*
+ * 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 {
+ Select,
+ SelectOption,
+ SelectList,
+ SelectOptionProps,
+ MenuToggle,
+ MenuToggleElement,
+ TextInputGroup,
+ TextInputGroupMain,
+ TextInputGroupUtilities,
+ Button
+} from '@patternfly/react-core';
+import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon';
+
+export interface Value {
+ value: string
+ children: string
+}
+
+interface Props {
+ listOfValues: Value[]
+ onSelect: (value: string) => void
+}
+
+export function TypeaheadSelect(props: Props) {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const [selected, setSelected] = React.useState<string>('');
+ const [inputValue, setInputValue] = React.useState<string>('');
+ const [filterValue, setFilterValue] = React.useState<string>('');
+ const [selectOptions, setSelectOptions] = React.useState<SelectOptionProps[]>(props.listOfValues);
+ const [focusedItemIndex, setFocusedItemIndex] = React.useState<number | undefined>(undefined);
+ const [activeItem, setActiveItem] = React.useState<string | undefined>(undefined);
+ const textInputRef = React.useRef<HTMLInputElement>();
+
+ React.useEffect(() => {
+ let newSelectOptions: SelectOptionProps[] = props.listOfValues;
+
+ // Filter menu items based on the text input value when one exists
+ if (filterValue) {
+ newSelectOptions = props.listOfValues.filter((menuItem) =>
+ String(menuItem.children).toLowerCase().includes(filterValue.toLowerCase())
+ );
+
+ // When no options are found after filtering, display 'No results found'
+ if (!newSelectOptions.length) {
+ newSelectOptions = [
+ { isDisabled: false, children: `No results found for "${filterValue}"`, value: 'no results' }
+ ];
+ }
+
+ // Open the menu when the input value changes and the new value is not empty
+ if (!isOpen) {
+ setIsOpen(true);
+ }
+ }
+
+ setSelectOptions(newSelectOptions);
+ setActiveItem(undefined);
+ setFocusedItemIndex(undefined);
+ }, [filterValue]);
+
+ const onToggleClick = () => {
+ setIsOpen(!isOpen);
+ };
+
+ const onSelect = (_event: React.MouseEvent<Element, MouseEvent> | undefined, value: string | number | undefined) => {
+ if (value) {
+ props.onSelect( value.toString())
+ }
+
+ if (value && value !== 'no results') {
+ setInputValue(value as string);
+ setFilterValue('');
+ const text = props.listOfValues.filter(v => v.value === value).at(0)?.children;
+ setSelected(text || value.toString());
+ setInputValue(text || value.toString());
+ }
+ setIsOpen(false);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+ };
+
+ const onTextInputChange = (_event: React.FormEvent<HTMLInputElement>, value: string) => {
+ setInputValue(value);
+ setFilterValue(value);
+ };
+
+ const handleMenuArrowKeys = (key: string) => {
+ let indexToFocus;
+
+ if (isOpen) {
+ if (key === 'ArrowUp') {
+ // When no index is set or at the first index, focus to the last, otherwise decrement focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === 0) {
+ indexToFocus = selectOptions.length - 1;
+ } else {
+ indexToFocus = focusedItemIndex - 1;
+ }
+ }
+
+ if (key === 'ArrowDown') {
+ // When no index is set or at the last index, focus to the first, otherwise increment focus index
+ if (focusedItemIndex === undefined || focusedItemIndex === selectOptions.length - 1) {
+ indexToFocus = 0;
+ } else {
+ indexToFocus = focusedItemIndex + 1;
+ }
+ }
+
+ if (indexToFocus !== undefined) {
+ setFocusedItemIndex(indexToFocus);
+ const focusedItem = selectOptions[indexToFocus];
+ setActiveItem(`select-typeahead-${focusedItem.value}`);
+ }
+ }
+ };
+
+ const onInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+ const enabledMenuItems = selectOptions.filter((option) => !option.isDisabled);
+ const [firstMenuItem] = enabledMenuItems;
+ const focusedItem = focusedItemIndex ? enabledMenuItems[focusedItemIndex] : firstMenuItem;
+
+ switch (event.key) {
+ // Select the first available option
+ case 'Enter':
+ if (isOpen && focusedItem.value !== 'no results') {
+ setInputValue(String(focusedItem.children));
+ setFilterValue('');
+ setSelected(String(focusedItem.children));
+ props.onSelect(focusedItem.value)
+ }
+
+ setIsOpen((prevIsOpen) => !prevIsOpen);
+ setFocusedItemIndex(undefined);
+ setActiveItem(undefined);
+
+ break;
+ case 'Tab':
+ case 'Escape':
+ setIsOpen(false);
+ setActiveItem(undefined);
+ break;
+ case 'ArrowUp':
+ case 'ArrowDown':
+ event.preventDefault();
+ handleMenuArrowKeys(event.key);
+ break;
+ }
+ };
+
+ const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
+ <MenuToggle ref={toggleRef} variant="typeahead" onClick={onToggleClick} isExpanded={isOpen} isFullWidth>
+ <TextInputGroup isPlain>
+ <TextInputGroupMain
+ value={inputValue}
+ onClick={onToggleClick}
+ onChange={onTextInputChange}
+ onKeyDown={onInputKeyDown}
+ id="typeahead-select-input"
+ autoComplete="off"
+ innerRef={textInputRef}
+ placeholder="Select a state"
+ {...(activeItem && { 'aria-activedescendant': activeItem })}
+ role="combobox"
+ isExpanded={isOpen}
+ aria-controls="select-typeahead-listbox"
+ />
+
+ <TextInputGroupUtilities>
+ {!!inputValue && (
+ <Button
+ variant="plain"
+ onClick={() => {
+ setSelected('');
+ setInputValue('');
+ setFilterValue('');
+ textInputRef?.current?.focus();
+ }}
+ aria-label="Clear input value"
+ >
+ <TimesIcon aria-hidden />
+ </Button>
+ )}
+ </TextInputGroupUtilities>
+ </TextInputGroup>
+ </MenuToggle>
+ );
+
+ return (
+ <Select
+ id="typeahead-select"
+ isOpen={isOpen}
+ selected={selected}
+ onSelect={onSelect}
+ onOpenChange={() => {
+ setIsOpen(false);
+ }}
+ toggle={toggle}
+ >
+ <SelectList id="select-typeahead-listbox">
+ {selectOptions.map((option, index) => (
+ <SelectOption
+ key={option.value || option.children}
+ isFocused={focusedItemIndex === index}
+ className={option.className}
+ onClick={() => setSelected(option.value)}
+ id={`select-typeahead-${option.value}`}
+ {...option}
+ ref={null}
+ />
+ ))}
+ </SelectList>
+ </Select>
+ );
+};
diff --git a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
index 2b672a8d..ec4d5796 100644
--- a/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/designer/utils/CamelUi.tsx
@@ -108,7 +108,7 @@ const StepElements: string[] = [
// "ErrorHandlerDefinition",
"FilterDefinition",
"IdempotentConsumerDefinition",
- "KameletDefinition",
+ // "KameletDefinition",
"LogDefinition",
"LoopDefinition",
"MarshalDefinition",
diff --git a/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx b/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
index 53473734..688c37e9 100644
--- a/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
+++ b/karavan-web/karavan-app/src/main/webui/src/project/files/CreateFileModal.tsx
@@ -22,9 +22,7 @@ import {
FormGroup,
ModalVariant,
Form,
- ToggleGroupItem, ToggleGroup, FormHelperText, HelperText, HelperTextItem, TextInput, Select,
- SelectOption,
- SelectList, MenuToggleElement, MenuToggle, TextInputGroup, TextInputGroupMain, TextInputGroupUtilities,
+ ToggleGroupItem, ToggleGroup, FormHelperText, HelperText, HelperTextItem, TextInput,
} from '@patternfly/react-core';
import '../../designer/karavan.css';
import {Integration, KameletTypes, MetadataLabels} from "karavan-core/lib/model/IntegrationDefinition";
@@ -36,6 +34,7 @@ import {ProjectService} from "../../api/ProjectService";
import {shallow} from "zustand/shallow";
import {CamelUtil} from "karavan-core/lib/api/CamelUtil";
import {KameletApi} from "karavan-core/lib/api/KameletApi";
+import {TypeaheadSelect, Value} from "../../designer/ui/TypeaheadSelect";
interface Props {
types: string[],
@@ -49,9 +48,7 @@ export function CreateFileModal(props: Props) {
const [name, setName] = useState<string>('');
const [fileType, setFileType] = useState<string>();
const [kameletType, setKameletType] = useState<KameletTypes>('source');
- const [inputValue, setInputValue] = useState<string>();
- const [selectIsOpen, setSelectIsOpen] = useState<boolean>(false);
- const [selectedKamelet, setSelectedKamelet] = useState<[string, string]>(['', '']);
+ const [selectedKamelet, setSelectedKamelet] = useState<string>();
useEffect(() => {
if (props.types.length > 0) {
@@ -73,11 +70,20 @@ export function CreateFileModal(props: Props) {
if (fileType === 'INTEGRATION') {
return CamelDefinitionYaml.integrationToYaml(Integration.createNew(name, 'plain'));
} else if (fileType === 'KAMELET') {
- console.log(selectedKamelet, inputValue);
const kameletName = name + (isKamelet ? '-' + kameletType : '');
const integration = Integration.createNew(kameletName, 'kamelet');
const meta: MetadataLabels = new MetadataLabels({"camel.apache.org/kamelet.type": kameletType});
integration.metadata.labels = meta;
+ if (selectedKamelet !== undefined && selectedKamelet !== '') {
+ const kamelet= KameletApi.getKamelets().filter(k => k.metadata.name === selectedKamelet).at(0);
+ if (kamelet) {
+ (integration as any).spec = kamelet.spec;
+ (integration as any).metadata.labels = kamelet.metadata.labels;
+ (integration as any).metadata.annotations = kamelet.metadata.annotations;
+ const i = CamelUtil.cloneIntegration(integration);
+ return CamelDefinitionYaml.integrationToYaml(i);
+ }
+ }
return CamelDefinitionYaml.integrationToYaml(integration);
} else {
return '';
@@ -112,51 +118,12 @@ export function CreateFileModal(props: Props) {
: CamelUi.javaNameFromTitle(name);
const fullFileName = filename + (isKamelet ? '-' + kameletType : '') + '.' + extension;
- const selectOptions: React.JSX.Element[] = []
- KameletApi.getKamelets()
+ const listOfValues: Value[] = KameletApi.getKamelets()
.filter(k => k.metadata.labels["camel.apache.org/kamelet.type"] === kameletType)
- .filter(k => k.spec.definition.title.toLowerCase().includes(inputValue?.toLowerCase() || ''))
- .forEach((kamelet) => {
- const s = <SelectOption key={kamelet.metadata.name}
- value={kamelet.metadata.name}
- description={kamelet.spec.definition.title}
- isFocused={kamelet.metadata.name === selectedKamelet[0]}
- onClick={event => {
- setSelectedKamelet([kamelet.metadata.name, kamelet.spec.definition.title]);
- setSelectIsOpen(false);
- setInputValue(kamelet.spec.definition.title)
- }}
- />;
- selectOptions.push(s);
- })
-
-
- const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
- <MenuToggle variant={"typeahead"}
- isFullWidth
- ref={toggleRef}
- onClick={() => setSelectIsOpen(!selectIsOpen)}
- isExpanded={selectIsOpen}
- isDisabled={false}>
- <TextInputGroup isPlain>
- <TextInputGroupMain
- value={inputValue}
- onClick={event => {}}
- onChange={(event, value) => {
- setInputValue(value);
- setSelectIsOpen(true)
- }}
- id="typeahead-select-input"
- autoComplete="off"
- // innerRef={textInputRef}
- placeholder="Select Kamelet"
- role="combobox"
- isExpanded={selectIsOpen}
- aria-controls="select-typeahead-listbox"
- />
- </TextInputGroup>
- </MenuToggle>
- );
+ .map(k => {
+ const v: Value = {value: k.metadata.name, children: k.spec.definition.title}
+ return v;
+ })
return (
<Modal
@@ -191,7 +158,7 @@ export function CreateFileModal(props: Props) {
isSelected={kameletType === type}
onChange={(_, selected) => {
setKameletType(type as KameletTypes);
- setSelectedKamelet(['', ''])
+ setSelectedKamelet(undefined)
}}/>
})}
</ToggleGroup>
@@ -205,18 +172,9 @@ export function CreateFileModal(props: Props) {
</FormHelperText>
</FormGroup>
{isKamelet && <FormGroup label="Copy from" fieldId="kamelet">
- <Select
- aria-label={"Kamelet"}
- onOpenChange={isOpen => setSelectIsOpen(false)}
- isOpen={selectIsOpen}
- aria-labelledby={"Kamelets"}
- toggle={toggle}
- shouldFocusToggleOnSelect
- >
- <SelectList>
- {selectOptions}
- </SelectList>
- </Select>
+ <TypeaheadSelect listOfValues={listOfValues} onSelect={value => {
+ setSelectedKamelet(value)
+ }}/>
</FormGroup>}
</Form>
</Modal>