This is an automated email from the ASF dual-hosted git repository. mgubaidullin pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit 79180ba14ccd47c21aed69338cf8f8858efe8b9a Author: Marat Gubaidullin <[email protected]> AuthorDate: Mon Aug 24 17:56:15 2026 -0400 Karavan-app UI Page Docs --- .../src/ui/page-documentation/Documentation.css | 136 ++++++++++++++ .../ui/page-documentation/DocumentationPage.tsx | 209 +++++++++++++++++++++ .../components/ComponentCard.tsx | 79 ++++++++ .../components/ComponentModal.tsx | 160 ++++++++++++++++ .../components/ComponentsTab.tsx | 46 +++++ .../src/ui/page-documentation/eip/EipCard.tsx | 59 ++++++ .../src/ui/page-documentation/eip/EipModal.tsx | 95 ++++++++++ .../webui/src/ui/page-documentation/eip/EipTab.tsx | 47 +++++ .../ui/page-documentation/useDocumentationHook.tsx | 25 +++ 9 files changed, 856 insertions(+) diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css b/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css new file mode 100644 index 00000000..cde2d913 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/Documentation.css @@ -0,0 +1,136 @@ +/* + * 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 .documentation-section { + height: 100%; + overflow: auto; + + .documentation-eip-section { + padding: 16px; + background-color: transparent; + } +} +.karavan .documentation-section .documentation-card { + cursor: pointer; + height: 160px; +} + +.karavan .documentation-section .documentation-card .pf-v6-c-card__header { + padding: 8px; +} + +.karavan .documentation-section .documentation-card .pf-v6-c-card__header .custom { + margin-left: auto; +} + +.karavan .documentation-section .documentation-card .pf-v6-c-card__header .pf-v6-c-card__header-main { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +.karavan .documentation-section .documentation-card .pf-v6-c-card__title { + font-size: 15px; + font-weight: 400; +} + +.karavan .documentation-section .documentation-card .pf-v6-c-card__body { + overflow: hidden; + position: relative; + line-height: 1.6em; + padding-left: 8px; +} + +.documentation-section .documentation-card .icon { + height: 24px; + max-width: 24px; + margin-top: auto; + margin-bottom: auto; + border: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.pf-v6-theme-dark .documentation-section .documentation-card .icon { + background-color: var(--pf-t--color--gray--40); + border-color: var(--pf-t--color--gray--40); + border-style: solid; + border-width: 1px; + border-radius: 24px; +} + +.karavan .documentation-section .documentation-card .header-labels { + padding: 5px; + display: flex; + flex-direction: row; + justify-content: flex-end; +} + +.karavan .documentation-section .documentation-card .header-labels .pf-v6-c-card__header-main { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.karavan .documentation-section .documentation-card .footer-labels { + padding: 5px; + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.karavan .documentation-section .documentation-card .version, +.karavan .documentation-section .documentation-card .support-type, +.karavan .documentation-section .documentation-card .support-level { + white-space: nowrap; +} + +.karavan .documentation-section .documentation-card .labels { + opacity: 0.5; + font-weight: 200; +} + +.karavan .documentation-section .documentation-card:hover .labels { + opacity: 1; +} + +.documentation-section .documentation-card .block-checkbox { + align-self: center; + margin-left: 6px; +} + +.documentation-section .documentation-card .pf-v6-c-card__body { + padding-bottom: 0; + height: 54px; +} + +.documentation-section .documentation-card .pf-v6-c-card__body { + padding-bottom: 0; + height: 54px; +} + +.documentation-section .documentation-card p { + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + color: var(--pf-t--global--text--color--subtle); +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx new file mode 100644 index 00000000..b937a564 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/DocumentationPage.tsx @@ -0,0 +1,209 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, {useEffect, useState} from 'react'; +import {Badge, capitalize, Content, Label, Switch, Tab, Tabs, TabTitleText, TextInput} from "@patternfly/react-core"; +import {EipTab} from "./eip/EipTab"; +import {ComponentsTab} from "./components/ComponentsTab"; +import {ComponentApi} from "@core/api/ComponentApi"; +import {CamelModelMetadata, ElementMeta} from "@core/model/CamelMetadata"; +import {RightPanel} from "@shared/ui/RightPanel"; +import './Documentation.css' +import {ProjectService} from "@services/ProjectService"; +import {Project, ProjectType} from "@models/ProjectModels"; +import {useFilesStore, useFileStore, useProjectStore} from "@stores/ProjectStore"; +import {shallow} from "zustand/shallow"; +import {extractTitleFromMarkdown} from "@utils/StringUtils"; +import MarkdownPreview from "@uiw/react-markdown-preview"; +import {ErrorBoundaryWrapper} from "@shared/ui/ErrorBoundaryWrapper"; +import {useTheme} from "@compass/theme/ThemeContext"; +import {SourcesTab} from "@page-project/files/SourcesTab"; +import {DeveloperManager} from "@developer/DeveloperManager"; +import {SourcesDrawerPanel} from "@page-project/files/SourcesDrawerPanel"; +import {TabProps} from "@patternfly/react-core/src/components/Tabs/Tab"; +import {useDocumentationHook} from "./useDocumentationHook"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {CAMEL_STEP_ELEMENTS} from "@designer/utils/CamelUi"; + +const BUILD_IN_DOCUMENTATION_PAGES = ['processors', 'components', 'source'] + +export const DocumentationPage = () => { + + const {isDark} = useTheme(); + const {loadData} = useDocumentationHook(); + const [files] = useFilesStore((s) => [s.files], shallow); + const [showBlocked, setShowBlocked] = useDocumentationStore((s) => [s.showBlocked, s.setShowBlocked], shallow); + const [setProject] = useProjectStore((s) => [s.setProject], shallow); + const [file, operation, setFile] = useFileStore((s) => [s.file, s.operation, s.setFile], shallow); + const showFilePanel = file !== undefined && operation === 'select'; + const [pages, setPages] = useState<string[]>(BUILD_IN_DOCUMENTATION_PAGES); + const [pageNames, setPageNames] = useState<string[]>(BUILD_IN_DOCUMENTATION_PAGES.map(item => capitalize(item))); + const [filter, setFilter] = useState<string>(""); + const [markdown, setMarkdown] = useState<string>(""); + const [activeItem, setActiveItem] = useState<string>(); + + const onNavSelect = (event: React.MouseEvent<HTMLElement, MouseEvent>, eventKey: TabProps['eventKey']) => { + setFile('none', undefined); + setActiveItem(eventKey?.toString()); + } + + useEffect(() => { + setProject(new Project({projectId: ProjectType.documentation, name: ProjectType.documentation, type: ProjectType.documentation}), "none"); + ProjectService.refreshProjectFiles(ProjectType.documentation); + loadData(); + return () => { + setProject(new Project(), "none"); + } + }, []); + + useEffect(() => { + if (!activeItemIsBuildIn()) { + const m = files.filter(f => f.name === activeItem)?.[0]?.code || ''; + setMarkdown(m) + } + }, [activeItem]); + + useEffect(() => { + const newPages: string[] = []; + const newPageNames: string[] = []; + files.filter(file => file.name.endsWith(".md")).sort((a, b) => a.name > b.name ? 1 : -1).forEach(file => { + newPages.push(file.name); + newPageNames.push(extractTitleFromMarkdown(file.code) || file.name); + }); + newPages.push(...BUILD_IN_DOCUMENTATION_PAGES); + newPageNames.push(...BUILD_IN_DOCUMENTATION_PAGES.map(item => capitalize(item))); + setPages(newPages); + setPageNames(newPageNames); + if (activeItem === undefined) { + setActiveItem(newPages[0]) + } + }, [files]); + + const allComponents = ComponentApi.getComponents(); + const availableComponents = allComponents?.filter(c => !ComponentApi.getBlockedComponentNames().includes(c.component.name)); + const components = allComponents + .filter(c => { + return showBlocked || !ComponentApi.getBlockedComponentNames().includes(c.component.name) + }) + .filter(c => { + return c.component?.name?.toLowerCase().includes(filter.toLowerCase()) + || c.component?.title?.toLowerCase().includes(filter.toLowerCase()) + || c.component?.description?.toLowerCase().includes(filter.toLowerCase()) + }).sort((a, b) => (a.component.title?.toLowerCase() > b.component.title?.toLowerCase() ? 1 : -1)); + const allComponentsQuantity = allComponents?.length; + const availableComponentsQuantity = availableComponents?.length; + + const processors = CamelModelMetadata + .filter(c => CAMEL_STEP_ELEMENTS.includes(c.className)) + .filter(c => c.name.toLowerCase().includes(filter.toLowerCase())).sort((a: ElementMeta, b: ElementMeta) => a.name > b.name ? 1 : -1); + + const processorsQuantity = processors?.length; + + function activeItemIsBuildIn() { + return activeItem && BUILD_IN_DOCUMENTATION_PAGES.includes(activeItem); + } + + function getNavigation() { + return ( + <Tabs onSelect={onNavSelect} isNav activeKey={activeItem}> + {pages.map((item, i) => { + return ( + <Tab + key={item} + eventKey={item} + title={<TabTitleText>{pageNames[i]}</TabTitleText>} + /> + ) + })} + </Tabs> + ) + } + + function title() { + return <Content> + <Content component="h2">Documentation</Content> + </Content> + } + + function getTools() { + return <div> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6}}> + {activeItem === 'components' && + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}> + <Label variant={'outline'}> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}> + <p>Available</p> + <Badge>{availableComponentsQuantity}</Badge> + <p>from</p> + <Badge isRead>{allComponentsQuantity}</Badge> + </div> + </Label> + <Switch + label="Show all" + isReversed + isChecked={showBlocked} + onChange={(_event, checked) => setShowBlocked(checked)} + /> + </div> + } + {activeItem === 'processors' && + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}> + <Label variant={'outline'}> + <div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6, textWrap: 'nowrap'}}> + <p>Available</p> + <Badge>{processorsQuantity}</Badge> + </div> + </Label> + </div> + } + <TextInput className="text-field" type="search" id="search" name="search" + value={filter} + onChange={(_event, value) => setFilter(value)} + autoComplete="off" + placeholder="Search by name"/> + </div> + </div> + } + + return ( + <RightPanel + title={title()} + toolsStart={getNavigation()} + tools={activeItemIsBuildIn() ? getTools() : undefined} + drawerPanel={<SourcesDrawerPanel/>} + mainPanel={ + <div className="right-panel-card"> + <div className="documentation-section"> + {!showFilePanel && activeItem === 'processors' && <EipTab elements={processors}/>} + {!showFilePanel && activeItem === 'components' && <ComponentsTab components={components}/>} + {!showFilePanel && activeItem === 'source' && <SourcesTab/>} + {showFilePanel && <DeveloperManager/>} + {!activeItemIsBuildIn() && + <ErrorBoundaryWrapper onError={error => console.error((error))}> + <MarkdownPreview key={"DocumentationMarkdownPreview"} + source={markdown} + className="documentation-eip-section" + wrapperElement={{'data-color-mode': isDark ? 'dark' : 'light'}} + /> + </ErrorBoundaryWrapper> + } + </div> + </div> + } + /> + ) + +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx new file mode 100644 index 00000000..5172fee7 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentCard.tsx @@ -0,0 +1,79 @@ +/* + * 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 {Badge, Card, CardBody, CardFooter, CardHeader, CardTitle, Checkbox, Content, Flex, Tooltip} from '@patternfly/react-core'; +import {shallow} from "zustand/shallow"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {Component} from "@core/model/ComponentModels"; +import {CamelUi} from "@designer/utils/CamelUi"; +import {useDocumentationHook} from "../useDocumentationHook"; +import {ComponentApi} from "@core/api/ComponentApi"; + +interface Props { + component: Component, +} + +export function ComponentCard(props: Props) { + + const {onChangeBlockedList} = useDocumentationHook(); + const [setComponent, setModalOpen] + = useDocumentationStore((s) => + [s.setComponent, s.setModalOpen], shallow) + const component = props.component; + + function click(event: React.MouseEvent) { + const {target} = event; + if (!(target as HTMLElement).parentElement?.className.includes("block-checkbox")) { + setComponent(component) + setModalOpen(true); + } + } + + const isBlockedComponent = ComponentApi.getBlockedComponentNames().includes(component.component.name); + const isRemote = component.component.remote; + const classNameBadge = 'label-component' + (component.component.supportLevel !== 'Stable' ? '-preview' : '') + return ( + <Card isCompact key={component.component.name} className="documentation-card" + onClick={event => click(event)} + > + <CardHeader className="header-labels"> + <Flex style={{width: '100%'}} gap={{default: 'gapSm'}} justifyContent={{default: 'justifyContentSpaceBetween'}}> + <Badge className={classNameBadge}>Component</Badge> + <Badge isRead className="support-level labels">{component.component.supportLevel}</Badge> + </Flex> + <Tooltip content={isBlockedComponent ? 'Unblock' : 'Block'}> + <Checkbox id={component.component.name} + className="block-checkbox labels" + isChecked={!isBlockedComponent} + onChange={(_, checked) => onChangeBlockedList("component", component.component.name, checked)} + /> + </Tooltip> + </CardHeader> + <CardHeader> + {CamelUi.getIconForComponent(component.component.title, component.component.label)} + <CardTitle>{component.component.title}</CardTitle> + </CardHeader> + <CardBody> + <Content component="p" className="-pf-v6-u-color-200">{component.component.description}</Content> + </CardBody> + <CardFooter className="footer-labels"> + <Badge isRead className="labels">{component.component.label}</Badge> + <Badge isRead className="labels">{isRemote ? 'remote' : 'internal'}</Badge> + </CardFooter> + </Card> + ) +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx new file mode 100644 index 00000000..5cad8ee0 --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentModal.tsx @@ -0,0 +1,160 @@ +/* + * 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 {Badge, Button, CardHeader, Content, Flex, Modal, ModalBody, ModalFooter, ModalHeader, Tab, Tabs, TabTitleText} from '@patternfly/react-core'; + +import '@designer/karavan.css'; +import {Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; +import {CamelUi} from "@designer/utils/CamelUi"; +import {ComponentApi} from "@core/api/ComponentApi"; +import {ComponentHeader, ComponentProperty} from "@core/model/ComponentModels"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {shallow} from "zustand/shallow"; + +export function ComponentModal() { + + const [component, isModalOpen, setModalOpen] = useDocumentationStore((s) => + [s.component, s.isModalOpen, s.setModalOpen], shallow) + + const [tab, setTab] = useState<string | number>('properties'); + + const props = new Map<string, ComponentProperty>(); + if (component) { + ComponentApi.getComponentProperties(component?.component.name, "consumer").forEach(cp => props.set(cp.name, cp)); + ComponentApi.getComponentProperties(component?.component.name, "producer").forEach(cp => props.set(cp.name, cp)); + } + + const headers = new Map<string, ComponentHeader>(); + if (component) { + ComponentApi.getComponentHeaders(component?.component.name, "consumer").forEach(cp => headers.set(cp.name, cp)); + ComponentApi.getComponentHeaders(component?.component.name, "producer").forEach(cp => headers.set(cp.name, cp)); + } + + + function getPropertiesTable() { + return ( + <Table aria-label="Properties table" variant='compact'> + <Thead> + <Tr> + <Th key='name'>Display Name / Name</Th> + <Th key='desc'>Description</Th> + <Th key='type'>Type</Th> + <Th key='label'>Label</Th> + </Tr> + </Thead> + <Tbody> + {Array.from(props.values()).map((p: ComponentProperty, idx: number) => ( + <Tr key={idx}> + <Td key={`${idx}_name`}> + <div> + <b>{p.displayName}</b> + <div>{p.name}</div> + </div> + </Td> + <Td key={`${idx}_desc`}> + <div> + <div>{p.description}</div> + {p.defaultValue && p.defaultValue.toString().length > 0 && + <div>{"Default value: " + p.defaultValue}</div>} + </div> + </Td> + <Td key={`${idx}_type`}>{p.type}</Td> + <Td key={`${idx}_label`}>{p.label}</Td> + </Tr> + ))} + </Tbody> + </Table> + ) + } + + function getHeadersTable() { + return ( + <Table aria-label="Headers table" variant='compact'> + <Thead> + <Tr> + <Th key='name'>Name</Th> + <Th key='desc' modifier={"breakWord"}>Description</Th> + <Th key='type'>Group</Th> + <Th key='label'>Java Type</Th> + <Th key='label'>Default Value</Th> + <Th key='label'>Autowired</Th> + </Tr> + </Thead> + <Tbody> + {Array.from(headers.values()).map((p: ComponentHeader, idx: number) => ( + <Tr key={idx}> + <Td key={`${idx}_name`}> + <div> + <b>{p.displayName}</b> + <div>{p.name}</div> + </div> + </Td> + <Td key={`${idx}_type`}>{p.description}</Td> + <Td key={`${idx}_label`}>{p.group}</Td> + <Td key={`${idx}_label`}>{p.javaType}</Td> + <Td key={`${idx}_label`}>{p.defaultValue}</Td> + <Td key={`${idx}_label`}>{p.autowired}</Td> + </Tr> + ))} + </Tbody> + </Table> + ) + } + + const showProps = props.size !== 0; + const showHeaders = headers.size !== 0; + + return ( + <Modal + aria-label={"Component"} + isOpen={isModalOpen} + onClose={() => setModalOpen(false)} + > + <ModalHeader title={component?.component.title}/> + <ModalBody> + <Flex direction={{default: 'column'}} key={component?.component.name} + className="kamelet-modal-card"> + <CardHeader actions={{ + actions: <><Badge className="badge" + isRead> {component?.component.label}</Badge></>, + hasNoOffset: false, + className: undefined + }}> + {component && CamelUi.getIconForComponent(component.component.title, component.component.label)} + + </CardHeader> + <Content component="p" className="description">{component?.component.description}</Content> + <Tabs + activeKey={tab} + onSelect={(event, eventKey) => setTab(eventKey)} + aria-label="Tabs in the default example" + role="region" + > + <Tab eventKey={'properties'} title={<TabTitleText>Properties</TabTitleText>}/> + <Tab eventKey={'headers'} title={<TabTitleText>Headers</TabTitleText>}/> + </Tabs> + {tab === 'properties' && showProps && getPropertiesTable()} + {tab === 'headers' && showHeaders && getHeadersTable()} + </Flex> + </ModalBody> + <ModalFooter> + <Button key="cancel" variant="primary" + onClick={e => setModalOpen(false)}>Close</Button> + </ModalFooter> + </Modal> + ) +} diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx new file mode 100644 index 00000000..36a8206c --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/components/ComponentsTab.tsx @@ -0,0 +1,46 @@ +/* + * 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 {Gallery} from '@patternfly/react-core'; +import {ComponentCard} from "./ComponentCard"; +import {ComponentModal} from "./ComponentModal"; +import {shallow} from "zustand/shallow"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {Component} from '@core/model/ComponentModels'; + +interface Props { + components: Component[], +} + +export function ComponentsTab(props: Props) { + + const [isModalOpen] = useDocumentationStore((s) => [s.isModalOpen], shallow) + + const {components} = props; + return ( + <div className="documentation-eip-section"> + {isModalOpen && <ComponentModal/>} + <div className="kamelets-page"> + <Gallery hasGutter> + {components.map(c => ( + <ComponentCard key={c.component.name} component={c}/> + ))} + </Gallery> + </div> + </div> + ) +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx new file mode 100644 index 00000000..42c3b00a --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipCard.tsx @@ -0,0 +1,59 @@ +/* + * 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 {Badge, Card, CardBody, CardFooter, CardHeader, CardTitle, Content} from '@patternfly/react-core'; +import {CamelUi} from "@designer/utils/CamelUi"; +import {ElementMeta} from "@core/model/CamelMetadata"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {shallow} from "zustand/shallow"; + +interface Props { + element: ElementMeta, +} + +export function EipCard(props: Props) { + + const [setElement, setModalOpen] = useDocumentationStore((s) => + [s.setElement, s.setModalOpen], shallow) + + const element = props.element; + + function click (event: React.MouseEvent) { + setElement(element) + setModalOpen(true); + } + return ( + <Card isCompact key={element.name} className="documentation-card" onClick={event => click(event)}> + <CardHeader> + <Badge className='label-processor'>Processor</Badge> + </CardHeader> + <CardHeader> + {CamelUi.getIconForDslName(element.className)} + <CardTitle>{element.title}</CardTitle> + </CardHeader> + <CardBody> + <Content component="p">{element.description}</Content> + </CardBody> + <CardFooter className="footer-labels"> + <div> + {element.labels.split(',').map((s: string, i: number) => <Badge key={s + i} isRead + className="labels">{s}</Badge>)} + </div> + </CardFooter> + </Card> + ) +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx new file mode 100644 index 00000000..21ab9cdc --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipModal.tsx @@ -0,0 +1,95 @@ +/* + * 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 {Badge, Button, CardHeader, CardTitle, Content, Flex, Modal, ModalBody, ModalFooter, ModalHeader} from '@patternfly/react-core'; + +import '@designer/karavan.css'; +import {Table, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; +import {CamelUi} from "@designer/utils/CamelUi"; +import {PropertyMeta} from "@core/model/CamelMetadata"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {shallow} from "zustand/shallow"; + + +export function EipModal() { + + const [element, isModalOpen, setModalOpen] = useDocumentationStore((s) => + [s.element, s.isModalOpen, s.setModalOpen], shallow) + + return ( + <Modal + aria-label={"Processor"} + isOpen={isModalOpen} + onClose={() => setModalOpen(false)} + > + <ModalHeader title={element?.title}/> + <ModalBody> + <Flex direction={{default: 'column'}} key={element?.name} className="kamelet-modal-card"> + <CardHeader actions={{ + actions: <><Badge className="badge" + isRead> {element?.labels}</Badge></>, hasNoOffset: false, className: undefined + }}> + {element && CamelUi.getIconForDslName(element?.className)} + + </CardHeader> + <Content component="p" className="description">{element?.description}</Content> + {element?.properties.length !== 0 && + <div> + <CardTitle>Properties</CardTitle> + <Table aria-label="Simple table" variant='compact'> + <Thead> + <Tr> + <Th key='name' width={10}>Name</Th> + <Th key='label'>Label</Th> + <Th key='display' width={10}>Display Name</Th> + <Th key='desc'>Description</Th> + <Th key='type'>Type</Th> + </Tr> + </Thead> + <Tbody> + {element?.properties.map((p: PropertyMeta, idx: number) => ( + <Tr key={idx}> + <Td modifier={"fitContent"}> + {p.name} + </Td> + <Td modifier={"fitContent"}> + <Badge className="badge" isRead>{p.label}</Badge> + </Td> + <Td modifier={"fitContent"}> + {p.displayName} + </Td> + <Td key={`${idx}_desc`}> + <div> + <div>{p.description}</div> + {p.defaultValue && p.defaultValue.toString().length > 0 && <div>{"Default value: " + p.defaultValue}</div>} + </div> + </Td> + <Td key={`${idx}_type`}>{p.type}</Td> + </Tr> + ))} + </Tbody> + </Table> + </div> + } + </Flex> + </ModalBody> + <ModalFooter> + <Button key="cancel" variant="primary" onClick={e => setModalOpen(false)}>Close</Button> + </ModalFooter> + </Modal> + ) +} diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx new file mode 100644 index 00000000..b76e47aa --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/eip/EipTab.tsx @@ -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. + */ +import React from 'react'; +import {Gallery,} from '@patternfly/react-core'; +import {EipCard} from "./EipCard"; +import {EipModal} from "./EipModal"; +import {useDocumentationStore} from "@stores/DocumentationStore"; +import {shallow} from "zustand/shallow"; +import {ElementMeta} from "@core/model/CamelMetadata"; + +interface Props { + elements: ElementMeta[], +} + +export function EipTab(props: Props) { + + const [isModalOpen] = useDocumentationStore((s) => [s.isModalOpen], shallow) + + const { elements } = props; + + return ( + <div className="documentation-eip-section"> + {isModalOpen && <EipModal/>} + <div className="kamelets-page"> + <Gallery hasGutter> + {elements.map(c => ( + <EipCard key={c.name} element={c}/> + ))} + </Gallery> + </div> + </div> + ) +} \ No newline at end of file diff --git a/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx b/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx new file mode 100644 index 00000000..57fc4bdd --- /dev/null +++ b/karavan-app/src/main/webui/src/ui/page-documentation/useDocumentationHook.tsx @@ -0,0 +1,25 @@ +import {ProjectFile, ProjectType} from "@models/ProjectModels"; +import {ProjectService} from "@services/ProjectService"; +import {ComponentApi} from "@core/api/ComponentApi"; +import {useFilesStore} from "@stores/ProjectStore"; + +export function useDocumentationHook() { + + const saveFile = useFilesStore((s) => s.saveFile); + + const loadData = async() => { + ProjectService.loadCustomKamelets(); + ProjectService.loadBlockedComponentAndKamelets(); + } + + async function onChangeBlockedList (type: string, name: string, checked: boolean) { + let fileContent = ''; + if (type === "component") { + fileContent = ComponentApi.saveBlockedComponentName(name, checked).join('\n'); + } + const file = new ProjectFile(type + "-blocklist.txt", ProjectType.configuration, fileContent, Date.now()); + saveFile(file); + } + + return { loadData, onChangeBlockedList}; +} \ No newline at end of file
