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 d0a18eaa Fix #1159 d0a18eaa is described below commit d0a18eaa73553f412cbc0c1c6860b4b9f96c4929 Author: Marat Gubaidullin <ma...@talismancloud.io> AuthorDate: Fri Mar 1 11:36:33 2024 -0500 Fix #1159 --- .../src/main/webui/src/designer/DesignerStore.ts | 2 +- .../webui/src/designer/route/RouteDesigner.tsx | 2 +- .../src/designer/route/useRouteDesignerHook.tsx | 2 +- .../webui/src/designer/selector/DslPreferences.tsx | 76 +++++++++++++++++ .../webui/src/designer/selector}/DslSelector.css | 20 +++++ .../webui/src/designer/selector}/DslSelector.tsx | 97 +++++++++++++++------- karavan-designer/src/designer/DesignerStore.ts | 2 +- .../src/designer/route/RouteDesigner.tsx | 2 +- .../src/designer/route/useRouteDesignerHook.tsx | 2 +- .../src/designer/selector/DslPreferences.tsx | 76 +++++++++++++++++ .../src/designer/selector}/DslSelector.css | 20 +++++ .../src/designer/selector}/DslSelector.tsx | 97 +++++++++++++++------- karavan-space/src/designer/DesignerStore.ts | 2 +- karavan-space/src/designer/route/RouteDesigner.tsx | 2 +- .../src/designer/route/useRouteDesignerHook.tsx | 2 +- .../src/designer/selector/DslPreferences.tsx | 76 +++++++++++++++++ .../src/designer/selector}/DslSelector.css | 20 +++++ .../src/designer/selector}/DslSelector.tsx | 97 +++++++++++++++------- 18 files changed, 504 insertions(+), 93 deletions(-) diff --git a/karavan-app/src/main/webui/src/designer/DesignerStore.ts b/karavan-app/src/main/webui/src/designer/DesignerStore.ts index e08d9a19..c19038ad 100644 --- a/karavan-app/src/main/webui/src/designer/DesignerStore.ts +++ b/karavan-app/src/main/webui/src/designer/DesignerStore.ts @@ -130,7 +130,7 @@ export const useSelectorStore = createWithEqualityFn<SelectorStateState>((set) = clearSelectedLabels: () => { set((state: SelectorStateState) => { state.selectedLabels.length = 0; - return state; + return {selectedLabels : [... state.selectedLabels]}; }) }, setSelectedLabels: (selectedLabels: string []) => { diff --git a/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx b/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx index 77678b3a..9ef5e827 100644 --- a/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx +++ b/karavan-app/src/main/webui/src/designer/route/RouteDesigner.tsx @@ -23,7 +23,7 @@ import { Button } from '@patternfly/react-core'; import '../karavan.css'; -import {DslSelector} from "./DslSelector"; +import {DslSelector} from "../selector/DslSelector"; import {DslProperties} from "../property/DslProperties"; import {DslConnections} from "./DslConnections"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; diff --git a/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx b/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx index 543f2a00..0f9b7236 100644 --- a/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx +++ b/karavan-app/src/main/webui/src/designer/route/useRouteDesignerHook.tsx @@ -226,7 +226,7 @@ export function useRouteDesignerHook () { setParentDsl(parentDsl); setShowSteps(showSteps); setSelectedPosition(position); - setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'kamelet' : 'eip'); + setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'components' : 'eip'); } function onDslSelect (dsl: DslMetaModel, parentId: string, position?: number | undefined) { diff --git a/karavan-app/src/main/webui/src/designer/selector/DslPreferences.tsx b/karavan-app/src/main/webui/src/designer/selector/DslPreferences.tsx new file mode 100644 index 00000000..87844b5a --- /dev/null +++ b/karavan-app/src/main/webui/src/designer/selector/DslPreferences.tsx @@ -0,0 +1,76 @@ +/* + * 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 {DslMetaModel} from "../utils/DslMetaModel"; + +export class PreferredElements { + eip: PreferredElement[] = []; + components: PreferredElement[] = []; + kamelets: PreferredElement[] = []; + + public constructor(init?: Partial<PreferredElements>) { + Object.assign(this, init); + } +} + +export class PreferredElement { + dslKey: string = ''; + count: number = 0; + + public constructor(init?: Partial<PreferredElement>) { + Object.assign(this, init); + } +} + +const PREFERRED_ELEMENTS_STORAGE_NAME = 'PREFERRED_ELEMENTS' + +function addCount(pe: PreferredElement): PreferredElement { + return new PreferredElement({dslKey: pe.dslKey, count: pe.count + 1}); +} + +export function getPreferredElements(type: 'eip' | 'components' | 'kamelets'): string[] { + const result: string[] = []; + try { + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + if (local !== null) { + const pes = new PreferredElements(JSON.parse(local)); + (pes as any)[type].forEach((pe: PreferredElement) => result.push(pe.dslKey)); + } + } catch (e) { + console.log(e); + } + return result; +} + +export function addPreferredElement(type: 'eip' | 'components' | 'kamelets', dsl: DslMetaModel) { + try { + const dslKey = type === 'eip' ? dsl.dsl : (type === 'components' ? dsl.uri : dsl.name); + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + const pes = local !== null ? new PreferredElements(JSON.parse(local)) : new PreferredElements(); + let list: PreferredElement[] = (pes as any)[type]; + if (list.findIndex(pe => pe.dslKey === dslKey) !== -1) { + list = list.map(pe => pe.dslKey === dslKey ? addCount(pe) : pe); + } else { + list.push(new PreferredElement({dslKey: dslKey, count: 1})) + } + list = list.sort((a, b) => b.count - a.count).filter((_, i) => i < 20); + (pes as any)[type] = [...list]; + localStorage.setItem(PREFERRED_ELEMENTS_STORAGE_NAME, JSON.stringify(pes)); + } catch (e) { + console.log(e); + } +} diff --git a/karavan-designer/src/designer/route/DslSelector.css b/karavan-app/src/main/webui/src/designer/selector/DslSelector.css similarity index 86% copy from karavan-designer/src/designer/route/DslSelector.css copy to karavan-app/src/main/webui/src/designer/selector/DslSelector.css index 4bca7774..279c1f41 100644 --- a/karavan-designer/src/designer/route/DslSelector.css +++ b/karavan-app/src/main/webui/src/designer/selector/DslSelector.css @@ -79,6 +79,7 @@ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + color: var(--pf-v5-global--Color--200); } .dsl-modal .pf-v5-c-card__footer { padding-bottom: 1em; @@ -91,6 +92,11 @@ flex-wrap: wrap; } +.dsl-modal .dsl-card .dsl-card-title { + color: var(--pf-v5-global--Color--100); + font-weight: bold; +} + .dsl-modal .dsl-card .header-labels { padding: 5px; display: flex; @@ -120,3 +126,17 @@ .dsl-modal .dsl-card:hover .labels { opacity: 1; } + +.dsl-modal .dsl-card:hover p { + color: var(--pf-v5-global--Color--100); +} + +.dsl-modal .dsl-fast-card .header p { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.dsl-modal .dsl-fast-card .header { + padding: 10px; +} diff --git a/karavan-designer/src/designer/route/DslSelector.tsx b/karavan-app/src/main/webui/src/designer/selector/DslSelector.tsx similarity index 71% copy from karavan-designer/src/designer/route/DslSelector.tsx copy to karavan-app/src/main/webui/src/designer/selector/DslSelector.tsx index c7dde3e7..fac2a6c6 100644 --- a/karavan-designer/src/designer/route/DslSelector.tsx +++ b/karavan-app/src/main/webui/src/designer/selector/DslSelector.tsx @@ -17,38 +17,37 @@ import React, {useEffect, useState} from 'react'; import { Badge, + Button, Card, CardBody, CardFooter, CardHeader, Flex, FlexItem, - Form, - FormGroup, Gallery, Modal, PageSection, + Switch, Tab, Tabs, TabTitleText, Text, - ToggleGroup, - ToggleGroupItem, - Switch, TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, - Button + ToggleGroup, + ToggleGroupItem } from '@patternfly/react-core'; import './DslSelector.css'; import {CamelUi} from "../utils/CamelUi"; import {DslMetaModel} from "../utils/DslMetaModel"; import {useDesignerStore, useSelectorStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; -import {useRouteDesignerHook} from "./useRouteDesignerHook"; -import { ComponentApi } from 'karavan-core/lib/api/ComponentApi'; -import { KameletApi } from 'karavan-core/lib/api/KameletApi'; +import {useRouteDesignerHook} from "../route/useRouteDesignerHook"; +import {ComponentApi} from 'karavan-core/lib/api/ComponentApi'; +import {KameletApi} from 'karavan-core/lib/api/KameletApi'; import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; +import {addPreferredElement, getPreferredElements} from "./DslPreferences"; interface Props { tabIndex?: string | number @@ -57,20 +56,25 @@ interface Props { export function DslSelector (props: Props) { const [showSelector, showSteps, parentId, parentDsl, selectorTabIndex, setShowSelector, setSelectorTabIndex, - selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel] = + selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel, clearSelectedLabels] = useSelectorStore((s) => [s.showSelector, s.showSteps, s.parentId, s.parentDsl, s.selectorTabIndex, s.setShowSelector, s.setSelectorTabIndex, - s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel], shallow) + s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel, s.clearSelectedLabels], shallow) const [dark] = useDesignerStore((s) => [s.dark], shallow) const {onDslSelect} = useRouteDesignerHook(); - const [filter, setFilter] = useState<string>(''); const [customOnly, setCustomOnly] = useState<boolean>(false); + const [preferredEip, setPreferredEip] = useState<string[]>([]); + const [preferredComponents, setPreferredComponents] = useState<string[]>([]); + const [preferredKamelets, setPreferredKamelets] = useState<string[]>([]); useEffect(() => { + setPreferredEip(getPreferredElements('eip')); + setPreferredComponents(getPreferredElements('components')); + setPreferredKamelets(getPreferredElements('kamelets')); }, [selectedLabels]); @@ -83,12 +87,14 @@ export function DslSelector (props: Props) { setFilter(''); setShowSelector(false); onDslSelect(dsl, parentId, selectedPosition); + const type = isEip ? 'eip' : (selectorTabIndex === 'components' ? 'components' : 'kamelets'); + addPreferredElement(type, dsl) } function searchInput() { return ( <Flex className="search"> - {selectorTabIndex === 'kamelet' && <FlexItem> + {selectorTabIndex === 'kamelets' && <FlexItem> <Switch label="Custom only" id="switch" @@ -127,10 +133,11 @@ export function DslSelector (props: Props) { </CardHeader> <CardHeader> {CamelUi.getIconForDsl(dsl)} - <Text>{dsl.title}</Text> + <Text className='dsl-card-title'>{dsl.title}</Text> </CardHeader> <CardBody> - <Text>{dsl.description}</Text> + {/*<Text>{dsl.description}</Text>*/} + <Text className="pf-v5-u-color-200">{dsl.description}</Text> </CardBody> <CardFooter className="footer-labels"> <div style={{display: "flex", flexDirection: "row", justifyContent: "start"}}> @@ -143,6 +150,18 @@ export function DslSelector (props: Props) { ) } + function getFastCard(dsl: DslMetaModel, index: number) { + return ( + <Card key={dsl.dsl + index} isCompact className="dsl-card dsl-fast-card" + onClick={event => selectDsl(event, dsl)}> + <CardHeader className='header'> + {CamelUi.getIconForDsl(dsl)} + <Text className='dsl-fast-card-title'>{dsl.title}</Text> + </CardHeader> + </Card> + ) + } + function close() { setFilter(''); setShowSelector(false); @@ -156,10 +175,10 @@ export function DslSelector (props: Props) { } } - function filterElements(elements: DslMetaModel[]):DslMetaModel[] { + function filterElements(elements: DslMetaModel[], type: 'eip' | 'components' | 'kamelets'):DslMetaModel[] { return elements.filter((dsl: DslMetaModel) => CamelUi.checkFilter(dsl, filter)) .filter((dsl: DslMetaModel) => { - if (!isEip || selectedLabels.length === 0) { + if (type !== 'eip' || selectedLabels.length === 0) { return true; } else { return dsl.labels.split(",").some(r => selectedLabels.includes(r)); @@ -181,16 +200,24 @@ export function DslSelector (props: Props) { .filter(dsl => (!blockedKamelets.includes(dsl.name))); if (customOnly) kameletElements = kameletElements.filter(k => KameletApi.getCustomKameletNames().includes(k.name)); - const filteredEipElements = filterElements(eipElements); - const filteredComponentElements = filterElements(componentElements); - const filteredKameletElements = filterElements(kameletElements); + const elements = navigation === 'components' + ? componentElements + : (navigation === 'kamelets' ? kameletElements : eipElements); + + const preferredElements = navigation === 'components' + ? preferredComponents + : (navigation === 'kamelets' ? preferredKamelets : preferredEip); + + const filteredEipElements = filterElements(eipElements, 'eip'); + const filteredComponentElements = filterElements(componentElements, 'components'); + const filteredKameletElements = filterElements(kameletElements, 'kamelets'); const eipLabels = [...new Set(eipElements.map(e => e.labels).join(",").split(",").filter(e => e !== 'eip'))]; - const filteredElement = navigation === 'component' + const filteredElements = navigation === 'components' ? filteredComponentElements - : (navigation === 'kamelet' ? filteredKameletElements : filteredEipElements); + : (navigation === 'kamelets' ? filteredKameletElements : filteredEipElements); console.log(parentDsl) @@ -216,15 +243,15 @@ export function DslSelector (props: Props) { </Tab> } {!isRouteConfig && - <Tab eventKey={'kamelet'} key={"tab-kamelet"} + <Tab eventKey={'components'} key={'tab-component'} title={ - <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> </Tab> } {!isRouteConfig && - <Tab eventKey={'component'} key={'tab-component'} + <Tab eventKey={'kamelets'} key={"tab-kamelet"} title={ - <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> </Tab> } </Tabs> @@ -241,9 +268,23 @@ export function DslSelector (props: Props) { isSelected={selectedLabels.includes(eipLabel)} onChange={selected => selectLabel(eipLabel)} />)} + <ToggleGroupItem key='clean' buttonId='clean' isSelected={false} onChange={clearSelectedLabels} icon={<TimesIcon/>}/> </ToggleGroup>} - <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery"> - {showSelector && filteredElement.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} + <Gallery key={"fast-gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '150px'}}> + {showSelector && elements + .filter((d: DslMetaModel) => { + if (isEip) { + return preferredElements.includes(d.dsl); + } else if (navigation === 'components') { + return d.uri && preferredElements.includes(d.uri) + } else { + return preferredElements.includes(d.name) + } + }) + .filter((_, i) => i < 7).map((dsl: DslMetaModel, index: number) => getFastCard(dsl, index))} + </Gallery> + <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '200px'}}> + {showSelector && filteredElements.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} </Gallery> </PageSection> </Modal> diff --git a/karavan-designer/src/designer/DesignerStore.ts b/karavan-designer/src/designer/DesignerStore.ts index e08d9a19..c19038ad 100644 --- a/karavan-designer/src/designer/DesignerStore.ts +++ b/karavan-designer/src/designer/DesignerStore.ts @@ -130,7 +130,7 @@ export const useSelectorStore = createWithEqualityFn<SelectorStateState>((set) = clearSelectedLabels: () => { set((state: SelectorStateState) => { state.selectedLabels.length = 0; - return state; + return {selectedLabels : [... state.selectedLabels]}; }) }, setSelectedLabels: (selectedLabels: string []) => { diff --git a/karavan-designer/src/designer/route/RouteDesigner.tsx b/karavan-designer/src/designer/route/RouteDesigner.tsx index 77678b3a..9ef5e827 100644 --- a/karavan-designer/src/designer/route/RouteDesigner.tsx +++ b/karavan-designer/src/designer/route/RouteDesigner.tsx @@ -23,7 +23,7 @@ import { Button } from '@patternfly/react-core'; import '../karavan.css'; -import {DslSelector} from "./DslSelector"; +import {DslSelector} from "../selector/DslSelector"; import {DslProperties} from "../property/DslProperties"; import {DslConnections} from "./DslConnections"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; diff --git a/karavan-designer/src/designer/route/useRouteDesignerHook.tsx b/karavan-designer/src/designer/route/useRouteDesignerHook.tsx index 543f2a00..0f9b7236 100644 --- a/karavan-designer/src/designer/route/useRouteDesignerHook.tsx +++ b/karavan-designer/src/designer/route/useRouteDesignerHook.tsx @@ -226,7 +226,7 @@ export function useRouteDesignerHook () { setParentDsl(parentDsl); setShowSteps(showSteps); setSelectedPosition(position); - setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'kamelet' : 'eip'); + setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'components' : 'eip'); } function onDslSelect (dsl: DslMetaModel, parentId: string, position?: number | undefined) { diff --git a/karavan-designer/src/designer/selector/DslPreferences.tsx b/karavan-designer/src/designer/selector/DslPreferences.tsx new file mode 100644 index 00000000..87844b5a --- /dev/null +++ b/karavan-designer/src/designer/selector/DslPreferences.tsx @@ -0,0 +1,76 @@ +/* + * 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 {DslMetaModel} from "../utils/DslMetaModel"; + +export class PreferredElements { + eip: PreferredElement[] = []; + components: PreferredElement[] = []; + kamelets: PreferredElement[] = []; + + public constructor(init?: Partial<PreferredElements>) { + Object.assign(this, init); + } +} + +export class PreferredElement { + dslKey: string = ''; + count: number = 0; + + public constructor(init?: Partial<PreferredElement>) { + Object.assign(this, init); + } +} + +const PREFERRED_ELEMENTS_STORAGE_NAME = 'PREFERRED_ELEMENTS' + +function addCount(pe: PreferredElement): PreferredElement { + return new PreferredElement({dslKey: pe.dslKey, count: pe.count + 1}); +} + +export function getPreferredElements(type: 'eip' | 'components' | 'kamelets'): string[] { + const result: string[] = []; + try { + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + if (local !== null) { + const pes = new PreferredElements(JSON.parse(local)); + (pes as any)[type].forEach((pe: PreferredElement) => result.push(pe.dslKey)); + } + } catch (e) { + console.log(e); + } + return result; +} + +export function addPreferredElement(type: 'eip' | 'components' | 'kamelets', dsl: DslMetaModel) { + try { + const dslKey = type === 'eip' ? dsl.dsl : (type === 'components' ? dsl.uri : dsl.name); + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + const pes = local !== null ? new PreferredElements(JSON.parse(local)) : new PreferredElements(); + let list: PreferredElement[] = (pes as any)[type]; + if (list.findIndex(pe => pe.dslKey === dslKey) !== -1) { + list = list.map(pe => pe.dslKey === dslKey ? addCount(pe) : pe); + } else { + list.push(new PreferredElement({dslKey: dslKey, count: 1})) + } + list = list.sort((a, b) => b.count - a.count).filter((_, i) => i < 20); + (pes as any)[type] = [...list]; + localStorage.setItem(PREFERRED_ELEMENTS_STORAGE_NAME, JSON.stringify(pes)); + } catch (e) { + console.log(e); + } +} diff --git a/karavan-app/src/main/webui/src/designer/route/DslSelector.css b/karavan-designer/src/designer/selector/DslSelector.css similarity index 86% rename from karavan-app/src/main/webui/src/designer/route/DslSelector.css rename to karavan-designer/src/designer/selector/DslSelector.css index 4bca7774..279c1f41 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslSelector.css +++ b/karavan-designer/src/designer/selector/DslSelector.css @@ -79,6 +79,7 @@ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + color: var(--pf-v5-global--Color--200); } .dsl-modal .pf-v5-c-card__footer { padding-bottom: 1em; @@ -91,6 +92,11 @@ flex-wrap: wrap; } +.dsl-modal .dsl-card .dsl-card-title { + color: var(--pf-v5-global--Color--100); + font-weight: bold; +} + .dsl-modal .dsl-card .header-labels { padding: 5px; display: flex; @@ -120,3 +126,17 @@ .dsl-modal .dsl-card:hover .labels { opacity: 1; } + +.dsl-modal .dsl-card:hover p { + color: var(--pf-v5-global--Color--100); +} + +.dsl-modal .dsl-fast-card .header p { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.dsl-modal .dsl-fast-card .header { + padding: 10px; +} diff --git a/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx b/karavan-designer/src/designer/selector/DslSelector.tsx similarity index 71% rename from karavan-app/src/main/webui/src/designer/route/DslSelector.tsx rename to karavan-designer/src/designer/selector/DslSelector.tsx index c7dde3e7..fac2a6c6 100644 --- a/karavan-app/src/main/webui/src/designer/route/DslSelector.tsx +++ b/karavan-designer/src/designer/selector/DslSelector.tsx @@ -17,38 +17,37 @@ import React, {useEffect, useState} from 'react'; import { Badge, + Button, Card, CardBody, CardFooter, CardHeader, Flex, FlexItem, - Form, - FormGroup, Gallery, Modal, PageSection, + Switch, Tab, Tabs, TabTitleText, Text, - ToggleGroup, - ToggleGroupItem, - Switch, TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, - Button + ToggleGroup, + ToggleGroupItem } from '@patternfly/react-core'; import './DslSelector.css'; import {CamelUi} from "../utils/CamelUi"; import {DslMetaModel} from "../utils/DslMetaModel"; import {useDesignerStore, useSelectorStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; -import {useRouteDesignerHook} from "./useRouteDesignerHook"; -import { ComponentApi } from 'karavan-core/lib/api/ComponentApi'; -import { KameletApi } from 'karavan-core/lib/api/KameletApi'; +import {useRouteDesignerHook} from "../route/useRouteDesignerHook"; +import {ComponentApi} from 'karavan-core/lib/api/ComponentApi'; +import {KameletApi} from 'karavan-core/lib/api/KameletApi'; import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; +import {addPreferredElement, getPreferredElements} from "./DslPreferences"; interface Props { tabIndex?: string | number @@ -57,20 +56,25 @@ interface Props { export function DslSelector (props: Props) { const [showSelector, showSteps, parentId, parentDsl, selectorTabIndex, setShowSelector, setSelectorTabIndex, - selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel] = + selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel, clearSelectedLabels] = useSelectorStore((s) => [s.showSelector, s.showSteps, s.parentId, s.parentDsl, s.selectorTabIndex, s.setShowSelector, s.setSelectorTabIndex, - s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel], shallow) + s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel, s.clearSelectedLabels], shallow) const [dark] = useDesignerStore((s) => [s.dark], shallow) const {onDslSelect} = useRouteDesignerHook(); - const [filter, setFilter] = useState<string>(''); const [customOnly, setCustomOnly] = useState<boolean>(false); + const [preferredEip, setPreferredEip] = useState<string[]>([]); + const [preferredComponents, setPreferredComponents] = useState<string[]>([]); + const [preferredKamelets, setPreferredKamelets] = useState<string[]>([]); useEffect(() => { + setPreferredEip(getPreferredElements('eip')); + setPreferredComponents(getPreferredElements('components')); + setPreferredKamelets(getPreferredElements('kamelets')); }, [selectedLabels]); @@ -83,12 +87,14 @@ export function DslSelector (props: Props) { setFilter(''); setShowSelector(false); onDslSelect(dsl, parentId, selectedPosition); + const type = isEip ? 'eip' : (selectorTabIndex === 'components' ? 'components' : 'kamelets'); + addPreferredElement(type, dsl) } function searchInput() { return ( <Flex className="search"> - {selectorTabIndex === 'kamelet' && <FlexItem> + {selectorTabIndex === 'kamelets' && <FlexItem> <Switch label="Custom only" id="switch" @@ -127,10 +133,11 @@ export function DslSelector (props: Props) { </CardHeader> <CardHeader> {CamelUi.getIconForDsl(dsl)} - <Text>{dsl.title}</Text> + <Text className='dsl-card-title'>{dsl.title}</Text> </CardHeader> <CardBody> - <Text>{dsl.description}</Text> + {/*<Text>{dsl.description}</Text>*/} + <Text className="pf-v5-u-color-200">{dsl.description}</Text> </CardBody> <CardFooter className="footer-labels"> <div style={{display: "flex", flexDirection: "row", justifyContent: "start"}}> @@ -143,6 +150,18 @@ export function DslSelector (props: Props) { ) } + function getFastCard(dsl: DslMetaModel, index: number) { + return ( + <Card key={dsl.dsl + index} isCompact className="dsl-card dsl-fast-card" + onClick={event => selectDsl(event, dsl)}> + <CardHeader className='header'> + {CamelUi.getIconForDsl(dsl)} + <Text className='dsl-fast-card-title'>{dsl.title}</Text> + </CardHeader> + </Card> + ) + } + function close() { setFilter(''); setShowSelector(false); @@ -156,10 +175,10 @@ export function DslSelector (props: Props) { } } - function filterElements(elements: DslMetaModel[]):DslMetaModel[] { + function filterElements(elements: DslMetaModel[], type: 'eip' | 'components' | 'kamelets'):DslMetaModel[] { return elements.filter((dsl: DslMetaModel) => CamelUi.checkFilter(dsl, filter)) .filter((dsl: DslMetaModel) => { - if (!isEip || selectedLabels.length === 0) { + if (type !== 'eip' || selectedLabels.length === 0) { return true; } else { return dsl.labels.split(",").some(r => selectedLabels.includes(r)); @@ -181,16 +200,24 @@ export function DslSelector (props: Props) { .filter(dsl => (!blockedKamelets.includes(dsl.name))); if (customOnly) kameletElements = kameletElements.filter(k => KameletApi.getCustomKameletNames().includes(k.name)); - const filteredEipElements = filterElements(eipElements); - const filteredComponentElements = filterElements(componentElements); - const filteredKameletElements = filterElements(kameletElements); + const elements = navigation === 'components' + ? componentElements + : (navigation === 'kamelets' ? kameletElements : eipElements); + + const preferredElements = navigation === 'components' + ? preferredComponents + : (navigation === 'kamelets' ? preferredKamelets : preferredEip); + + const filteredEipElements = filterElements(eipElements, 'eip'); + const filteredComponentElements = filterElements(componentElements, 'components'); + const filteredKameletElements = filterElements(kameletElements, 'kamelets'); const eipLabels = [...new Set(eipElements.map(e => e.labels).join(",").split(",").filter(e => e !== 'eip'))]; - const filteredElement = navigation === 'component' + const filteredElements = navigation === 'components' ? filteredComponentElements - : (navigation === 'kamelet' ? filteredKameletElements : filteredEipElements); + : (navigation === 'kamelets' ? filteredKameletElements : filteredEipElements); console.log(parentDsl) @@ -216,15 +243,15 @@ export function DslSelector (props: Props) { </Tab> } {!isRouteConfig && - <Tab eventKey={'kamelet'} key={"tab-kamelet"} + <Tab eventKey={'components'} key={'tab-component'} title={ - <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> </Tab> } {!isRouteConfig && - <Tab eventKey={'component'} key={'tab-component'} + <Tab eventKey={'kamelets'} key={"tab-kamelet"} title={ - <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> </Tab> } </Tabs> @@ -241,9 +268,23 @@ export function DslSelector (props: Props) { isSelected={selectedLabels.includes(eipLabel)} onChange={selected => selectLabel(eipLabel)} />)} + <ToggleGroupItem key='clean' buttonId='clean' isSelected={false} onChange={clearSelectedLabels} icon={<TimesIcon/>}/> </ToggleGroup>} - <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery"> - {showSelector && filteredElement.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} + <Gallery key={"fast-gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '150px'}}> + {showSelector && elements + .filter((d: DslMetaModel) => { + if (isEip) { + return preferredElements.includes(d.dsl); + } else if (navigation === 'components') { + return d.uri && preferredElements.includes(d.uri) + } else { + return preferredElements.includes(d.name) + } + }) + .filter((_, i) => i < 7).map((dsl: DslMetaModel, index: number) => getFastCard(dsl, index))} + </Gallery> + <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '200px'}}> + {showSelector && filteredElements.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} </Gallery> </PageSection> </Modal> diff --git a/karavan-space/src/designer/DesignerStore.ts b/karavan-space/src/designer/DesignerStore.ts index e08d9a19..c19038ad 100644 --- a/karavan-space/src/designer/DesignerStore.ts +++ b/karavan-space/src/designer/DesignerStore.ts @@ -130,7 +130,7 @@ export const useSelectorStore = createWithEqualityFn<SelectorStateState>((set) = clearSelectedLabels: () => { set((state: SelectorStateState) => { state.selectedLabels.length = 0; - return state; + return {selectedLabels : [... state.selectedLabels]}; }) }, setSelectedLabels: (selectedLabels: string []) => { diff --git a/karavan-space/src/designer/route/RouteDesigner.tsx b/karavan-space/src/designer/route/RouteDesigner.tsx index 77678b3a..9ef5e827 100644 --- a/karavan-space/src/designer/route/RouteDesigner.tsx +++ b/karavan-space/src/designer/route/RouteDesigner.tsx @@ -23,7 +23,7 @@ import { Button } from '@patternfly/react-core'; import '../karavan.css'; -import {DslSelector} from "./DslSelector"; +import {DslSelector} from "../selector/DslSelector"; import {DslProperties} from "../property/DslProperties"; import {DslConnections} from "./DslConnections"; import PlusIcon from "@patternfly/react-icons/dist/esm/icons/plus-icon"; diff --git a/karavan-space/src/designer/route/useRouteDesignerHook.tsx b/karavan-space/src/designer/route/useRouteDesignerHook.tsx index 543f2a00..0f9b7236 100644 --- a/karavan-space/src/designer/route/useRouteDesignerHook.tsx +++ b/karavan-space/src/designer/route/useRouteDesignerHook.tsx @@ -226,7 +226,7 @@ export function useRouteDesignerHook () { setParentDsl(parentDsl); setShowSteps(showSteps); setSelectedPosition(position); - setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'kamelet' : 'eip'); + setSelectorTabIndex((parentId === undefined && parentDsl === undefined) ? 'components' : 'eip'); } function onDslSelect (dsl: DslMetaModel, parentId: string, position?: number | undefined) { diff --git a/karavan-space/src/designer/selector/DslPreferences.tsx b/karavan-space/src/designer/selector/DslPreferences.tsx new file mode 100644 index 00000000..87844b5a --- /dev/null +++ b/karavan-space/src/designer/selector/DslPreferences.tsx @@ -0,0 +1,76 @@ +/* + * 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 {DslMetaModel} from "../utils/DslMetaModel"; + +export class PreferredElements { + eip: PreferredElement[] = []; + components: PreferredElement[] = []; + kamelets: PreferredElement[] = []; + + public constructor(init?: Partial<PreferredElements>) { + Object.assign(this, init); + } +} + +export class PreferredElement { + dslKey: string = ''; + count: number = 0; + + public constructor(init?: Partial<PreferredElement>) { + Object.assign(this, init); + } +} + +const PREFERRED_ELEMENTS_STORAGE_NAME = 'PREFERRED_ELEMENTS' + +function addCount(pe: PreferredElement): PreferredElement { + return new PreferredElement({dslKey: pe.dslKey, count: pe.count + 1}); +} + +export function getPreferredElements(type: 'eip' | 'components' | 'kamelets'): string[] { + const result: string[] = []; + try { + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + if (local !== null) { + const pes = new PreferredElements(JSON.parse(local)); + (pes as any)[type].forEach((pe: PreferredElement) => result.push(pe.dslKey)); + } + } catch (e) { + console.log(e); + } + return result; +} + +export function addPreferredElement(type: 'eip' | 'components' | 'kamelets', dsl: DslMetaModel) { + try { + const dslKey = type === 'eip' ? dsl.dsl : (type === 'components' ? dsl.uri : dsl.name); + const local = localStorage.getItem(PREFERRED_ELEMENTS_STORAGE_NAME); + const pes = local !== null ? new PreferredElements(JSON.parse(local)) : new PreferredElements(); + let list: PreferredElement[] = (pes as any)[type]; + if (list.findIndex(pe => pe.dslKey === dslKey) !== -1) { + list = list.map(pe => pe.dslKey === dslKey ? addCount(pe) : pe); + } else { + list.push(new PreferredElement({dslKey: dslKey, count: 1})) + } + list = list.sort((a, b) => b.count - a.count).filter((_, i) => i < 20); + (pes as any)[type] = [...list]; + localStorage.setItem(PREFERRED_ELEMENTS_STORAGE_NAME, JSON.stringify(pes)); + } catch (e) { + console.log(e); + } +} diff --git a/karavan-designer/src/designer/route/DslSelector.css b/karavan-space/src/designer/selector/DslSelector.css similarity index 86% rename from karavan-designer/src/designer/route/DslSelector.css rename to karavan-space/src/designer/selector/DslSelector.css index 4bca7774..279c1f41 100644 --- a/karavan-designer/src/designer/route/DslSelector.css +++ b/karavan-space/src/designer/selector/DslSelector.css @@ -79,6 +79,7 @@ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + color: var(--pf-v5-global--Color--200); } .dsl-modal .pf-v5-c-card__footer { padding-bottom: 1em; @@ -91,6 +92,11 @@ flex-wrap: wrap; } +.dsl-modal .dsl-card .dsl-card-title { + color: var(--pf-v5-global--Color--100); + font-weight: bold; +} + .dsl-modal .dsl-card .header-labels { padding: 5px; display: flex; @@ -120,3 +126,17 @@ .dsl-modal .dsl-card:hover .labels { opacity: 1; } + +.dsl-modal .dsl-card:hover p { + color: var(--pf-v5-global--Color--100); +} + +.dsl-modal .dsl-fast-card .header p { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.dsl-modal .dsl-fast-card .header { + padding: 10px; +} diff --git a/karavan-designer/src/designer/route/DslSelector.tsx b/karavan-space/src/designer/selector/DslSelector.tsx similarity index 71% rename from karavan-designer/src/designer/route/DslSelector.tsx rename to karavan-space/src/designer/selector/DslSelector.tsx index c7dde3e7..fac2a6c6 100644 --- a/karavan-designer/src/designer/route/DslSelector.tsx +++ b/karavan-space/src/designer/selector/DslSelector.tsx @@ -17,38 +17,37 @@ import React, {useEffect, useState} from 'react'; import { Badge, + Button, Card, CardBody, CardFooter, CardHeader, Flex, FlexItem, - Form, - FormGroup, Gallery, Modal, PageSection, + Switch, Tab, Tabs, TabTitleText, Text, - ToggleGroup, - ToggleGroupItem, - Switch, TextInputGroup, TextInputGroupMain, TextInputGroupUtilities, - Button + ToggleGroup, + ToggleGroupItem } from '@patternfly/react-core'; import './DslSelector.css'; import {CamelUi} from "../utils/CamelUi"; import {DslMetaModel} from "../utils/DslMetaModel"; import {useDesignerStore, useSelectorStore} from "../DesignerStore"; import {shallow} from "zustand/shallow"; -import {useRouteDesignerHook} from "./useRouteDesignerHook"; -import { ComponentApi } from 'karavan-core/lib/api/ComponentApi'; -import { KameletApi } from 'karavan-core/lib/api/KameletApi'; +import {useRouteDesignerHook} from "../route/useRouteDesignerHook"; +import {ComponentApi} from 'karavan-core/lib/api/ComponentApi'; +import {KameletApi} from 'karavan-core/lib/api/KameletApi'; import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; +import {addPreferredElement, getPreferredElements} from "./DslPreferences"; interface Props { tabIndex?: string | number @@ -57,20 +56,25 @@ interface Props { export function DslSelector (props: Props) { const [showSelector, showSteps, parentId, parentDsl, selectorTabIndex, setShowSelector, setSelectorTabIndex, - selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel] = + selectedPosition, selectedLabels, addSelectedLabel, deleteSelectedLabel, clearSelectedLabels] = useSelectorStore((s) => [s.showSelector, s.showSteps, s.parentId, s.parentDsl, s.selectorTabIndex, s.setShowSelector, s.setSelectorTabIndex, - s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel], shallow) + s.selectedPosition, s.selectedLabels, s.addSelectedLabel, s.deleteSelectedLabel, s.clearSelectedLabels], shallow) const [dark] = useDesignerStore((s) => [s.dark], shallow) const {onDslSelect} = useRouteDesignerHook(); - const [filter, setFilter] = useState<string>(''); const [customOnly, setCustomOnly] = useState<boolean>(false); + const [preferredEip, setPreferredEip] = useState<string[]>([]); + const [preferredComponents, setPreferredComponents] = useState<string[]>([]); + const [preferredKamelets, setPreferredKamelets] = useState<string[]>([]); useEffect(() => { + setPreferredEip(getPreferredElements('eip')); + setPreferredComponents(getPreferredElements('components')); + setPreferredKamelets(getPreferredElements('kamelets')); }, [selectedLabels]); @@ -83,12 +87,14 @@ export function DslSelector (props: Props) { setFilter(''); setShowSelector(false); onDslSelect(dsl, parentId, selectedPosition); + const type = isEip ? 'eip' : (selectorTabIndex === 'components' ? 'components' : 'kamelets'); + addPreferredElement(type, dsl) } function searchInput() { return ( <Flex className="search"> - {selectorTabIndex === 'kamelet' && <FlexItem> + {selectorTabIndex === 'kamelets' && <FlexItem> <Switch label="Custom only" id="switch" @@ -127,10 +133,11 @@ export function DslSelector (props: Props) { </CardHeader> <CardHeader> {CamelUi.getIconForDsl(dsl)} - <Text>{dsl.title}</Text> + <Text className='dsl-card-title'>{dsl.title}</Text> </CardHeader> <CardBody> - <Text>{dsl.description}</Text> + {/*<Text>{dsl.description}</Text>*/} + <Text className="pf-v5-u-color-200">{dsl.description}</Text> </CardBody> <CardFooter className="footer-labels"> <div style={{display: "flex", flexDirection: "row", justifyContent: "start"}}> @@ -143,6 +150,18 @@ export function DslSelector (props: Props) { ) } + function getFastCard(dsl: DslMetaModel, index: number) { + return ( + <Card key={dsl.dsl + index} isCompact className="dsl-card dsl-fast-card" + onClick={event => selectDsl(event, dsl)}> + <CardHeader className='header'> + {CamelUi.getIconForDsl(dsl)} + <Text className='dsl-fast-card-title'>{dsl.title}</Text> + </CardHeader> + </Card> + ) + } + function close() { setFilter(''); setShowSelector(false); @@ -156,10 +175,10 @@ export function DslSelector (props: Props) { } } - function filterElements(elements: DslMetaModel[]):DslMetaModel[] { + function filterElements(elements: DslMetaModel[], type: 'eip' | 'components' | 'kamelets'):DslMetaModel[] { return elements.filter((dsl: DslMetaModel) => CamelUi.checkFilter(dsl, filter)) .filter((dsl: DslMetaModel) => { - if (!isEip || selectedLabels.length === 0) { + if (type !== 'eip' || selectedLabels.length === 0) { return true; } else { return dsl.labels.split(",").some(r => selectedLabels.includes(r)); @@ -181,16 +200,24 @@ export function DslSelector (props: Props) { .filter(dsl => (!blockedKamelets.includes(dsl.name))); if (customOnly) kameletElements = kameletElements.filter(k => KameletApi.getCustomKameletNames().includes(k.name)); - const filteredEipElements = filterElements(eipElements); - const filteredComponentElements = filterElements(componentElements); - const filteredKameletElements = filterElements(kameletElements); + const elements = navigation === 'components' + ? componentElements + : (navigation === 'kamelets' ? kameletElements : eipElements); + + const preferredElements = navigation === 'components' + ? preferredComponents + : (navigation === 'kamelets' ? preferredKamelets : preferredEip); + + const filteredEipElements = filterElements(eipElements, 'eip'); + const filteredComponentElements = filterElements(componentElements, 'components'); + const filteredKameletElements = filterElements(kameletElements, 'kamelets'); const eipLabels = [...new Set(eipElements.map(e => e.labels).join(",").split(",").filter(e => e !== 'eip'))]; - const filteredElement = navigation === 'component' + const filteredElements = navigation === 'components' ? filteredComponentElements - : (navigation === 'kamelet' ? filteredKameletElements : filteredEipElements); + : (navigation === 'kamelets' ? filteredKameletElements : filteredEipElements); console.log(parentDsl) @@ -216,15 +243,15 @@ export function DslSelector (props: Props) { </Tab> } {!isRouteConfig && - <Tab eventKey={'kamelet'} key={"tab-kamelet"} + <Tab eventKey={'components'} key={'tab-component'} title={ - <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> </Tab> } {!isRouteConfig && - <Tab eventKey={'component'} key={'tab-component'} + <Tab eventKey={'kamelets'} key={"tab-kamelet"} title={ - <TabTitleText>{`Components (${filteredComponentElements?.length})`}</TabTitleText>}> + <TabTitleText>{`Kamelets (${filteredKameletElements?.length})`}</TabTitleText>}> </Tab> } </Tabs> @@ -241,9 +268,23 @@ export function DslSelector (props: Props) { isSelected={selectedLabels.includes(eipLabel)} onChange={selected => selectLabel(eipLabel)} />)} + <ToggleGroupItem key='clean' buttonId='clean' isSelected={false} onChange={clearSelectedLabels} icon={<TimesIcon/>}/> </ToggleGroup>} - <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery"> - {showSelector && filteredElement.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} + <Gallery key={"fast-gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '150px'}}> + {showSelector && elements + .filter((d: DslMetaModel) => { + if (isEip) { + return preferredElements.includes(d.dsl); + } else if (navigation === 'components') { + return d.uri && preferredElements.includes(d.uri) + } else { + return preferredElements.includes(d.name) + } + }) + .filter((_, i) => i < 7).map((dsl: DslMetaModel, index: number) => getFastCard(dsl, index))} + </Gallery> + <Gallery key={"gallery-" + navigation} hasGutter className="dsl-gallery" minWidths={{default: '200px'}}> + {showSelector && filteredElements.map((dsl: DslMetaModel, index: number) => getCard(dsl, index))} </Gallery> </PageSection> </Modal>