diff --git a/packages/ui/src/camel-utils/camel-to-table.adapter.test.ts b/packages/ui/src/camel-utils/camel-to-table.adapter.test.ts index 1e944857a..349746c88 100644 --- a/packages/ui/src/camel-utils/camel-to-table.adapter.test.ts +++ b/packages/ui/src/camel-utils/camel-to-table.adapter.test.ts @@ -219,7 +219,7 @@ describe('camelComponentToTable', () => { }); it('should return a apis IPropertiesTable with the correct values', () => { const table = camelComponentApisToTable({ apis: componentDef.apis!, apiProperties: componentDef.apiProperties! }); - expect(table.type).toEqual(PropertiesTableType.Simple); + expect(table.type).toEqual(PropertiesTableType.Tree); expect(table.headers).toContain(PropertiesHeaders.Name); expect(table.headers).toContain(PropertiesHeaders.Description); expect(table.headers).toContain(PropertiesHeaders.Type); @@ -248,7 +248,7 @@ describe('camelComponentToTable', () => { { apis: componentDef.apis!, apiProperties: componentDef.apiProperties! }, { filterKey: 'description', filterValue: 'whatever' }, ); - expect(table.type).toEqual(PropertiesTableType.Simple); + expect(table.type).toEqual(PropertiesTableType.Tree); expect(table.headers).toContain(PropertiesHeaders.Name); expect(table.headers).toContain(PropertiesHeaders.Description); expect(table.headers).toContain(PropertiesHeaders.Type); diff --git a/packages/ui/src/camel-utils/camel-to-table.adapter.ts b/packages/ui/src/camel-utils/camel-to-table.adapter.ts index aee51df85..f4db5c532 100644 --- a/packages/ui/src/camel-utils/camel-to-table.adapter.ts +++ b/packages/ui/src/camel-utils/camel-to-table.adapter.ts @@ -6,6 +6,7 @@ import { } from '../components/PropertiesModal'; import { ICamelComponentApi, + ICamelComponentApiKind, ICamelComponentApiProperty, ICamelComponentHeader, ICamelComponentProperty, @@ -109,8 +110,13 @@ export const camelComponentApisToTable = ( propertiesRows.push({ name: propertyName, description: property.description, - type: property.type, - rowAdditionalInfo: {}, + type: getClassNameOnly(property.javaType), + rowAdditionalInfo: { + required: property.required, + autowired: property.autowired, + enum: property.enum, + apiKind: ICamelComponentApiKind.PARAM + }, }); } methodsRows.push({ @@ -118,7 +124,9 @@ export const camelComponentApisToTable = ( description: method.description, type: '', children: propertiesRows, - rowAdditionalInfo: {}, + rowAdditionalInfo: { + apiKind: ICamelComponentApiKind.METHOD + }, }); } apisRows.push({ @@ -126,11 +134,13 @@ export const camelComponentApisToTable = ( description: api.description, type: getApiType(api.consumerOnly, api.producerOnly), children: methodsRows, - rowAdditionalInfo: {}, + rowAdditionalInfo: { + apiKind: ICamelComponentApiKind.API + }, }); } return { - type: PropertiesTableType.Simple, + type: PropertiesTableType.Tree, headers: [PropertiesHeaders.Name, PropertiesHeaders.Description, PropertiesHeaders.Type], rows: apisRows, }; diff --git a/packages/ui/src/components/PropertiesModal/PropertiesModal.md b/packages/ui/src/components/PropertiesModal/PropertiesModal.md index 6fefd7652..0b909550d 100644 --- a/packages/ui/src/components/PropertiesModal/PropertiesModal.md +++ b/packages/ui/src/components/PropertiesModal/PropertiesModal.md @@ -1,9 +1,15 @@ -### PropertiesModal +## PropertiesModal -This folder contains `PropertiesModal` component which defines how the properties detail modal with the table is rendered. The table is rendered according to `IPropertiesTable` object. `PropertiesModal` component decides which columns are shown according to `CatalogKind` enum (from the input tile) and use `camel-to-table.adapter.ts` util to get corresponding `IPropertiesTable` model for render. +This folder contains `PropertiesModal` component which defines how the properties detail modal with tabs and tables is rendered. The `PropertiesModal` component decides which tabs should be shown according to `CatalogKind` enum (from the input tile). For a particular `CatalogKind`, it uses a particular transform function from `camel-to-tabs.adapter.ts` util to get an array of `IPropertiesTab` for rendering. It passes all that info to the `PropertiesTabs` component which is responsible for rendering tables. The `PropertiesTabs` component renders all tabs from the input array of `IPropertiesTab` and for each tab, it renders all tables which are stored in `IPropertiesTab.tables`. According to a type of table, `Simple` or `Tree` table is rendered. + +That transformation functions in `camel-to-tabs.adapter.ts` contain a "definition" of tabs for each `CatalogKind` and according to that, it calls particular functions from `camel-to-table.adapter.ts` util to get all `IPropertiesTable` which are related to that `CatalogKind`. -To add a new column, extend `PropertiesTable.models.ts`, update `camel-to-table.adapter.ts` if the column is needed also for that definition, and update the table in `PropertiesModal`. -To add a new type of catalog definition, extend the switch in `PropertiesModal` which will cover that new type case. +That functions in `camel-to-table.adapter.ts` contain a "definition" of tables. + + +### How to update table to add a new column + +To add a new column, extend models in `PropertiesTable.models.ts` (`PropertiesHeaders` and `IPropertiesRow`), update `camel-to-table.adapter.ts` if the column is needed for a particular definition, and when the data in the cell contins special formation, add new case into `PropertiesTableCommon.tsx` __Make sure__ that orders of headers in the `IPropertiesTable.headers` match with the orders of element in a particular row `IPropertiesRow` @@ -21,6 +27,22 @@ e.g. required: "xyz", }], } +``` +### How to add brand new Catalog type -``` +To add a new type of catalog definition, extend the switch in `PropertiesModal` which will cover that new type case. After that create a tab transformation function into `camel-to-tabs.adapter.ts` where define which tabs will be rendered and which tables will be contained. If you need a new table type, add the table transformation function into `camel-to-table.adapter.ts` + +### How to add metadata information into table for existing cell + +If you need to pass some metadata information, e.g. for formatting existing cells (e.g. if Required==true => add Required as suffix of text), extend `IPropertiesRowAdditionalInfo` object about new information. That information will be available in `PropertiesTableCommon.tsx` where you can define what should be done according to that data. + +### Index + +- `PropertiesModal` - modal component +- `PropertiesTabs` - tabs component +- `PropertiesTableCommon` - the functions render headers row or data cells row. They contain cases when cell data needs some special formatting according to row metadata (`IPropertiesRow.rowAdditionalInfo`). The functions are common in both, Simple and Tree tables. +- `PropertiesTableSimple` - simple table which render data by rows +- `PropertiesTableTree` - tree table which render data by rows and childrens +- `camel-to-tabs` - functions which define how many tabs will be rendered for a particular catalog type, how they will look and which tables will be contained +- `camel-to-table` - functions which define what data will be in the table according to properties type diff --git a/packages/ui/src/components/PropertiesModal/PropertiesModal.models.ts b/packages/ui/src/components/PropertiesModal/PropertiesModal.models.ts index 4a117e92f..0dae9b267 100644 --- a/packages/ui/src/components/PropertiesModal/PropertiesModal.models.ts +++ b/packages/ui/src/components/PropertiesModal/PropertiesModal.models.ts @@ -1,3 +1,5 @@ +import { ICamelComponentApiKind } from '../../models'; + export const enum PropertiesTableType { Simple, Tree, @@ -12,13 +14,20 @@ export const enum PropertiesHeaders { Example = 'example', } +/** + * Metadata which is not rendered as a separate cell but according to which, some formatting is applied + */ export interface IPropertiesRowAdditionalInfo { required?: boolean; group?: string; autowired?: boolean; enum?: string[]; + apiKind?: ICamelComponentApiKind; } +/** + * Row for table with cells + */ export interface IPropertiesRow { property?: string; name: string; @@ -30,6 +39,9 @@ export interface IPropertiesRow { children?: IPropertiesRow[]; } +/** + * Whole table data for rendering + */ export type IPropertiesTable = { type: PropertiesTableType; headers: PropertiesHeaders[]; @@ -37,6 +49,9 @@ export type IPropertiesTable = { caption?: string; }; +/** + * Whole tab data for rendering + */ export interface IPropertiesTab { rootName: string; tables: IPropertiesTable[]; diff --git a/packages/ui/src/components/PropertiesModal/PropertiesModal.test.tsx b/packages/ui/src/components/PropertiesModal/PropertiesModal.test.tsx index 50f3066c3..d6af6844c 100644 --- a/packages/ui/src/components/PropertiesModal/PropertiesModal.test.tsx +++ b/packages/ui/src/components/PropertiesModal/PropertiesModal.test.tsx @@ -66,7 +66,7 @@ describe('Component tile', () => { description: 'Client api', methods: { send: { - description: 'Client send', + description: 'Send ediMessage to trading partner', signatures: [''], }, }, @@ -83,7 +83,26 @@ describe('Component tile', () => { }, }, }, - apiProperties: {}, + apiProperties: { + client: { + methods: { + send: { + properties: { + as2From: { + index: 0, + kind: 'parameter', + displayName: 'As2 From', + group: 'producer', + required: true, + type: 'String', + javaType: 'java.lang.String', + description: 'AS2 name of sender', + }, + }, + }, + }, + }, + }, }, }; @@ -161,17 +180,73 @@ describe('Component tile', () => { //tab 3 expect(screen.getByTestId('tab-3')).toHaveTextContent('APIs (2)'); - //headers + // //headers expect(screen.getByTestId('tab-3-table-0-header-name')).toHaveTextContent('name'); expect(screen.getByTestId('tab-3-table-0-header-description')).toHaveTextContent('description'); expect(screen.getByTestId('tab-3-table-0-header-type')).toHaveTextContent('type'); - // rows + // // rows + expect(screen.getByTestId('tab-3-table-0-row-0-cell-apiKind')).toHaveTextContent('Api'); expect(screen.getByTestId('tab-3-table-0-row-0-cell-name')).toHaveTextContent('client'); expect(screen.getByTestId('tab-3-table-0-row-0-cell-description')).toHaveTextContent('Client api'); expect(screen.getByTestId('tab-3-table-0-row-0-cell-type')).toHaveTextContent('Both'); - expect(screen.getByTestId('tab-3-table-0-row-1-cell-name')).toHaveTextContent('client2'); - expect(screen.getByTestId('tab-3-table-0-row-1-cell-description')).toHaveTextContent('Client2 api'); - expect(screen.getByTestId('tab-3-table-0-row-1-cell-type')).toHaveTextContent('Producer'); + + expect(screen.getByTestId('tab-3-table-0-row-1-cell-apiKind')).toHaveTextContent('Method'); + expect(screen.getByTestId('tab-3-table-0-row-1-cell-name')).toHaveTextContent('send'); + expect(screen.getByTestId('tab-3-table-0-row-1-cell-description')).toHaveTextContent( + 'Send ediMessage to trading partner', + ); + expect(screen.getByTestId('tab-3-table-0-row-1-cell-type')).toHaveTextContent(''); + + expect(screen.getByTestId('tab-3-table-0-row-2-cell-apiKind')).toHaveTextContent('Param'); + expect(screen.getByTestId('tab-3-table-0-row-2-cell-name')).toHaveTextContent('as2From'); + expect(screen.getByTestId('tab-3-table-0-row-2-cell-description')).toHaveTextContent('Required AS2 name of sender'); + expect(screen.getByTestId('tab-3-table-0-row-2-cell-type')).toHaveTextContent('String'); + + expect(screen.getByTestId('tab-3-table-0-row-3-cell-apiKind')).toHaveTextContent('Api'); + expect(screen.getByTestId('tab-3-table-0-row-3-cell-name')).toHaveTextContent('client2'); + expect(screen.getByTestId('tab-3-table-0-row-3-cell-description')).toHaveTextContent('Client2 api'); + expect(screen.getByTestId('tab-3-table-0-row-3-cell-type')).toHaveTextContent('Producer'); + }); + + it('switchs between tabs and apis', async () => { + // modal uses React portals so baseElement needs to be used here + render(); + // switch to API tab + expect(screen.getByTestId('tab-0-table-0-properties-modal-table-caption')).toBeVisible(); + expect(screen.getByTestId('tab-1-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-2-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-3-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-3')).toHaveAttribute('aria-selected', 'false'); + screen.getByTestId('tab-3').click(); + await new Promise(process.nextTick); + expect(screen.getByTestId('tab-0-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-1-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-2-table-0-properties-modal-table-caption')).not.toBeVisible(); + expect(screen.getByTestId('tab-3-table-0-properties-modal-table-caption')).toBeVisible(); + expect(screen.getByTestId('tab-3')).toHaveAttribute('aria-selected', 'true'); + + // expand api + expect(screen.getByLabelText('Expand row 0')).toHaveAttribute('aria-expanded', 'false'); + screen.getByLabelText('Expand row 0').click(); + await new Promise(process.nextTick); + expect(screen.getByLabelText('Collapse row 0')).toHaveAttribute('aria-expanded', 'true'); + + // expand method + expect(screen.getByLabelText('Expand row 1')).toHaveAttribute('aria-expanded', 'false'); + screen.getByLabelText('Expand row 1').click(); + await new Promise(process.nextTick); + expect(screen.getByLabelText('Collapse row 1')).toHaveAttribute('aria-expanded', 'true'); + + // close api, method is invisible but should still be expanded + screen.getByLabelText('Collapse row 0').click(); + await new Promise(process.nextTick); + expect(screen.getByLabelText('Expand row 0')).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByLabelText('Collapse row 1')).not.toBeVisible(); + expect(screen.getByLabelText('Collapse row 1')).toHaveAttribute('aria-expanded', 'true'); + screen.getByLabelText('Expand row 0').click(); + await new Promise(process.nextTick); + expect(screen.getByLabelText('Collapse row 1')).toBeVisible(); + expect(screen.getByLabelText('Collapse row 1')).toHaveAttribute('aria-expanded', 'true'); }); }); diff --git a/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableCommon.tsx b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableCommon.tsx new file mode 100644 index 000000000..edcf73a41 --- /dev/null +++ b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableCommon.tsx @@ -0,0 +1,74 @@ +import { List, ListItem } from '@patternfly/react-core'; +import { Td, Th } from '@patternfly/react-table'; +import { ReactNode } from 'react'; +import { IPropertiesRow, PropertiesHeaders } from '../PropertiesModal.models'; + +/** + * Returns table headers as array of + */ +export const renderHeaders = (propertiesHeaders: PropertiesHeaders[], rootDataTestId: string): ReactNode[] => { + return propertiesHeaders.map((header) => ( + + {header} + + )); +}; + +/** + * Returns table row cells as array of + */ +export const renderRowData = ( + propertiesHeaders: PropertiesHeaders[], + row: IPropertiesRow, + rootDataTestId: string, + row_index: number, +): ReactNode[] => { + const dataTestIdCell = rootDataTestId + '-cell-'; + + return propertiesHeaders.map((header) => ( + + { + //suffix required for description cell if needed + header == PropertiesHeaders.Description && row.rowAdditionalInfo.required ? ( + Required + ) : ( + '' + ) + } + { + //suffix autowired for description cell if needed + header == PropertiesHeaders.Description && row.rowAdditionalInfo.autowired ? ( + Autowired + ) : ( + '' + ) + } + {{row[header]?.toString()}} + { + //prefix with group for name cell if needed + header == PropertiesHeaders.Name && row.rowAdditionalInfo.group ? ( + ({row.rowAdditionalInfo.group}) + ) : ( + '' + ) + } + { + //prefix with enum for description cell if needed + header == PropertiesHeaders.Description && row.rowAdditionalInfo.enum ? ( + <> +

Enum values:

+ + {row.rowAdditionalInfo.enum.map((item, enum_index) => ( + + {item} + + ))} + + + ) : ( + '' + ) + } + + )); +}; diff --git a/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableSimple.tsx b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableSimple.tsx index ced120d7b..1215beb20 100644 --- a/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableSimple.tsx +++ b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableSimple.tsx @@ -1,7 +1,7 @@ -import { List, ListItem } from '@patternfly/react-core'; -import { Caption, Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { Caption, Table, Tbody, Thead, Tr } from '@patternfly/react-table'; import { FunctionComponent } from 'react'; -import { IPropertiesTable, PropertiesHeaders } from '../PropertiesModal.models'; +import { IPropertiesTable } from '../PropertiesModal.models'; +import { renderHeaders, renderRowData } from './PropertiesTableCommon'; interface IPropertiesTableSimpleProps { rootDataTestId: string; @@ -9,79 +9,17 @@ interface IPropertiesTableSimpleProps { } export const PropertiesTableSimple: FunctionComponent = (props) => { - const table = props.table; return ( - + - - {table.headers.map((header) => ( - - ))} - + {renderHeaders(props.table.headers, props.rootDataTestId)} - {table.rows.length != 0 && - table.rows.map((row, row_index) => ( + {props.table.rows.length != 0 && + props.table.rows.map((row, row_index) => ( - {table.headers.map((header) => ( - - ))} + {renderRowData(props.table.headers, row, props.rootDataTestId + '-row-' + row_index, row_index)} ))} diff --git a/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableTree.tsx b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableTree.tsx index 02310740e..67d9fe29d 100644 --- a/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableTree.tsx +++ b/packages/ui/src/components/PropertiesModal/Tables/PropertiesTableTree.tsx @@ -1,33 +1,85 @@ -import { TreeView, TreeViewDataItem } from '@patternfly/react-core'; +import { Caption, Table, Tbody, Td, TdProps, Th, Thead, Tr, TreeRowWrapper } from '@patternfly/react-table'; import * as React from 'react'; +import { FunctionComponent, useState } from 'react'; import { IPropertiesRow, IPropertiesTable } from '../PropertiesModal.models'; +import { renderHeaders, renderRowData } from './PropertiesTableCommon'; interface IPropertiesTableTreeProps { rootDataTestId: string; table: IPropertiesTable; } -const getChildren = (root: IPropertiesRow[]): TreeViewDataItem[] => { - const finalArray: TreeViewDataItem[] = [] - root.forEach((item) => { - let row: TreeViewDataItem = { - name: item.name - } - if (item.children){ - row.children = getChildren(item.children) +export const PropertiesTableTree: FunctionComponent = (props) => { + const [expandedNodeId, setExpandedNodeId] = useState([]); + /** + source: https://www.patternfly.org/components/table#tree-table + Recursive function which flattens the data into an array of flattened TreeRowWrapper components + params: + - nodes - array of a single level of tree nodes + - level - number representing how deeply nested the current row is + - posinset - position of the row relative to this row's siblings + - rowIndex - position of the row relative to the entire table, which is unique (used in expandedNodeId array to memory which rows are expanded) + - isHidden - defaults to false, true if this row's parent is expanded + */ + const renderTreeRows = ( + [node, ...remainingNodes]: IPropertiesRow[], + level = 1, + posinset = 1, + rowIndex = 0, + isHidden = false, + ): React.ReactNode[] => { + if (!node) { + return []; } - finalArray.push(row); - }) - return finalArray; -} + const isExpanded = expandedNodeId.includes(rowIndex); + + const treeRow: TdProps['treeRow'] = { + onCollapse: () => + setExpandedNodeId((prevExpanded) => { + const otherExpandedNodeNames = prevExpanded.filter((row_unique_id) => row_unique_id !== rowIndex); + return isExpanded ? otherExpandedNodeNames : [...otherExpandedNodeNames, rowIndex]; + }), + rowIndex, + props: { + isExpanded, + isHidden, + 'aria-level': level, + 'aria-posinset': posinset, + 'aria-setsize': node.children ? node.children.length : 0, + }, + }; + + const childRows = + node.children && node.children.length + ? renderTreeRows(node.children, level + 1, 1, rowIndex + 1, !isExpanded || isHidden) + : []; + + return [ + + + {renderRowData(props.table.headers, node, props.rootDataTestId + '-row-' + rowIndex, rowIndex)} + , + ...childRows, + ...renderTreeRows(remainingNodes, level, posinset + 1, rowIndex + 1 + childRows.length, isHidden), + ]; + }; -export const PropertiesTableTree: React.FunctionComponent = (props) => { - const data = getChildren(props.table.rows); return ( - +
{table.caption}{props.table.caption}
- {header} -
- { - //suffix required for description cell if needed - header == PropertiesHeaders.Description && row.rowAdditionalInfo.required ? ( - Required - ) : ( - '' - ) - } - { - //suffix autowired for description cell if needed - header == PropertiesHeaders.Description && row.rowAdditionalInfo.autowired ? ( - Autowired - ) : ( - '' - ) - } - {{row[header]?.toString()}} - { - //prefix with group for name cell if needed - header == PropertiesHeaders.Name && row.rowAdditionalInfo.group ? ( - ({row.rowAdditionalInfo.group}) - ) : ( - '' - ) - } - { - //prefix with enum for description cell if needed - header == PropertiesHeaders.Description && row.rowAdditionalInfo.enum ? ( - <> -

Enum values:

- - {row.rowAdditionalInfo.enum.map((item, enum_index) => ( - - {item} - - ))} - - - ) : ( - '' - ) - } -
+ {node.rowAdditionalInfo.apiKind?.toString()} +
+ + + + + {renderHeaders(props.table.headers, props.rootDataTestId)} + + + {renderTreeRows(props.table.rows)} +
{props.table.caption}
Kind
); }; diff --git a/packages/ui/src/models/camel-components-catalog.ts b/packages/ui/src/models/camel-components-catalog.ts index 12a8cdc72..aa779b6b9 100644 --- a/packages/ui/src/models/camel-components-catalog.ts +++ b/packages/ui/src/models/camel-components-catalog.ts @@ -64,3 +64,9 @@ export interface ICamelComponentApiProperty { export interface ICamelComponentApiPropertyMethod { properties: Record; // api.method.property is same as camelcomponentproperty } + +export enum ICamelComponentApiKind { + API='Api', + METHOD='Method', + PARAM='Param', +}